Les fonctions cloud de Firebase sont très lentes

131

Nous travaillons sur une application qui utilise les nouvelles fonctions cloud de Firebase. Ce qui se passe actuellement, c'est qu'une transaction est placée dans le nœud de file d'attente. Et puis la fonction supprime ce nœud et le place dans le nœud correct. Cela a été mis en œuvre en raison de la possibilité de travailler hors ligne.

Notre problème actuel est la vitesse de la fonction. La fonction elle-même prend environ 400 ms, donc ce n'est pas grave. Mais parfois, les fonctions prennent un temps très long (environ 8 secondes), alors que l'entrée a déjà été ajoutée à la file d'attente.

Nous soupçonnons que le serveur met du temps à démarrer, car lorsque nous recommençons l'action après la première. Cela prend beaucoup moins de temps.

Existe-t-il un moyen de résoudre ce problème? Ici, j'ai ajouté le code de notre fonction. Nous pensons qu'il n'y a rien de mal à cela, mais nous l'avons ajouté au cas où.

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
la source
Est-il prudent de ne pas renvoyer la promesse des appels «once ()» ci-dessus?
jazzgil

Réponses:

112

firebaser ici

Il semble que vous rencontriez un soi-disant démarrage à froid de la fonction.

Lorsque votre fonction n'a pas été exécutée depuis un certain temps, Cloud Functions la met dans un mode qui utilise moins de ressources. Ensuite, lorsque vous appuyez à nouveau sur la fonction, elle restaure l'environnement à partir de ce mode. Le temps nécessaire à la restauration se compose d'un coût fixe (par exemple, restaurer le conteneur) et d'un coût variable en partie (par exemple, si vous utilisez beaucoup de modules de nœuds, cela peut prendre plus de temps).

Nous surveillons en permanence les performances de ces opérations pour garantir la meilleure combinaison entre l'expérience des développeurs et l'utilisation des ressources. Attendez-vous donc à ce que ces temps s'améliorent avec le temps.

La bonne nouvelle est que vous ne devriez en faire l'expérience que pendant le développement. Une fois que vos fonctions sont fréquemment déclenchées en production, il y a de fortes chances qu'elles ne recommencent presque jamais à démarrer à froid.

Frank van Puffelen
la source
3
Note du modérateur : Tous les commentaires hors sujet sur ce message ont été supprimés. Veuillez utiliser les commentaires pour demander des éclaircissements ou suggérer uniquement des améliorations. Si vous avez une question connexe mais différente, posez une nouvelle question et incluez un lien vers celle-ci pour aider à fournir un contexte.
Bhargav Rao
55

Mise à jour mai 2020 Merci pour le commentaire de maganap - dans Node 10+ FUNCTION_NAMEest remplacé par K_SERVICE( FUNCTION_TARGETest la fonction elle-même, pas son nom, le remplacement ENTRY_POINT). Les exemples de code ci-dessous ont été mis à jour ci-dessous.

Plus d'informations sur https://cloud.google.com/functions/docs/migrating/nodejs-runtimes#nodejs-10-changes

Mise à jour - il semble que beaucoup de ces problèmes peuvent être résolus en utilisant la variable cachée process.env.FUNCTION_NAMEcomme on le voit ici: https://github.com/firebase/functions-samples/issues/170#issuecomment-323375462

Mettre à jour avec le code - Par exemple, si vous disposez du fichier d'index suivant:

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

Ensuite, tous vos fichiers seront chargés, et toutes les exigences de ces fichiers seront également chargées, ce qui entraînera beaucoup de surcharge et polluera votre portée globale pour toutes vos fonctions.

Au lieu de cela, séparez vos inclusions en:

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

Cela ne chargera le (s) fichier (s) requis que lorsque cette fonction sera spécifiquement appelée; vous permettant de garder votre lunette globale beaucoup plus propre, ce qui devrait entraîner des démarrages à froid plus rapides.


