Las funciones en la nube de Firebase son muy lentas

131

Estamos trabajando en una aplicación que utiliza las nuevas funciones en la nube de Firebase. Lo que está sucediendo actualmente es que una transacción se coloca en el nodo de la cola. Y luego la función elimina ese nodo y lo coloca en el nodo correcto. Esto se ha implementado debido a la capacidad de trabajar sin conexión.

Nuestro problema actual es la velocidad de la función. La función en sí misma tarda unos 400 ms, así que está bien. Pero a veces las funciones toman mucho tiempo (alrededor de 8 segundos), mientras que la entrada ya se agregó a la cola.

Sospechamos que el servidor tarda en iniciarse, porque cuando hacemos la acción una vez más después del primero. Lleva mucho menos tiempo.

Hay alguna manera de arreglar este problema? Aquí abajo agregué el código de nuestra función. Sospechamos que no tiene nada de malo, pero lo agregamos por si acaso.

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
fuente
¿Es seguro no devolver la promesa de las llamadas 'once ()' anteriores?
jazzgil

Respuestas:

111

bombero aquí

Parece que está experimentando un llamado arranque en frío de la función.

Cuando su función no se ha ejecutado en algún momento, Cloud Functions la pone en un modo que utiliza menos recursos. Luego, cuando vuelve a presionar la función, restaura el entorno desde este modo. El tiempo que se tarda en restaurar consiste en un costo fijo (por ejemplo, restaurar el contenedor) y un costo variable parcial (por ejemplo, si usa muchos módulos de nodo, puede llevar más tiempo).

Continuamente estamos monitoreando el desempeño de estas operaciones para asegurar la mejor combinación entre la experiencia del desarrollador y el uso de recursos. Espere que estos tiempos mejoren con el tiempo.

La buena noticia es que solo debe experimentar esto durante el desarrollo. Una vez que sus funciones se activan con frecuencia en la producción, es probable que casi nunca vuelvan a comenzar en frío.

Frank van Puffelen
fuente
3
Nota para el moderador : Se han eliminado todos los comentarios fuera de tema en esta publicación. Utilice los comentarios para solicitar aclaraciones o sugerir solo mejoras. Si tiene una pregunta relacionada pero diferente, haga una nueva pregunta e incluya un enlace a esta para ayudar a proporcionar contexto.
Bhargav Rao
55

Actualización de mayo de 2020 Gracias por el comentario de maganap: en el Nodo 10+ FUNCTION_NAMEse reemplaza por K_SERVICE( FUNCTION_TARGETes la función en sí, no su nombre, el reemplazo ENTRY_POINT). Los ejemplos de código a continuación se han actualizado a continuación.

Más información en https://cloud.google.com/functions/docs/migrating/nodejs-runtimes#nodejs-10-changes

Actualización : parece que muchos de estos problemas se pueden resolver utilizando la variable oculta process.env.FUNCTION_NAMEcomo se ve aquí: https://github.com/firebase/functions-samples/issues/170#issuecomment-323375462

Actualizar con código : por ejemplo, si tiene el siguiente archivo de índice:

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

Luego, todos sus archivos se cargarán y todos los requisitos de esos archivos también se cargarán, lo que generará una gran sobrecarga y contaminará su alcance global para todas sus funciones.

En lugar de separar su incluye 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');
}

Esto solo cargará los archivos requeridos cuando se llama específicamente a esa función; lo que le permite mantener su alcance global mucho más limpio, lo que debería resultar en botas de frío más rápidas.


Esto debería permitir una solución mucho más ordenada que la que he hecho a continuación (aunque la explicación a continuación sigue siendo válida).


Respuesta original

Parece que requerir archivos e inicialización general en el ámbito global es una gran causa de desaceleración durante el arranque en frío.

