As funções de nuvem do Firebase são muito lentas

131

Estamos trabalhando em um aplicativo que usa as novas funções da nuvem do firebase. O que está acontecendo atualmente é que uma transação é colocada no nó da fila. E então a função remove esse nó e o coloca no nó correto. Isso foi implementado devido à capacidade de trabalhar offline.

Nosso problema atual é a velocidade da função. A função em si leva cerca de 400ms, então está tudo bem. Às vezes, porém, as funções demoram muito tempo (cerca de 8 segundos), enquanto a entrada já foi adicionada à fila.

Suspeitamos que o servidor leva um tempo para inicializar, porque quando executamos a ação mais uma vez após a primeira. Leva muito menos tempo.

Existe alguma maneira de corrigir este problema? Aqui embaixo eu adicionei o código da nossa função. Suspeitamos que não há nada de errado com isso, mas adicionamos apenas por precauçã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
fonte
É seguro não retornar a promessa acima chamada "uma vez ()"?
jazzgil

Respostas:

111

firebaser aqui

Parece que você está experimentando o chamado início a frio da função.

Quando sua função não é executada há algum tempo, o Cloud Functions a coloca em um modo que utiliza menos recursos. Então, quando você pressiona a função novamente, ela restaura o ambiente desse modo. O tempo que leva para restaurar consiste em um custo fixo (por exemplo, restaurar o contêiner) e um custo variável parcial (por exemplo, se você usar muitos módulos de nós, pode demorar mais tempo).

Estamos monitorando continuamente o desempenho dessas operações para garantir a melhor combinação entre a experiência do desenvolvedor e o uso de recursos. Portanto, espere que esses horários melhorem com o tempo.

A boa notícia é que você só deve experimentar isso durante o desenvolvimento. Uma vez que suas funções são ativadas com freqüência na produção, é provável que elas nunca cheguem a um começo a frio novamente.

Frank van Puffelen
fonte
3
Nota do moderador : Todos os comentários fora do tópico nesta publicação foram removidos. Use os comentários para solicitar esclarecimentos ou sugerir apenas melhorias. Se você tiver uma pergunta relacionada, mas diferente, faça uma nova pergunta e inclua um link para ajudar a fornecer contexto.
Bhargav Rao
55

Atualização maio de 2020 Obrigado pelo comentário de maganap - o Nó 10+ FUNCTION_NAMEé substituído por K_SERVICE( FUNCTION_TARGETé a própria função, não o nome, substituindo ENTRY_POINT). As amostras de código abaixo foram atualizadas abaixo.

Mais informações em https://cloud.google.com/functions/docs/migrating/nodejs-runtimes#nodejs-10-changes

Atualização - parece que muitos desses problemas podem ser resolvidos usando a variável oculta, process.env.FUNCTION_NAMEconforme mostrado aqui: https://github.com/firebase/functions-samples/issues/170#issuecomment-323375462

Atualizar com código - por exemplo, se você tiver o seguinte arquivo de índice:

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

Todos os seus arquivos serão carregados e os requisitos de todos esses arquivos também serão carregados, resultando em muita sobrecarga e poluindo seu escopo global para todas as suas funções.

Em vez disso, separe suas inclusões como:

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

Isso carregará apenas o (s) arquivo (s) necessário (s) quando essa função for chamada especificamente; permitindo que você mantenha seu escopo global muito mais limpo, o que deve resultar em botas de frio mais rápidas.


Isso deve permitir uma solução muito mais limpa do que o que eu fiz abaixo (embora a explicação abaixo ainda seja válida).


Resposta original

Parece que exigir arquivos e a inicialização geral acontecendo no escopo global é uma grande causa de desaceleração durante a inicialização a frio.

