Die Funktionen der Firebase-Cloud sind sehr langsam

131

Wir arbeiten an einer Anwendung, die die neuen Firebase-Cloud-Funktionen verwendet. Derzeit wird eine Transaktion in den Warteschlangenknoten gestellt. Und dann entfernt die Funktion diesen Knoten und fügt ihn in den richtigen Knoten ein. Dies wurde aufgrund der Möglichkeit implementiert, offline zu arbeiten.

Unser aktuelles Problem ist die Geschwindigkeit der Funktion. Die Funktion selbst dauert ungefähr 400 ms, das ist also in Ordnung. Manchmal dauern die Funktionen jedoch sehr lange (ca. 8 Sekunden), während der Eintrag bereits zur Warteschlange hinzugefügt wurde.

Wir vermuten, dass der Server Zeit braucht, um hochzufahren, denn wenn wir die Aktion nach dem ersten noch einmal ausführen. Es dauert viel weniger Zeit.

Gibt es eine Möglichkeit, dieses Problem zu beheben? Hier unten habe ich den Code unserer Funktion hinzugefügt. Wir vermuten, dass daran nichts falsch ist, aber wir haben es für alle Fälle hinzugefügt.

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;
}
Stan van Heumen
quelle
Ist es sicher, das Versprechen der oben genannten "einmal ()" - Aufrufe nicht zurückzugeben?
Jazzgil

Antworten:

111

Firebaser hier

Es hört sich so an, als würden Sie einen sogenannten Kaltstart der Funktion erleben.

Wenn Ihre Funktion seit einiger Zeit nicht mehr ausgeführt wurde, versetzt Cloud Functions sie in einen Modus, der weniger Ressourcen verbraucht. Wenn Sie die Funktion dann erneut drücken, wird die Umgebung aus diesem Modus wiederhergestellt. Die für die Wiederherstellung erforderliche Zeit besteht aus festen Kosten (z. B. Wiederherstellung des Containers) und variablen Teilkosten (z. B. wenn Sie viele Knotenmodule verwenden, kann dies länger dauern).

Wir überwachen kontinuierlich die Leistung dieser Vorgänge, um die beste Mischung zwischen Entwicklererfahrung und Ressourcennutzung sicherzustellen. Erwarten Sie also, dass sich diese Zeiten im Laufe der Zeit verbessern.

Die gute Nachricht ist, dass Sie dies nur während der Entwicklung erleben sollten. Sobald Ihre Funktionen in der Produktion häufig ausgelöst werden, werden sie wahrscheinlich kaum wieder einen Kaltstart erleben.

Frank van Puffelen
quelle
3
Moderator Hinweis : Alle Off-Topic-Kommentare zu diesem Beitrag wurden entfernt. Bitte verwenden Sie die Kommentare, um Klarstellungen anzufordern oder nur Verbesserungen vorzuschlagen. Wenn Sie eine verwandte, aber andere Frage haben, stellen Sie eine neue Frage und fügen Sie einen Link zu dieser hinzu, um den Kontext bereitzustellen.
Bhargav Rao
55

Update Mai 2020 Danke für den Kommentar von maganap - in Node 10+ FUNCTION_NAMEwird durch ersetzt K_SERVICE( FUNCTION_TARGETist die Funktion selbst, nicht der Name, ersetzt ENTRY_POINT). Die folgenden Codebeispiele wurden unten aufgeführt.

Weitere Informationen finden Sie unter https://cloud.google.com/functions/docs/migrating/nodejs-runtimes#nodejs-10-changes

Update - Es sieht so aus, als könnten viele dieser Probleme mithilfe der versteckten Variablen gelöst werden, process.env.FUNCTION_NAMEwie hier dargestellt: https://github.com/firebase/functions-samples/issues/170#issuecomment-323375462

Mit Code aktualisieren - Wenn Sie beispielsweise die folgende Indexdatei haben:

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

Dann werden alle Ihre Dateien geladen, und alle Anforderungen dieser Dateien werden auch geladen, was zu viel Overhead führt und Ihren globalen Bereich für alle Ihre Funktionen verschmutzt.

Trennen Sie stattdessen Ihre Includes wie folgt:

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');
}

Dadurch werden die erforderlichen Dateien nur geladen, wenn diese Funktion speziell aufgerufen wird. So können Sie Ihr globales Zielfernrohr viel sauberer halten, was zu schnelleren Kaltstarts führen sollte.