A medida que un proyecto obtiene más funciones, el alcance global se contamina cada vez más, lo que empeora el problema, especialmente si coloca sus funciones en archivos separados (como al usar Object.assign(exports, require('./more-functions.js'));en su index.js.

Me las arreglé para ver grandes ganancias en el rendimiento de arranque en frío moviendo todos mis requisitos a un método init como se muestra a continuación y luego llamándolo como la primera línea dentro de cualquier definición de función para ese archivo. P.ej:

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

He visto mejoras de aproximadamente 7-8 segundos a 2-3 segundos al aplicar esta técnica a un proyecto con ~ 30 funciones en 8 archivos. Esto también parece causar que las funciones deban iniciarse en frío con menos frecuencia (¿presumiblemente debido a un menor uso de memoria?)

Desafortunadamente, esto todavía hace que las funciones HTTP sean apenas utilizables para el uso de producción orientado al usuario.

Esperando que el equipo de Firebase tenga algunos planes en el futuro para permitir el alcance adecuado de las funciones, de modo que solo los módulos relevantes necesiten cargarse para cada función.

Tyris
fuente
Hola Tyris, estoy enfrentando el mismo problema con la operación del tiempo, estoy tratando de implementar tu solución. solo tratando de entender, ¿quién llama a la función init y cuándo?
Manspof
Hola @AdirZoari, mi explicación del uso de init () y demás probablemente no sea la mejor práctica; su valor es solo para demostrar mis hallazgos sobre el problema central. Sería mucho mejor mirar la variable oculta process.env.FUNCTION_NAMEy usarla para incluir condicionalmente los archivos necesarios para esa función. ¡El comentario en github.com/firebase/functions-samples/issues/… da una muy buena descripción de este trabajo! Asegura que el alcance global no esté contaminado con métodos e incluye funciones irrelevantes.
Tyris
1
Hola @davidverweij, no creo que esto ayude en términos de la posibilidad de que sus funciones se ejecuten dos veces o en paralelo. Las funciones escalan automáticamente según sea necesario para que múltiples funciones (la misma función o diferentes) puedan ejecutarse en paralelo en cualquier momento. Esto significa que debe considerar la seguridad de los datos y considerar el uso de transacciones. Además, consulte este artículo sobre sus funciones que posiblemente se ejecuten dos veces: cloud.google.com/blog/products/serverless/…
Tyris
1
El aviso FUNCTIONS_NAMEsolo es válido con los nodos 6 y 8, como se explica aquí: cloud.google.com/functions/docs/… . Nodo 10 debería usarFUNCTION_TARGET
maganap
1
Gracias por la actualización @maganap, parece que debería usarse K_SERVICEsegún doco en cloud.google.com/functions/docs/migrating/… - He actualizado mi respuesta.
Tyris
7

Estoy enfrentando problemas similares con las funciones de nube de firestore. El más grande es el rendimiento. Especialmente en el caso de nuevas empresas en etapa inicial, cuando no puede permitirse que sus primeros clientes vean aplicaciones "lentas". Una función simple de generación de documentación, por ejemplo, proporciona esto:

- La ejecución de la función tomó 9522 ms, finalizó con el código de estado: 200

Luego: tenía una página de términos y condiciones sencilla. Con las funciones en la nube, la ejecución debido al arranque en frío tardaría entre 10 y 15 segundos, incluso a veces. Luego lo moví a una aplicación node.js, alojada en un contenedor de appengine. El tiempo se ha reducido a 2-3 segundos.

He estado comparando muchas de las características de mongodb con firestore y, a veces, también me pregunto si durante esta fase temprana de mi producto también debería pasar a una base de datos diferente. El mayor consejo que tuve en firestore fue la funcionalidad de activación onCreate, onUpdate de objetos de documento.

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

Básicamente, si hay partes estáticas de su sitio que pueden descargarse en un entorno de motor, quizás no sea una mala idea.

Sudhakar R
fuente
1
No creo que las funciones de Firebase sean adecuadas para el propósito en cuanto a mostrar contenido dinámico para el usuario. Usamos algunas funciones HTTP con moderación para cosas como el restablecimiento de contraseña, pero en general si tiene contenido dinámico, sirva en otro lugar como una aplicación express (o use un lenguaje diff).
Tyris
2

También he hecho estas cosas, lo que mejora el rendimiento una vez que se calientan las funciones, pero el arranque en frío me está matando. Uno de los otros problemas que he encontrado es con cors, porque se necesitan dos viajes a las funciones de la nube para hacer el trabajo. Sin embargo, estoy seguro de que puedo arreglar eso.

Cuando tiene una aplicación en su fase inicial (demo) cuando no se usa con frecuencia, el rendimiento no será excelente. Esto es algo que debe considerarse, ya que los primeros usuarios con productos tempranos deben verse lo mejor posible frente a clientes / inversores potenciales. Nos encantó la tecnología, por lo que migramos de marcos de trabajo antiguos y probados, pero nuestra aplicación parece bastante lenta en este momento. A continuación, intentaré algunas estrategias de calentamiento para que se vea mejor

Stan Swiniarski
fuente
Estamos probando un trabajo cron para activar cada función. Quizás este enfoque también te ayude.
Jesús Fuentes
hola @ JesúsFuentes Me preguntaba si despertar la función funcionó para ti. Suena como una solución loca: D
Alexandr Zavalii
1
Hola @Alexandr, lamentablemente aún no tuvimos tiempo de hacerlo, pero está en nuestra lista de máxima prioridad. Sin embargo, debería funcionar teóricamente. El problema viene con las funciones onCall, que deben iniciarse desde una aplicación Firebase. ¿Quizás llamarlos desde el cliente cada X minutos? Ya veremos.
Jesús Fuentes
1
@Alexandr ¿tendremos una conversación fuera de Stackoverflow? Podríamos ayudarnos mutuamente con nuevos enfoques.
Jesús Fuentes
1
@Alexandr aún no probamos esta solución alternativa de "activación", pero ya implementamos nuestras funciones en europe-west1. Aún así, tiempos inaceptables.
Jesús Fuentes
0

ACTUALIZACIÓN / EDICIÓN: nueva sintaxis y actualizaciones próximamente MAYO2020

Acabo de publicar un paquete llamado better-firebase-functions, busca automáticamente en su directorio de funciones y anida correctamente todas las funciones encontradas en su objeto de exportaciones, al tiempo que aísla las funciones entre sí para mejorar el rendimiento del arranque en frío.

Si carga de manera diferida y almacena en caché solo las dependencias que necesita para cada función dentro del alcance del módulo, encontrará que es la forma más simple y fácil de mantener sus funciones de manera óptima y eficiente en un proyecto de rápido crecimiento.

import { exportFunctions } from 'better-firebase-functions'
exportFunctions({__filename, exports})
George43g
fuente
interesante ... ¿dónde puedo ver el repositorio de 'better-firebase-functions'?
JerryGoyal
1
github.com/gramstr/better-firebase-functions - ¡échale un vistazo y hazme saber lo que piensas! Siéntase libre de contribuir también :)
George43g