Cela devrait permettre une solution beaucoup plus soignée que ce que j'ai fait ci-dessous (bien que l'explication ci-dessous soit toujours valable).


Réponse originale

Il semble que le fait d'exiger des fichiers et une initialisation générale dans la portée globale soit une cause énorme de ralentissement lors du démarrage à froid.

Au fur et à mesure qu'un projet obtient de plus en plus de fonctions, la portée globale est de plus en plus polluée, ce qui aggrave le problème - surtout si vous étendez vos fonctions dans des fichiers séparés (par exemple en utilisant Object.assign(exports, require('./more-functions.js'));dans votre index.js.

J'ai réussi à voir d'énormes gains dans les performances de démarrage à froid en déplaçant tous mes besoins dans une méthode init comme ci-dessous, puis en l'appelant comme la première ligne de toute définition de fonction pour ce fichier. Par exemple:

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

J'ai vu des améliorations d'environ 7-8 à 2-3s lors de l'application de cette technique à un projet avec ~ 30 fonctions sur 8 fichiers. Cela semble également obliger les fonctions à être démarrées à froid moins souvent (probablement en raison d'une moindre utilisation de la mémoire?)

Malheureusement, cela rend encore les fonctions HTTP à peine utilisables pour une utilisation en production face à l'utilisateur.

En espérant que l'équipe Firebase ait des plans à l'avenir pour permettre une définition appropriée des fonctions afin que seuls les modules pertinents aient jamais besoin d'être chargés pour chaque fonction.

Tyris
la source
Hey Tyris, je suis confronté au même problème avec le fonctionnement du temps, j'essaye de mettre en œuvre votre solution. essayant juste de comprendre, qui appelle à la fonction init et quand?
Manspof
Salut @AdirZoari, mon explication sur l'utilisation de init () et ainsi de suite n'est probablement pas la meilleure pratique; sa valeur est simplement de démontrer mes conclusions sur le problème central. Vous feriez bien mieux de regarder la variable cachée process.env.FUNCTION_NAMEet de l'utiliser pour inclure conditionnellement les fichiers requis pour cette fonction. Le commentaire sur github.com/firebase/functions-samples/issues/… donne une très bonne description de ce fonctionnement! Cela garantit que la portée globale n'est pas polluée par des méthodes et inclut des fonctions non pertinentes.
Tyris
1
Salut @davidverweij, je ne pense pas que cela aidera en termes de possibilité que vos fonctions fonctionnent deux fois ou en parallèle. Les fonctions s'adaptent automatiquement au besoin afin que plusieurs fonctions (la même fonction ou des fonctions différentes) puissent être exécutées en parallèle à tout moment. Cela signifie que vous devez tenir compte de la sécurité des données et envisager d'utiliser des transactions. Consultez également cet article sur vos fonctions pouvant être exécutées deux fois: cloud.google.com/blog/products/serverless/…
Tyris
1
L'avis FUNCTIONS_NAMEn'est valide qu'avec les nœuds 6 et 8, comme expliqué ici: cloud.google.com/functions/docs/… . Node 10 devrait utiliserFUNCTION_TARGET
maganap le
1
Merci pour la mise à jour @maganap, on dirait qu'elle devrait utiliser K_SERVICEselon doco sur cloud.google.com/functions/docs/migrating / ... - J'ai mis à jour ma réponse.
Tyris
7

Je suis confronté à des problèmes similaires avec les fonctions cloud de Firestore. Le plus gros est la performance. Surtout en cas de démarrage précoce, lorsque vous ne pouvez pas permettre à vos premiers clients de voir des applications «lentes». Une simple fonction de génération de documentation pour par exemple donne ceci:

- L'exécution de la fonction a pris 9522 ms, terminée avec le code d'état: 200

Ensuite: j'avais une page de conditions générales simple. Avec les fonctions cloud, l'exécution due au démarrage à froid prendrait de 10 à 15 secondes, même parfois. Je l'ai ensuite déplacé vers une application node.js, hébergée sur le conteneur appengine. Le temps est descendu à 2-3 secondes.