Dies sollte eine viel sauberere Lösung ermöglichen als das, was ich unten getan habe (obwohl die folgende Erklärung immer noch gilt).


Ursprüngliche Antwort

Es sieht so aus, als ob das Erfordernis von Dateien und die allgemeine Initialisierung im globalen Bereich eine große Ursache für die Verlangsamung beim Kaltstart ist.

Wenn ein Projekt mehr Funktionen erhält, wird der globale Bereich immer mehr verschmutzt, was das Problem verschlimmert - insbesondere, wenn Sie Ihre Funktionen in separate Dateien einteilen (z. B. durch Verwendung Object.assign(exports, require('./more-functions.js'));in Ihrem index.js.

Ich habe es geschafft, enorme Verbesserungen bei der Kaltstartleistung zu erzielen, indem ich alle meine Anforderungen in eine Init-Methode wie unten verschoben und sie dann als erste Zeile in einer Funktionsdefinition für diese Datei aufgerufen habe. Z.B:

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;
}

Ich habe Verbesserungen von ungefähr 7-8s auf 2-3s gesehen, als ich diese Technik auf ein Projekt mit ~ 30 Funktionen in 8 Dateien angewendet habe. Dies scheint auch dazu zu führen, dass Funktionen seltener kalt gestartet werden müssen (vermutlich aufgrund einer geringeren Speichernutzung?).

Leider sind HTTP-Funktionen dadurch immer noch kaum für die benutzerbezogene Produktion geeignet.

In der Hoffnung, dass das Firebase-Team in Zukunft einige Pläne hat, um einen ordnungsgemäßen Funktionsumfang zu ermöglichen, sodass für jede Funktion nur die relevanten Module geladen werden müssen.

Tyris
quelle
Hey Tyris, ich habe das gleiche Problem mit der Zeitoperation. Ich versuche, Ihre Lösung zu implementieren. Ich versuche nur zu verstehen, wer wann die Init-Funktion aufruft.
Manspof
Hallo @AdirZoari, meine Erklärung zur Verwendung von init () usw. ist wahrscheinlich keine bewährte Methode. Sein Wert ist nur, um meine Erkenntnisse über das Kernproblem zu demonstrieren. Sie sollten sich die versteckte Variable besser ansehen process.env.FUNCTION_NAMEund damit die für diese Funktion erforderlichen Dateien bedingt einschließen. Der Kommentar unter github.com/firebase/functions-samples/issues/… gibt eine wirklich gute Beschreibung dieser Arbeitsweise! Es stellt sicher, dass der globale Bereich nicht mit Methoden verschmutzt wird und irrelevante Funktionen enthält.
Tyris
1
Hallo @davidverweij, ich denke nicht, dass dies im Hinblick auf die Möglichkeit hilfreich sein wird, dass Ihre Funktionen zweimal oder parallel ausgeführt werden. Funktionen werden nach Bedarf automatisch skaliert, sodass mehrere Funktionen (dieselbe oder verschiedene Funktionen) jederzeit parallel ausgeführt werden können. Dies bedeutet, dass Sie die Datensicherheit und die Verwendung von Transaktionen berücksichtigen müssen. Lesen
Tyris
1
Hinweis FUNCTIONS_NAMEgilt nur für Knoten 6 und 8, wie hier erläutert: cloud.google.com/functions/docs/… . Knoten 10 sollte verwendenFUNCTION_TARGET
Maganap
1
Vielen Dank für das Update @maganap. Es sollte K_SERVICElaut doco unter cloud.google.com/functions/docs/migrating/… verwendet werden. - Ich habe meine Antwort aktualisiert.
Tyris
7

Ich habe ähnliche Probleme mit Firestore-Cloud-Funktionen. Das größte ist die Leistung. Insbesondere bei Startups im Frühstadium, wenn Sie es Ihren frühen Kunden nicht leisten können, "träge" Apps zu sehen. Eine einfache Dokumentationsgenerierungsfunktion für z.

- Die Funktionsausführung dauerte 9522 ms und endete mit dem Statuscode: 200

Dann: Ich hatte eine einfache Seite mit den allgemeinen Geschäftsbedingungen. Bei Cloud-Funktionen würde die Ausführung aufgrund des Kaltstarts auch zeitweise 10-15 Sekunden dauern. Ich habe es dann in eine node.js-App verschoben, die auf einem Appengine-Container gehostet wird. Die Zeit ist auf 2-3 Sekunden gesunken.

