Firebase云功能非常慢


131

我们正在开发使用新的Firebase云功能的应用程序。当前正在发生的事情是将事务放入队列节点中。然后函数删除该节点并将其放入正确的节点。由于能够脱机工作,因此已经实现了该功能。

我们当前的问题是功能的速度。该函数本身大约需要400毫秒,所以没关系。但是有时该功能需要很长时间(大约8秒),而该条目已被添加到队列中。

我们怀疑服务器需要花费一些时间来启动,因为在第一个操作之后我们再次执行该操作。它花费的时间更少。

有什么办法可以解决这个问题?在这里,我添加了我们函数的代码。我们怀疑它没有问题,但是为了以防万一,我们添加了它。

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

不返回上述“ once()”调用的承诺是否安全?
jazzgil

Answers:


111

在这里放火

听起来您正在经历所谓的函数冷启动。

如果一段时间未执行您的函数,Cloud Functions会将其置于使用较少资源的模式下。然后,当您再次点击该功能时,它将从该模式恢复环境。恢复所花费的时间包括固定成本(例如,恢复容器)和部分可变成本(例如,如果您使用很多节点模块,则可能会花费更长的时间)。

我们将持续监控这些操作的性能,以确保开发人员体验和资源使用之间的最佳结合。因此,期望这些时间随着时间的推移而改善。

好消息是,您应该仅在开发过程中体会到这一点。一旦您的功能在生产中被频繁触发,它们很有可能再也不会冷落了。


3
主持人注意:此帖上所有主题外的评论均已删除。请使用评论要求澄清或仅提出改进建议。如果您有一个相关但不同的问题,请提出一个新问题,并包括指向该问题的链接以帮助提供上下文。
巴尔加夫(Bhargav Rao)

55

2020年5月更新感谢maganap的评论-在节点10+中FUNCTION_NAME被替换为K_SERVICEFUNCTION_TARGET是函数本身,不是它的名称,替换为ENTRY_POINT)。下面的代码示例已在下面加注。

有关更多信息,访问https://cloud.google.com/functions/docs/migrating/nodejs-runtimes#nodejs-10-changes

更新 -看起来很多这些问题都可以使用隐藏变量来解决,process.env.FUNCTION_NAME如下所示:https : //github.com/firebase/functions-samples/issues/170#issuecomment-323375462

使用代码更新 -例如,如果您具有以下索引文件:

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

然后,将加载所有文件,并且还将加载所有这些文件的要求,这将导致大量开销并污染所有功能的全局作用域。

而是将您的包含项分离为:

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

仅在专门调用该函数时,才加载所需文件。使您的全球范围更整洁,这将导致更快的冷启动。


与下面我所做的相比,这应该可以提供更整洁的解决方案(尽管下面的解释仍然成立)。


原始答案

看起来需要文件,并且在全局范围内进行的常规初始化是导致冷启动过程中速度降低的一个重要原因。

