Le funzioni cloud di Firebase sono molto lente


131

Stiamo lavorando su un'applicazione che utilizza le nuove funzioni cloud Firebase. Ciò che sta attualmente accadendo è che una transazione viene inserita nel nodo della coda. E quindi la funzione rimuove quel nodo e lo inserisce nel nodo corretto. Questo è stato implementato grazie alla possibilità di lavorare offline.

Il nostro problema attuale è la velocità della funzione. La stessa funzione dura circa 400 ms, quindi va bene. Ma a volte le funzioni richiedono molto tempo (circa 8 secondi), mentre la voce è già stata aggiunta alla coda.

Sospettiamo che il server impieghi tempo per avviarsi, perché quando eseguiamo l'azione ancora una volta dopo la prima. Ci vuole molto meno tempo.

C'è un modo per risolvere questo problema? Quaggiù ho aggiunto il codice della nostra funzione. Sospettiamo che non ci sia nulla di sbagliato in questo, ma lo abbiamo aggiunto per ogni evenienza.

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

È sicuro non restituire la Promessa di chiamate "once ()" sopra?
jazzgil

Risposte:


111

firebaser qui

Sembra che tu stia sperimentando un cosiddetto avvio a freddo della funzione.

Quando la tua funzione non è stata eseguita da un po 'di tempo, Cloud Functions la mette in una modalità che utilizza meno risorse. Quindi quando si preme di nuovo la funzione, ripristina l'ambiente da questa modalità. Il tempo necessario per il ripristino è costituito da un costo fisso (ad es. Ripristinare il contenitore) e da un costo variabile della parte (ad es. Se si utilizzano molti moduli di nodo, potrebbe essere necessario più tempo).

Monitoriamo continuamente le prestazioni di queste operazioni per garantire il miglior mix tra esperienza degli sviluppatori e utilizzo delle risorse. Quindi aspettati che questi tempi migliorino nel tempo.

La buona notizia è che dovresti sperimentarlo solo durante lo sviluppo. Una volta che le tue funzioni vengono spesso attivate durante la produzione, è probabile che non riescano quasi mai a ripartire a freddo.


3
Nota del moderatore : tutti i commenti fuori tema su questo post sono stati rimossi. Si prega di utilizzare i commenti per richiedere chiarimenti o suggerire solo miglioramenti. Se hai una domanda correlata ma diversa, fai una nuova domanda e includi un link a questa per aiutare a fornire un contesto.
Bhargav Rao

55

Aggiornamento maggio 2020 Grazie per il commento di maganap - nel Nodo 10+ FUNCTION_NAMEviene sostituito con K_SERVICE( FUNCTION_TARGETè la funzione stessa, non è il nome, la sostituzione ENTRY_POINT). Gli esempi di codice di seguito sono stati elencati di seguito.

Maggiori informazioni su https://cloud.google.com/functions/docs/migrating/nodejs-runtimes#nodejs-10-changes

Aggiornamento : sembra che molti di questi problemi possano essere risolti usando la variabile nascosta process.env.FUNCTION_NAMEcome mostrato qui: https://github.com/firebase/functions-samples/issues/170#issuecomment-323375462

Aggiorna con codice - Ad esempio, se hai il seguente file indice:

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

Quindi verranno caricati tutti i tuoi file e verranno caricati anche tutti i requisiti di quei file, con conseguente sovraccarico e inquinando il tuo ambito globale per tutte le tue funzioni.

Invece separando i tuoi include come:

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

Questo caricherà i file richiesti solo quando quella funzione viene chiamata in modo specifico; permettendoti di mantenere il tuo ambito globale molto più pulito, il che dovrebbe tradursi in un avvio più rapido.


Ciò dovrebbe consentire una soluzione molto più accurata di quella che ho fatto di seguito (anche se la spiegazione di seguito vale ancora).


Risposta originale

Sembra che richiedere file e l'inizializzazione generale che si verificano nell'ambito globale sia un'enorme causa di rallentamento durante l'avvio a freddo.

Man mano che un progetto ottiene più funzioni, l'ambito globale viene inquinato sempre di più, peggiorando il problema, specialmente se si orientano le funzioni in file separati (ad esempio utilizzando Object.assign(exports, require('./more-functions.js'));nel proprio index.js.

Sono riuscito a vedere enormi guadagni nelle prestazioni di avvio a freddo spostando tutte le mie esigenze in un metodo init come di seguito e quindi chiamandolo come prima riga all'interno di qualsiasi definizione di funzione per quel file. Per esempio:

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

Ho visto miglioramenti da circa 7-8 a 2-3 secondi quando si applica questa tecnica a un progetto con ~ 30 funzioni su 8 file. Ciò sembra anche causare l'avvio meno frequente delle funzioni da avviare a freddo (presumibilmente a causa di un minore utilizzo della memoria?)

Sfortunatamente, ciò rende le funzioni HTTP a malapena utilizzabili per l'uso in produzione rivolto agli utenti.

Sperando che il team di Firebase abbia in futuro dei piani per consentire il corretto scoping delle funzioni in modo che solo i moduli pertinenti debbano mai essere caricati per ogni funzione.


Ehi Tyris, sto affrontando lo stesso problema con il funzionamento a tempo, sto cercando di implementare la tua soluzione. sto solo cercando di capire, chi chiama alla funzione init e quando?
Manspof,

Ciao @AdirZoari, la mia spiegazione sull'uso di init () e così via non è probabilmente la migliore pratica; il suo valore è solo per dimostrare le mie scoperte sul problema principale. Faresti molto meglio a guardare la variabile nascosta process.env.FUNCTION_NAMEe usarla per includere condizionalmente i file richiesti per quella funzione. Il commento su github.com/firebase/functions-samples/issues/… fornisce un'ottima descrizione di questo lavoro! Assicura che l'ambito globale non sia inquinato da metodi e includa funzioni irrilevanti.
Tyris,