Ich habe viele Funktionen von Mongodb mit Firestore verglichen und manchmal frage ich mich auch, ob ich in dieser frühen Phase meines Produkts auch in eine andere Datenbank wechseln sollte. Der größte Vorteil, den ich im Firestore hatte, war die Triggerfunktion onCreate, onUpdate von Dokumentobjekten.

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

Grundsätzlich ist es keine schlechte Idee, wenn statische Teile Ihrer Site in die Appengine-Umgebung ausgelagert werden können.

Sudhakar R.
quelle
1
Ich denke nicht, dass Firebase-Funktionen für die Anzeige dynamischer benutzerbezogener Inhalte geeignet sind. Wir verwenden einige HTTP-Funktionen sparsam für Dinge wie das Zurücksetzen von Passwörtern. Wenn Sie jedoch über dynamischen Inhalt verfügen, können Sie diesen im Allgemeinen als Express-App an anderer Stelle verwenden (oder eine andere Sprache verwenden).
Tyris
2

Ich habe auch diese Dinge getan, was die Leistung verbessert, sobald die Funktionen aufgewärmt sind, aber der Kaltstart bringt mich um. Eines der anderen Probleme, auf die ich gestoßen bin, betrifft Cors, da zwei Fahrten zu den Cloud-Funktionen erforderlich sind, um die Aufgabe zu erledigen. Ich bin mir sicher, dass ich das beheben kann.

Wenn sich eine App in der frühen (Demo-) Phase befindet, in der sie nicht häufig verwendet wird, ist die Leistung nicht besonders gut. Dies sollte berücksichtigt werden, da Early Adopters mit Early Product vor potenziellen Kunden / Investoren ihr Bestes geben müssen. Wir haben die Technologie geliebt und sind daher von älteren, bewährten Frameworks migriert, aber unsere App scheint zu diesem Zeitpunkt ziemlich träge zu sein. Ich werde als nächstes einige Aufwärmstrategien ausprobieren, damit es besser aussieht

Stan Swiniarski
quelle
Wir testen einen Cron-Job, um jede einzelne Funktion zu aktivieren. Vielleicht hilft Ihnen dieser Ansatz auch.
Jesús Fuentes
hey @ JesúsFuentes Ich habe mich nur gefragt, ob das Aufwecken der Funktion für Sie funktioniert. Klingt nach einer verrückten Lösung: D
Alexandr Zavalii
1
Hallo @Alexandr, leider hatten wir noch keine Zeit dafür, aber es steht auf unserer Liste mit den höchsten Prioritäten. Es sollte jedoch theoretisch funktionieren. Das Problem tritt bei onCall-Funktionen auf, die über eine Firebase-App gestartet werden müssen. Vielleicht alle X Minuten vom Client anrufen? Wir werden sehen.
Jesús Fuentes
1
@Alexandr sollen wir uns außerhalb von Stackoverflow unterhalten? Wir könnten uns gegenseitig mit neuen Ansätzen helfen.
Jesús Fuentes
1
@Alexandr Wir haben diese 'Wakeup'-Problemumgehung noch nicht getestet, aber wir haben unsere Funktionen bereits für Europa-West1 bereitgestellt. Trotzdem inakzeptable Zeiten.
Jesús Fuentes
0

UPDATE / EDIT: Neue Syntax und Updates kommen im Mai 2020

Ich habe gerade ein Paket mit dem Namen better-firebase-functionsveröffentlicht. Es durchsucht automatisch Ihr Funktionsverzeichnis und verschachtelt alle gefundenen Funktionen in Ihrem Exportobjekt korrekt, während die Funktionen voneinander isoliert werden, um die Kaltstartleistung zu verbessern.

Wenn Sie nur die Abhängigkeiten, die Sie für jede Funktion innerhalb des Modulbereichs benötigen, verzögert laden und zwischenspeichern, ist dies der einfachste und einfachste Weg, um Ihre Funktionen in einem schnell wachsenden Projekt optimal effizient zu halten.

import { exportFunctions } from 'better-firebase-functions'
exportFunctions({__filename, exports})
George43g
quelle
interessant .. wo kann ich das Repo von 'Better-Firebase-Funktionen' sehen?
JerryGoyal
1
github.com/gramstr/better-firebase-functions - bitte schau es dir an und lass mich wissen, was du denkst! Fühlen Sie sich frei, auch beizutragen :)
George43g