J'ai comparé de nombreuses fonctionnalités de mongodb avec Firestore et parfois je me demande aussi si, au cours de cette première phase de mon produit, je devrais également passer à une autre base de données. Le plus gros avantage que j'avais dans Firestore était la fonctionnalité de déclenchement onCreate, onUpdate des objets de document.

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

Fondamentalement, s'il y a des parties statiques de votre site qui peuvent être déchargées vers l'environnement appengine, ce n'est peut-être pas une mauvaise idée.

Sudhakar R
la source
1
Je ne pense pas que les fonctions Firebase soient adaptées pour afficher du contenu dynamique destiné aux utilisateurs. Nous utilisons avec parcimonie quelques fonctions HTTP pour des choses comme la réinitialisation de mot de passe, mais en général, si vous avez du contenu dynamique, servez-le ailleurs en tant qu'application express (ou utilisez un langage diff).
Tyris
2

J'ai également fait ces choses, ce qui améliore les performances une fois les fonctions réchauffées, mais le démarrage à froid me tue. L'un des autres problèmes que j'ai rencontrés concerne les cors, car il faut deux voyages vers les fonctions cloud pour faire le travail. Je suis sûr que je peux résoudre ce problème.

Lorsque vous avez une application dans sa phase initiale (démo) lorsqu'elle n'est pas utilisée fréquemment, les performances ne seront pas excellentes. C'est quelque chose qui doit être pris en compte, car les premiers utilisateurs avec un produit précoce doivent être sous leur meilleur jour devant les clients / investisseurs potentiels. Nous avons adoré la technologie, nous avons donc migré à partir d'anciens frameworks éprouvés, mais notre application semble assez lente à ce stade. Je vais ensuite essayer quelques stratégies d'échauffement pour le rendre meilleur

Stan Swiniarski
la source
Nous testons un cron-job pour réactiver chaque fonction. Peut-être que cette approche vous aide aussi.
Jesús Fuentes
hey @ JesúsFuentes Je me demandais simplement si la fonction de réveil fonctionnait pour vous. Sonne comme une solution folle: D
Alexandr Zavalii
1
Salut @Alexandr, malheureusement, nous n'avons pas encore eu le temps de le faire, mais c'est dans notre liste de priorité absolue. Cela devrait cependant fonctionner en théorie. Le problème vient des fonctions onCall, qui doivent être lancées à partir d'une application Firebase. Peut-être les appeler du client toutes les X minutes? Nous verrons.
Jesús Fuentes
1
@Alexandr devons-nous avoir une conversation en dehors de Stackoverflow? Nous pourrions nous entraider avec de nouvelles approches.
Jesús Fuentes
1
@Alexandr nous n'avons pas encore testé cette solution de «réveil» mais nous avons déjà déployé nos fonctions sur europe-west1. Pourtant, des temps inacceptables.
Jesús Fuentes
0

UPDATE / EDIT: nouvelle syntaxe et mises à jour à venir MAI2020

Je viens de publier un package appelé better-firebase-functions, il recherche automatiquement votre répertoire de fonctions et imbrique correctement toutes les fonctions trouvées dans votre objet d'exportations, tout en isolant les fonctions les unes des autres pour améliorer les performances de démarrage à froid.

Si vous chargez et mettez en cache uniquement les dépendances dont vous avez besoin pour chaque fonction dans la portée du module, vous constaterez que c'est le moyen le plus simple et le plus simple de garder vos fonctions de manière optimale efficaces sur un projet à croissance rapide.

import { exportFunctions } from 'better-firebase-functions'
exportFunctions({__filename, exports})
George43g
la source
intéressant .. où puis-je voir le repo de 'better-firebase-functions'?
JerryGoyal
1
github.com/gramstr/better-firebase-functions - s'il vous plaît vérifier et dites-moi ce que vous en pensez! N'hésitez pas à contribuer également :)
George43g