Firebaseクラウド機能が非常に遅い


131

現在、新しいfirebaseクラウド機能を使用するアプリケーションに取り組んでいます。現在起こっているのは、トランザクションがキューノードに置かれていることです。そして、関数はそのノードを削除し、それを正しいノードに配置します。これは、オフラインで作業できるために実装されました。

現在の問題は、関数の速度です。関数自体は約400msかかるので、それで問題ありません。ただし、エントリがすでにキューに追加されている間、関数に非常に長い時間がかかる場合があります(約8秒)。

サーバーが起動するのに時間がかかると思われます。これは、最初のアクションをもう一度実行するときに発生するためです。時間がかかりません。

この問題を解決する方法はありますか?ここで、関数のコードを追加しました。何も問題はないようですが、念のため追加しました。

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const database = admin.database();

exports.insertTransaction = functions.database
    .ref('/userPlacePromotionTransactionsQueue/{userKey}/{placeKey}/{promotionKey}/{transactionKey}')
    .onWrite(event => {
        if (event.data.val() == null) return null;

        // get keys
        const userKey = event.params.userKey;
        const placeKey = event.params.placeKey;
        const promotionKey = event.params.promotionKey;
        const transactionKey = event.params.transactionKey;

        // init update object
        const data = {};

        // get the transaction
        const transaction = event.data.val();

        // transfer transaction
        saveTransaction(data, transaction, userKey, placeKey, promotionKey, transactionKey);
        // remove from queue
        data[`/userPlacePromotionTransactionsQueue/${userKey}/${placeKey}/${promotionKey}/${transactionKey}`] = null;

        // fetch promotion
        database.ref(`promotions/${promotionKey}`).once('value', (snapshot) => {
            // Check if the promotion exists.
            if (!snapshot.exists()) {
                return null;
            }

            const promotion = snapshot.val();

            // fetch the current stamp count
            database.ref(`userPromotionStampCount/${userKey}/${promotionKey}`).once('value', (snapshot) => {
                let currentStampCount = 0;
                if (snapshot.exists()) currentStampCount = parseInt(snapshot.val());

                data[`userPromotionStampCount/${userKey}/${promotionKey}`] = currentStampCount + transaction.amount;

                // determines if there are new full cards
                const currentFullcards = Math.floor(currentStampCount > 0 ? currentStampCount / promotion.stamps : 0);
                const newStamps = currentStampCount + transaction.amount;
                const newFullcards = Math.floor(newStamps / promotion.stamps);

                if (newFullcards > currentFullcards) {
                    for (let i = 0; i < (newFullcards - currentFullcards); i++) {
                        const cardTransaction = {
                            action: "pending",
                            promotion_id: promotionKey,
                            user_id: userKey,
                            amount: 0,
                            type: "stamp",
                            date: transaction.date,
                            is_reversed: false
                        };

                        saveTransaction(data, cardTransaction, userKey, placeKey, promotionKey);

                        const completedPromotion = {
                            promotion_id: promotionKey,
                            user_id: userKey,
                            has_used: false,
                            date: admin.database.ServerValue.TIMESTAMP
                        };

                        const promotionPushKey = database
                            .ref()
                            .child(`userPlaceCompletedPromotions/${userKey}/${placeKey}`)
                            .push()
                            .key;

                        data[`userPlaceCompletedPromotions/${userKey}/${placeKey}/${promotionPushKey}`] = completedPromotion;
                        data[`userCompletedPromotions/${userKey}/${promotionPushKey}`] = completedPromotion;
                    }
                }

                return database.ref().update(data);
            }, (error) => {
                // Log to the console if an error happened.
                console.log('The read failed: ' + error.code);
                return null;
            });

        }, (error) => {
            // Log to the console if an error happened.
            console.log('The read failed: ' + error.code);
            return null;
        });
    });

function saveTransaction(data, transaction, userKey, placeKey, promotionKey, transactionKey) {
    if (!transactionKey) {
        transactionKey = database.ref('transactions').push().key;
    }

    data[`transactions/${transactionKey}`] = transaction;
    data[`placeTransactions/${placeKey}/${transactionKey}`] = transaction;
    data[`userPlacePromotionTransactions/${userKey}/${placeKey}/${promotionKey}/${transactionKey}`] = transaction;
}

上記の「once()」呼び出しのPromiseを返さないのは安全ですか?
jazzgil 2017年

回答:


111

ここにファイアベーサー

関数のいわゆるコールドスタートが発生しているようです。

関数がしばらく実行されていない場合、Cloud Functionsは関数をより少ないリソースを使用するモードにします。次に、関数をもう一度押すと、このモードから環境が復元されます。復元にかか​​る時間は、固定コスト(例:コンテナーの復元)と一部変動コスト(例:多数のノードモジュールを使用する場合、さらに時間がかかる場合があります)で構成されます。