随着项目获得更多的功能,全局范围受到越来越多的污染,使得问题更加严重-尤其是如果您将函数范围划分为单独的文件(例如通过Object.assign(exports, require('./more-functions.js'));index.js

通过将我的所有需求移到下面的init方法中,然后将该文件作为该文件的任何函数定义中的第一行,我已经设法在冷启动性能上取得了巨大的进步。例如:

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

当将此技术应用于在8个文件中具有约30个功能的项目时,我看到了从大约7-8s到2-3s的改进。这似乎也导致不需要频繁启动功能(大概是由于较低的内存使用量?)

不幸的是,这仍然使得HTTP函数几乎不能用于面向用户的生产使用。

希望Firebase团队将来有一些计划,以便对功能进行适当的范围界定,以便每个功能仅需要加载相关模块。


嗨,泰瑞斯,我在计时方面也遇到了同样的问题,我正在尝试实施您的解决方案。只是想了解,谁在何时调用init函数?
Manspof

@AdirZoari,您好,我对使用init()等的解释可能不是最佳实践。它的价值仅仅是证明我对核心问题的发现。您最好查看隐藏变量,process.env.FUNCTION_NAME然后使用该变量有条件地包括该功能所需的文件。github.com/firebase/functions-samples/issues/…上的评论对这项工作给出了很好的描述!它确保全局范围不会被方法污染,并且不会包含不相关的函数。
泰瑞斯(Tyris)'18

1
@davidverweij,您好,对于您的函数运行两次或并行运行的可能性,我认为这没有帮助。功能会根据需要自动缩放,因此可以随时并行运行多个功能(相同功能或不同功能)。这意味着您必须考虑数据安全性并考虑使用事务。此外,检查出这篇文章可能会运行在你的函数两次:cloud.google.com/blog/products/serverless/...
Tyris

1
注意FUNCTIONS_NAME仅在节点6和8上有效,如此处所述:cloud.google.com/functions/docs/…。节点10应该使用FUNCTION_TARGET
maganap

1
感谢@maganap的更新,看来应该K_SERVICE根据cloud.google.com/functions/docs/migrating / ... 上的doco 使用-我已经更新了答案。
泰里斯(Tyris)

7

我在使用Firestore云功能时遇到类似的问题。最大的是性能。特别是在早期创业公司的情况下,当您无法让早期客户看到“呆滞”的应用程序时。一个简单的文档生成功能,例如:

-函数执行花费了9522毫秒,状态码为200

然后:我有一个直观的条款和条件页面。使用云功能时,由于冷启动而导致的执行有时甚至需要10-15秒。然后,我将其移至托管在appengine容器上的node.js应用程序。时间已减少到2-3秒。

我一直在比较mongodb和firestore的许多功能,有时我也想知道在产品的早期阶段是否也应该转移到其他数据库。我在firestore中最大的副词是触发功能,即文档对象的onCreate,onUpdate。

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

基本上,如果您网站上的静态部分可以卸载到Appengine环境中,则可能不是一个坏主意。


1
我认为Firebase Functions不适合显示动态的面向用户的内容。我们很少使用HTTP功能来进行密码重置等操作,但是通常,如果您具有动态内容,则可以将其作为快速应用程序(或使用diff语言)在其他地方使用。
泰里斯(Tyris)

2

我也做了这些事情,可以在功能预热后提高性能,但是冷启动使我丧命。我遇到的另一个问题是cors,因为需要两次访问云功能才能完成工作。我相信我可以解决这个问题。

当您的应用处于早期(演示)阶段且不经常使用时,其性能将不会很好。这是应该考虑的事情,因为拥有早期产品的早期采用者需要在潜在客户/投资者面前展现自己的最佳状态。我们热爱这项技术,因此从较旧的可靠框架进行了迁移,但此时我们的应用程序似乎很缓慢。我接下来将尝试一些热身策略以使其看起来更好


我们正在测试一个cron-job来唤醒每个功能。也许这种方法对您也有帮助。
赫苏斯·富恩特斯

嘿@JesúsFuentes我只是想知道唤醒该功能是否对您有用。听起来像是个疯狂的解决方案:D
亚历山大·扎瓦利

1
@Alexandr,您好,很遗憾,我们还没有时间这样做,但是它在我们的首要任务列表中。但是,它应该在理论上起作用。问题在于onCall函数,该函数需要从Firebase App启动。也许每隔X分钟从客户那里打电话给他们一次?我们拭目以待。
赫苏斯·富恩特斯

1
@Alexandr我们应该在Stackoverflow之外进行对话吗?我们可能会通过新方法互相帮助。
赫苏斯·富恩特斯

1
@Alexandr我们尚未测试此“唤醒”解决方法,但我们已经将功能部署到europe-west1。尽管如此,仍然是无法接受的时期。
赫苏斯·富恩特斯

0

更新/编辑:2020年5月即将推出的新语法和更新

我刚刚发布了一个名为的程序包better-firebase-functions,它将自动搜索您的函数目录,并将所有找到的函数正确嵌套在导出对象中,同时将这些函数彼此隔离以提高冷启动性能。

如果您仅延迟加载和缓存模块范围内每个函数所需的依赖项,您会发现这是在快速增长的项目上保持函数最佳效率的最简单,最简单的方法。

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

有趣..我在哪里可以看到“更好的firebase功能”的回购?
JerryGoyal

1
github.com/gramstr/better-firebase-functions-请检查一下,让我知道您的想法!也可以贡献自己的力量:)
George43g '19
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.