【问题标题】:Firebase cloud functions is very slowFirebase 云功能很慢
【发布时间】:2017-08-01 07:10:45
【问题描述】:

我们正在开发一个使用新的 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()' 调用的 Promise 是否安全?

标签: node.js firebase firebase-realtime-database google-cloud-functions


【解决方案1】:

firebaser 在这里

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

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

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

好消息是您应该只在开发过程中体验到这一点。一旦你的函数在生产环境中被频繁触发,它们很可能再也不会冷启动了。

【讨论】:

  • 版主注意:本帖所有离题的cmets都已被删除。请使用 cmets 请求澄清或仅提出改进建议。如果您有一个相关但不同的问题,ask a new question,并附上指向此问题的链接以帮助提供上下文。
【解决方案2】:

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

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

这只会在专门调用该函数时加载所需的文件;允许您保持全局范围更清洁,这将导致更快的冷启动。


这应该允许比我在下面所做的更简洁的解决方案(尽管下面的解释仍然成立)。


原答案

看起来需要文件和在全局范围内发生的一般初始化是冷启动期间速度变慢的一个重要原因。

随着项目获得更多功能,全局范围受到越来越多的污染,使问题变得更糟 - 特别是如果您将功能范围划分到单独的文件中(例如在您的 index.js 中使用 Object.assign(exports, require('./more-functions.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-8 秒缩短到 2-3 秒。这似乎也导致函数不需要经常冷启动(可能是由于内存使用率较低?)

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

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

【讨论】:

  • 嘿 Tyris,我在时间操作方面遇到了同样的问题,我正在尝试实施您的解决方案。只是想了解,谁调用了 init 函数以及何时调用?
  • 嗨@davidverweij,我认为这对您的函数运行两次或并行运行的可能性没有帮助。函数会根据需要自动扩展,因此多个函数(相同或不同的函数)可以随时并行运行。这意味着您必须考虑数据安全并考虑使用事务。另外,请查看这篇关于您的函数可能运行两次的文章:cloud.google.com/blog/products/serverless/…
  • 注意 FUNCTIONS_NAME 仅对节点 6 和 8 有效,如下所述:cloud.google.com/functions/docs/…。节点 10 应使用 FUNCTION_TARGET
  • 感谢@maganap 的更新,根据 cloud.google.com/functions/docs/migrating/… 的 doco 看来,它应该使用 K_SERVICE - 我已经更新了我的答案。
  • @virus - 函数为每个注册的函数创建一个唯一的实例。这就是为什么它有效,否则你会遇到你描述的问题。这也是您可以独立部署每个函数的原因,以及 Firebase 控制台中列出的每个函数都可以分配不同的节点版本来运行它们的原因。具有讽刺意味的是,如果所有功能都在单个服务器实例中运行,那么这种冷启动问题将显着降低,因为服务器实例的停机频率会降低。
【解决方案3】:

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

-- 函数执行耗时 9522 毫秒,完成状态码:200

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

我一直在将 mongodb 的许多功能与 firestore 进行比较,有时我也想知道在我的产品的早期阶段是否也应该迁移到不同的数据库。我在 firestore 中获得的最大广告是文档对象的 onCreate、onUpdate 触发功能。

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

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

【讨论】:

  • 我认为 Firebase 函数不适合显示面向用户的动态内容。我们很少使用一些 HTTP 函数来进行密码重置等操作,但一般来说,如果您有动态内容,请将其作为快速应用程序(或使用差异语言)在其他地方提供。
【解决方案4】:

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

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

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

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

【讨论】:

  • 有趣.. 我在哪里可以看到“更好的火力基地功能”的回购?
  • github.com/gramstr/better-firebase-functions - 请查看并告诉我您的想法!也可以随意贡献:)
  • 包装很整洁,但有一堆未解决的问题需要解决。
【解决方案5】:

我也做过这些事情,一旦功能热身,性能就会提高,但是冷启动让我很生气。我遇到的其他问题之一是 cors,因为它需要两次访问云功能才能完成工作。不过,我确信我可以解决这个问题。

当您的应用程序处于早期(演示)阶段且不经常使用时,性能不会很好。这是应该考虑的事情,因为早期产品的早期采用者需要在潜在客户/投资者面前表现出最好的一面。我们喜欢这项技术,因此我们从旧的久经考验的框架迁移,但我们的应用程序在这一点上似乎相当缓慢。接下来我要尝试一些热身策略,让它看起来更好

【讨论】:

  • 我们正在测试一个 cron-job 来唤醒每一个函数。也许这种方法对您也有帮助。
  • 嘿@JesúsFuentes 我只是想知道唤醒该功能是否适合您。听起来像一个疯狂的解决方案:D
  • 嗨@Alexandr,遗憾的是我们还没有时间去做,但它在我们的首要任务列表中。不过,它应该在理论上有效。问题来自 onCall 函数,需要从 Firebase 应用程序启动。也许每 X 分钟从客户那里给他们打电话?我们拭目以待。
  • @Alexandr 我们可以在 Stackoverflow 之外进行对话吗?我们可能会在新方法上互相帮助。
  • @Alexandr 我们尚未测试这种“唤醒”解决方法,但我们已经将我们的功能部署到 europe-west1。仍然是不可接受的时期。
【解决方案6】:

我在 Firebase Functions 中的第一个项目中遇到了非常糟糕的性能,其中一个简单的函数将在几分钟内执行(知道函数执行的 60 秒限制,我知道我的函数有问题)。我的问题是我没有正确终止函数

如果有人遇到同样的问题,请确保通过以下方式终止函数:

  1. 为 HTTP 触发器发送响应
  2. 返回对后台触发器的承诺

这是来自 Firebase 的 youtube link,它帮助我解决了问题

【讨论】:

    【解决方案7】:

    由于其中使用了 gRpc 库,Cloud Functions 与 Firestore 库一起使用时冷启动时间不一致。

    我们最近制作了一个完全兼容的 Rest 客户端 (@bountyrush/firestore),旨在与官方 nodejs-firestore 客户端同步更新。

    幸运的是,冷启动现在好多了,我们甚至放弃了使用我们之前使用的 redis 内存存储作为缓存。

    集成步骤:

    1. npm install @bountyrush/firestore
    2. Replace require('@google-cloud/firestore') with require('@bountyrush/firestore')
    3. Have FIRESTORE_USE_REST_API = 'true' in your environment variables. (process.env.FIRESTORE_USE_REST_API should be set to 'true' for using in rest mode. If its not set, it just standard firestore with grpc connections)
    

    【讨论】:

      猜你喜欢
      • 2020-11-12
      • 1970-01-01
      • 2020-06-09
      • 2017-11-08
      • 2018-11-29
      • 2018-05-09
      • 2020-06-13
      • 2019-03-22
      相关资源
      最近更新 更多