À medida que um projeto adquire mais funções, o escopo global é poluído cada vez mais, piorando o problema - especialmente se você colocar suas funções em arquivos separados (como usar Object.assign(exports, require('./more-functions.js'));em seuindex.js .

Eu consegui obter grandes ganhos no desempenho da inicialização a frio, movendo todos os meus requisitos para um método init, como abaixo, e chamando-o como a primeira linha dentro de qualquer definição de função para esse arquivo. Por exemplo:

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

Vi melhorias de 7-8s a 2-3s ao aplicar essa técnica a um projeto com ~ 30 funções em 8 arquivos. Isso também parece fazer com que as funções precisem ser inicializadas a frio com menos frequência (provavelmente devido ao menor uso de memória?)

Infelizmente, isso ainda torna as funções HTTP pouco úteis para o uso da produção voltada ao usuário.

Esperando que a equipe do Firebase tenha alguns planos no futuro para permitir o escopo adequado das funções, para que apenas os módulos relevantes precisem ser carregados para cada função.

Tyris
fonte
Olá Tyris, estou enfrentando o mesmo problema com a operação de tempo, estou tentando implementar sua solução. apenas tentando entender, quem chama a função init e quando?
Manspof 22/04/19
Olá @AdirZoari, minha explicação sobre o uso de init () e assim por diante provavelmente não é uma prática recomendada; seu valor é apenas demonstrar minhas descobertas sobre o problema principal. Você ficaria muito melhor analisando a variável oculta process.env.FUNCTION_NAMEe usando-a para incluir condicionalmente os arquivos necessários para essa função. O comentário em github.com/firebase/functions-samples/issues/… fornece uma descrição muito boa desse trabalho! Ele garante que o escopo global não seja poluído com métodos e inclui funções irrelevantes.
Tyris
1
Olá @davidverweij, acho que isso não ajudará em termos da possibilidade de suas funções serem executadas duas vezes ou em paralelo. As funções são dimensionadas automaticamente, conforme necessário, para que várias funções (a mesma função ou diferentes) possam ser executadas em paralelo a qualquer momento. Isso significa que você deve considerar a segurança dos dados e o uso de transações. Além disso, confira este artigo sobre as funções possivelmente executando duas vezes: cloud.google.com/blog/products/serverless/...
Tyris
1
O aviso FUNCTIONS_NAMEé válido apenas nos nós 6 e 8, conforme explicado aqui: cloud.google.com/functions/docs/… . O nó 10 deve usarFUNCTION_TARGET
maganap 29/04
1
Obrigado pela atualização @maganap, parece que ele deve ser usado de K_SERVICEacordo com o doco em cloud.google.com/functions/docs/migrating/… - Atualizei minha resposta.
Tyris
7

Estou enfrentando problemas semelhantes com as funções da nuvem do firestore. O maior é o desempenho. Especialmente no caso de startups em estágio inicial, quando você não pode permitir que seus clientes antigos vejam aplicativos "lentos". Uma função simples de geração de documentação para, por exemplo, fornece:

- A execução da função levou 9522 ms, finalizada com o código de status: 200

Então: eu tinha uma página simples de termos e condições. Com as funções de nuvem, a execução devido ao arranque a frio levaria 10 a 15 segundos, mesmo às vezes. Em seguida, mudei para um aplicativo node.js, hospedado no contêiner de mecanismo de aplicativo. O tempo caiu para 2-3 segundos.

Comparo muitos dos recursos do mongodb com o firestore e, às vezes, também me pergunto se durante essa fase inicial do meu produto eu também deveria mudar para um banco de dados diferente. O maior anúncio que tive no firestore foi a funcionalidade de gatilho onCreate, onUpdate dos objetos do documento.

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

Basicamente, se houver partes estáticas do seu site que possam ser descarregadas para o ambiente do aplicativo, talvez não seja uma má idéia.

Sudhakar R
fonte
1
Eu não acho que as funções do Firebase sejam adequadas para a finalidade, na medida em que exibem conteúdo dinâmico voltado para o usuário. Usamos algumas funções HTTP moderadamente para coisas como redefinição de senha, mas, em geral, se você tiver conteúdo dinâmico, sirva-o em outro lugar como um aplicativo expresso (ou use uma linguagem diff).
Tyris
2

Fiz essas coisas também, o que melhora o desempenho quando as funções são aquecidas, mas o começo a frio está me matando. Um dos outros problemas que encontrei é o cors, porque são necessárias duas viagens às funções da nuvem para fazer o trabalho. Tenho certeza de que posso consertar isso, no entanto.

Quando você tem um aplicativo em sua fase inicial (demo), quando não é usado com frequência, o desempenho não será ótimo. Isso é algo que deve ser considerado, pois os adotantes iniciais do produto precisam procurar o melhor possível diante de clientes / investidores em potencial. Como amamos a tecnologia, migramos de estruturas testadas e comprovadas, mas nosso aplicativo parece bastante lento nesse momento. Vou tentar algumas estratégias de aquecimento para melhorar a aparência

Stan Swiniarski
fonte
Estamos testando um cron-job para ativar todas as funções. Talvez essa abordagem também ajude você.
Jesús Fuentes
hey @ JesúsFuentes Eu queria saber se acordar a função funcionou para você. Soa como uma solução louca: D
Alexandr Zavalii
1
Olá @Alexandr, infelizmente ainda não tivemos tempo para fazê-lo, mas está em nossa lista de prioridades. Deve funcionar teoricamente, no entanto. O problema vem com as funções onCall, que precisam ser iniciadas a partir de um aplicativo Firebase. Talvez chamá-los do cliente a cada X minutos? Veremos.
Jesús Fuentes
1
@ Alexandr, devemos conversar fora do Stackoverflow? Podemos nos ajudar com novas abordagens.
Jesús Fuentes
1
@Alexandr, ainda não testamos essa solução alternativa, mas já implantamos nossas funções no europe-west1. Ainda assim, tempos inaceitáveis.
Jesús Fuentes
0

UPDATE / EDIT: nova sintaxe e atualizações em MAY2020

Acabei de publicar um pacote chamado better-firebase-functions : ele pesquisa automaticamente o diretório de funções e aninha corretamente todas as funções encontradas no objeto de exportação, enquanto isola as funções umas das outras para melhorar o desempenho da inicialização a frio.

Se você carregar e armazenar em cache apenas as dependências necessárias para cada função no escopo do módulo, descobrirá que é a maneira mais simples e fácil de manter suas funções otimizadas de maneira eficiente em um projeto de crescimento rápido.

import { exportFunctions } from 'better-firebase-functions'
exportFunctions({__filename, exports})
George43g
fonte
interessante .. onde posso ver o repo de 'better-firebase-functions'?
JerryGoyal
1
github.com/gramstr/better-firebase-functions - confira e deixe-me saber o que você pensa! Sinta-se livre para contribuir também :)
George43g