1
Ciao @davidverweij, non credo che ciò contribuirà in termini di possibilità che le tue funzioni possano essere eseguite due volte o in parallelo. Le funzioni si ridimensionano automaticamente in base alle esigenze, in modo che più funzioni (la stessa funzione o diverse) possano essere eseguite in parallelo in qualsiasi momento. Ciò significa che devi considerare la sicurezza dei dati e considerare l'utilizzo delle transazioni. Inoltre, leggi questo articolo sulle tue funzioni eventualmente in esecuzione due volte: cloud.google.com/blog/products/serverless/…
Tyris

1
L'avviso FUNCTIONS_NAMEè valido solo con i nodi 6 e 8, come spiegato qui: cloud.google.com/functions/docs/… . Il nodo 10 dovrebbe usareFUNCTION_TARGET
maganap il

1
Grazie per l'aggiornamento @maganap, sembra che dovrebbe essere utilizzato in K_SERVICEbase al doco su cloud.google.com/functions/docs/migrating/… - Ho aggiornato la mia risposta.
Tyris,

7

Sto affrontando problemi simili con le funzioni cloud del camino. Il più grande è la prestazione. Soprattutto in caso di startup in fase iniziale, quando non puoi permetterti ai tuoi primi clienti di vedere app "lente". Una semplice funzione di generazione della documentazione, ad esempio, fornisce questo:

- L'esecuzione della funzione ha richiesto 9522 ms, terminata con il codice di stato: 200

Quindi: ho avuto una semplice pagina di termini e condizioni. Con le funzioni cloud, l'esecuzione a causa dell'avvio a freddo richiederebbe 10-15 secondi anche a volte. L'ho quindi spostato in un'app node.js, ospitata sul contenitore dell'appengine. Il tempo è sceso a 2-3 secondi.

Ho confrontato molte delle funzionalità di mongodb con il camino e talvolta mi chiedo anche se durante questa fase iniziale del mio prodotto dovrei anche passare a un database diverso. Il più grande adv che ho avuto nel negozio di firme era la funzionalità di trigger onCreate, onUpdate di oggetti documento.

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

Fondamentalmente se ci sono parti statiche del tuo sito che possono essere scaricate nell'ambiente appengine, forse non è una cattiva idea.


1
Non credo che le funzioni Firebase siano adatte allo scopo per quanto riguarda la visualizzazione di contenuti dinamici rivolti all'utente. Usiamo alcune funzioni HTTP con parsimonia per cose come la reimpostazione della password, ma in generale se hai contenuti dinamici, servili altrove come app express (o usi un linguaggio diff).
Tyris,

2

Ho fatto anche queste cose, il che migliora le prestazioni una volta che le funzioni sono state riscaldate, ma l'avvio a freddo mi sta uccidendo. Uno degli altri problemi che ho riscontrato è con cors, perché ci vogliono due viaggi nelle funzioni cloud per portare a termine il lavoro. Sono sicuro di poterlo risolvere, però.

Quando hai un'app nella sua fase iniziale (demo) quando non viene utilizzata frequentemente, le prestazioni non saranno eccezionali. Questo è qualcosa che dovrebbe essere preso in considerazione, poiché i primi utenti con i primi prodotti devono apparire al meglio di fronte a potenziali clienti / investitori. Abbiamo adorato la tecnologia, quindi siamo migrati da vecchi framework collaudati, ma la nostra app sembra piuttosto lenta a questo punto. Ora proverò alcune strategie di riscaldamento per farlo sembrare migliore


Stiamo testando un cron-job per riattivare ogni singola funzione. Forse questo approccio aiuta anche te.
Jesús Fuentes,

hey @ JesúsFuentes Mi stavo solo chiedendo se svegliarti funzionasse per te. Sembra una soluzione folle: D
Alexandr Zavalii,

1
Ciao @Alexandr, purtroppo non abbiamo ancora avuto il tempo di farlo, ma è nella nostra lista delle priorità. Dovrebbe funzionare teoricamente, però. Il problema si presenta con le funzioni onCall, che richiedono l'avvio da un'app Firebase. Forse chiamandoli dal client ogni X minuti? Vedremo.
Jesús Fuentes,

1
@Alexandr dovremmo avere una conversazione fuori Stackoverflow? Potremmo aiutarci a vicenda con nuovi approcci.
Jesús Fuentes,

1
@Alexandr non abbiamo ancora testato questa soluzione alternativa "sveglia" ma abbiamo già implementato le nostre funzioni in europa-ovest1. Ancora, tempi inaccettabili.
Jesús Fuentes,

0

AGGIORNAMENTO / MODIFICA: nuova sintassi e aggiornamenti in arrivo a MAGGIO2020

Ho appena pubblicato un pacchetto chiamato better-firebase-functions, cerca automaticamente la directory delle funzioni e nidifica correttamente tutte le funzioni trovate nell'oggetto export, isolando le funzioni l'una dall'altra per migliorare le prestazioni di avvio a freddo.

Se carichi lentamente e memorizzi nella cache solo le dipendenze necessarie per ciascuna funzione nell'ambito del modulo, scoprirai che è il modo più semplice e facile per mantenere le tue funzioni in modo ottimale efficiente su un progetto in rapida crescita.

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

interessante .. dove posso vedere il repository di "funzioni best-firebase"?
JerryGoyal,

1
github.com/gramstr/better-firebase-functions - per favore controlla e fammi sapere cosa ne pensi! Sentiti libero di contribuire anche
tu
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.