Firebase 云功能很慢

新手上路,请多包涵

我们正在开发一个使用新的 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;
}

原文由 Stan van Heumen 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 739
2 个回答

火力基地在这里

听起来您正在经历该功能的所谓冷启动。

当您的函数在一段时间内未执行时,Cloud Functions 会将其置于使用较少资源的模式,这样您就无需为未使用的计算时间付费。然后,当您再次点击该功能时,它会从此模式恢复环境。恢复所需的时间包括固定成本(例如恢复容器)和部分可变成本(例如 ,如果您使用大量节点模块,可能需要更长的时间)。

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

好消息是你应该只在开发过程中体验到这一点。一旦你的函数在生产环境中被频繁触发,它们很可能几乎不会再次冷启动,特别是如果它们有稳定的流量。但是,如果某些功能倾向于看到流量高峰,您仍然会看到每个高峰的冷启动。在这种情况下,您可能需要考虑 minInstances 设置 以始终保持一定数量的延迟关键函数实例温暖。

原文由 Frank van Puffelen 发布,翻译遵循 CC BY-SA 4.0 许可协议

2021 年 3 月更新 可能值得查看以下来自 @George43g 的答案,它提供了一个巧妙的解决方案来自动执行以下过程。注意 - 我自己没有尝试过,所以不能保证,但它似乎使这里描述的过程自动化。您可以在 https://github.com/gramstr/better-firebase-functions 阅读更多内容 - 否则继续阅读以了解如何自己实现它并了解函数内部发生的事情。

2020 年 5 月更新 感谢 maganap 的评论 - 在 Node 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;
}

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

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

希望 Firebase 团队在未来有一些计划来允许适当的功能范围界定,以便只需要为每个功能加载相关模块。

原文由 Tyris 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题