これらの操作のパフォーマンスを継続的に監視して、開発者のエクスペリエンスとリソースの使用量の最適な組み合わせを確保しています。したがって、これらの時間は時間とともに改善すると予想されます。

良い知らせは、これは開発中にのみ経験すべきであるということです。本番環境で関数が頻繁にトリガーされると、コールドスタートに戻ることはほとんどありません。


3
モデレーターのメモ:この投稿のトピック外のコメントはすべて削除されました。コメントを使用して、説明を要求するか、改善のみを提案してください。関連しているが異なる質問がある場合は、新しい質問をし、この質問へのリンクを含めてコンテキストを提供します。
Bhargav Rao

55

2020年5月更新 maganapによるコメントに感謝-ノード10+ FUNCTION_NAMEはに置き換えられますK_SERVICEFUNCTION_TARGETは関数自体であり、名前ではなくに置き換えられますENTRY_POINT)。以下のコードサンプルは以下に更新されています。

https://cloud.google.com/functions/docs/migrating/nodejs-runtimes#nodejs-10-changesで詳細をご覧ください。

更新 -これらの問題の多くは、次のように隠し変数を使用して解決できるようprocess.env.FUNCTION_NAMEです:https : //github.com/firebase/functions-samples/issues/170#issuecomment-323375462

コードで更新 -たとえば、次のインデックスファイルがあるとします。

...
exports.doSomeThing = require('./doSomeThing');
exports.doSomeThingElse = require('./doSomeThingElse');
exports.doOtherStuff = require('./doOtherStuff');
// and more.......

次に、すべてのファイルが読み込まれ、それらのファイルの要件もすべて読み込まれるため、オーバーヘッドが大きくなり、すべての関数のグローバルスコープが汚染されます。

代わりにインクルードを次のように分離します:

const function_name = process.env.FUNCTION_NAME || process.env.K_SERVICE;
if (!function_name || function_name === 'doSomeThing') {
  exports.doSomeThing = require('./doSomeThing');
}
if (!function_name || function_name === 'doSomeThingElse') {
  exports.doSomeThingElse = require('./doSomeThingElse');
}
if (!function_name || function_name === 'doOtherStuff') {
  exports.doOtherStuff = require('./doOtherStuff');
}

これにより、その関数が具体的に呼び出されたときにのみ必要なファイルがロードされます。グローバルスコープをよりクリーンに保つことができるため、コールドブートが高速になります。


これにより、以下で行ったものよりもはるかに優れたソリューションが可能になります(ただし、以下の説明は引き続き有効です)。


元の回答

ファイルが必要で、グローバルスコープで発生する一般的な初期化は、コールドブート時のスローダウンの大きな原因のようです。

プロジェクトがより多くの関数を取得するにつれて、グローバルスコープはますます汚染され、特に関数を別のファイルにスコープする場合(などで使用Object.assign(exports, require('./more-functions.js'));する場合)は問題を悪化させますindex.js

コールドブートのパフォーマンスを大幅に向上させるために、以下のように必要なものをすべてinitメソッドに移動し、そのファイルの関数定義内の最初の行として呼び出します。例えば:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
// Late initialisers for performance
let initialised = false;
let handlebars;
let fs;
let path;
let encrypt;

function init() {
  if (initialised) { return; }

  handlebars = require('handlebars');
  fs = require('fs');
  path = require('path');
  ({ encrypt } = require('../common'));
  // Maybe do some handlebars compilation here too

  initialised = true;
}

この手法を8つのファイルにまたがる〜30の機能を持つプロジェクトに適用すると、約7〜8秒から2〜3秒に改善が見られます。これにより、関数をコールドブートする必要が少なくなるようです(おそらくメモリ使用量が少ないためでしょうか?)

残念ながら、これはまだHTTP機能をユーザー向けプロダクション用途にはほとんど使用できません。

Firebaseチームが関数の適切なスコープを可能にし、関連するモジュールのみを関数ごとにロードする必要があるようにする将来の計画があることを願っています。


Tyrisさん、私は時間操作で同じ問題に直面しています。私はあなたのソリューションを実装しようとしています。init関数を呼び出すのは誰ですか?
Manspof

こんにちは@ AdirZoari、init()などの使用に関する私の説明はおそらくベストプラクティスではありません。その価値は、中心的な問題についての私の発見を実証することだけです。隠し変数process.env.FUNCTION_NAMEを見て、その関数に必要なファイルを条件付きで含めるためにそれを使用する方がはるかに良いでしょう。github.com/firebase/functions-samples/issues/…のコメントは、この動作について非常によく説明しています。これにより、グローバルスコープがメソッドで汚染されず、無関係な関数が含まれることがなくなります。
Tyris 2018

1
こんにちは@davidverweijです。これは、関数が2回または並列に実行される可能性に関しては役立つとは思いません。関数は必要に応じて自動スケーリングされるため、複数の関数(同じ関数または異なる関数)をいつでも並列実行できます。つまり、データの安全性とトランザクションの使用を考慮する必要があります。また、おそらく二回実行している機能でこの記事をチェックアウト:cloud.google.com/blog/products/serverless/...を
Tyris

1
FUNCTIONS_NAMEここで説明するように、通知はノード6および8でのみ有効です:cloud.google.com/functions/docs/…。ノード10が使用する必要があるFUNCTION_TARGET
maganap

1
@maganapを更新していただきありがとうございます。docoのcloud.google.com/functions/docs/migrating/ K_SERVICEに従って使用する必要があるようです...-回答を更新しました。
タイリス

7

Firestoreのクラウド機能でも同様の問題に直面しています。最大のものはパフォーマンスです。特に初期段階のスタートアップの場合、初期の顧客に「遅い」アプリを見る余裕がありません。例えばのための簡単なドキュメンテーション生成関数はこれを与えます:

-関数の実行には9522ミリ秒かかり、ステータスコード200で終了しました

それから:私はまっすぐな契約条件ページを持っていました。クラウド機能を使用すると、コールドスタートによる実行には、場合によっては10〜15秒かかります。次に、それをappengineコンテナーでホストされているnode.jsアプリに移動しました。時間は2〜3秒になりました。

私はmongodbの機能の多くをファイアストアと比較しており、製品のこの初期段階で別のデータベースに移動する必要があるのではないかと思うこともあります。私がファイアストアで持っていた最大のadvは、ドキュメントオブジェクトのトリガー機能onCreate、onUpdateでした。

https://db-engines.com/en/system/Google+Cloud+Firestore%3BMongoDB

基本的に、サイトの静的部分がappengine環境にオフロードできる場合、おそらく悪い考えではありません。


1
Firebase Functionsは、動的なユーザー向けコンテンツを表示する限り、目的に適しているとは思いません。パスワードのリセットなどのために、いくつかのHTTP関数を控えめに使用していますが、一般に、動的なコンテンツがある場合は、それをエクスプレスアプリとして他の場所に提供します(またはdiff言語を使用します)。
Tyris

2

私はこれらのことも行ったので、関数がウォームアップされるとパフォーマンスが向上しますが、コールドスタートは私を殺します。私が遭遇した他の問題の1つは、仕事を完了するためにクラウド関数に2回アクセスする必要があるため、corsに関するものです。しかし、私はそれを修正できると確信しています。

アプリが初期(デモ)フェーズにあり、頻繁に使用されていない場合、パフォーマンスはそれほど良くありません。初期の製品を使用する早期導入ユーザーは、潜在的な顧客/投資家の前で最善を尽くす必要があるため、これは考慮すべき事項です。私たちはこのテクノロジーが大好きだったので、古くて実績のあるフレームワークから移行しましたが、現時点ではアプリの動作がかなり遅いようです。次に、見栄えを良くするためのウォームアップ戦略をいくつか試します


cronジョブをテストして、すべての機能を起動します。多分このアプローチはあなたにも役立ちます。
ヘスス・フエンテス

ちょっと@JesúsFuentes機能を呼び起こすとうまくいくのかと思っていました。クレイジーなソリューションのように
聞こえ

1
こんにちは@Alexandrです。残念ながら、まだそれを行う時間はありませんでしたが、最優先のリストにあります。ただし、理論的には機能するはずです。この問題は、Firebaseアプリから起動する必要があるonCall関数で発生します。X分ごとにクライアントから呼び出す可能性がありますか?わかります。
ヘスス・フエンテス

1
@AlexandrはStackoverflowの外で会話をしませんか?私たちは新しいアプローチで互いに助け合うかもしれません。
ヘスス・フエンテス

1
@Alexandrこの「ウェイクアップ」回避策はまだテストしていませんが、関数を既にeurope-west1にデプロイしています。それでも、受け入れられない時代。
ヘスス・フエンテス

0

更新/編集:2020年5月に新しい構文と更新

というパッケージを公開したところbetter-firebase-functions、関数ディレクトリが自動的に検索され、見つかったすべての関数がエクスポートオブジェクトに正しくネストされます。また、関数を互いに分離して、コールドブートのパフォーマンスを向上させます。

モジュールスコープ内の各関数に必要な依存関係のみを遅延ロードしてキャッシュすると、急成長するプロジェクトで関数を最適な状態に保つための最も簡単で簡単な方法であることがわかります。

import { exportFunctions } from 'better-firebase-functions'
exportFunctions({__filename, exports})

興味深い..「better-firebase-functions」のリポジトリはどこにありますか?
JerryGoyal

1
github.com/gramstr/better-firebase-functions-チェックして、感想を教えてください!同様に貢献してください:)
George43g
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.