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;
}
57531 次浏览

我是消防员

听起来你正在经历一个所谓的冷启动功能。

当你的函数在一段时间内没有被执行时,云函数会把它放在一个使用更少资源的模式下,这样你就不会为你没有使用的计算时间付费。然后,当您再次命中该函数时,它将从此模式恢复环境。恢复所需的时间包括一个固定成本(例如恢复容器)和一个部件变量成本(例如 如果使用大量的节点模块,则可能需要更长的时间)。

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

好消息是您只能在开发过程中体验到这一点。一旦您的功能在生产环境中频繁地被触发,它们很可能再也不会出现冷启动,特别是如果它们具有一致的流量。但是,如果某些函数倾向于看到流量峰值,那么每个峰值仍然会看到冷启动。在这种情况下,您可能需要考虑使用 minInstances设定来始终保持一个关键延迟功能的设置数量。

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

更新2020年5月 感谢 maganap 的评论-在 Node 10 + FUNCTION_NAME被取代为 K_SERVICE(FUNCTION_TARGET是函数本身,而不是它的名字,取代了 ENTRY_POINT)。下面的代码示例已经在下面进行了更新。

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

更新 -看起来很多这样的问题可以通过使用隐藏变量 process.env.FUNCTION_NAME来解决,如下所示: < a href = “ https://github.com/firebase/function-sample/questions/170 # issecomment-323375462”rel = “ norefrer”> 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 团队在未来有一些计划,以允许适当的功能范围,以便只有相关的模块曾经需要为每个功能加载。

我也做了这些事情,一旦功能被预热,性能就会得到改善,但是冷启动会让我痛不欲生。我遇到的另一个问题是 cors,因为需要两次访问云功能才能完成任务。不过我肯定能搞定。

当你有一个应用程序在其早期(演示)阶段,当它不是经常使用,性能不会是很好的。这是应该考虑的事情,因为早期产品的早期采用者需要在潜在客户/投资者面前展示他们最好的一面。我们喜欢这项技术,所以我们从旧的可靠的框架迁移过来,但是我们的应用程序在这一点上似乎相当迟缓。接下来我将尝试一些热身策略,使它看起来更好

我在火灾恢复云功能方面也面临着类似的问题。最重要的是表现。特别是在创业初期,你不能让你的早期客户看到“迟缓”的应用程序。一个简单的文档生成函数,例如:

——执行功能需要9522毫秒,完成状态代码: 200

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

我一直在比较 mongodb 和 firestore 的许多特性,有时我也想知道,在我的产品的这个早期阶段,我是否也应该迁移到一个不同的数据库。火灾恢复中最大的广告是文档对象的 onCreate,onUpdate 触发器功能。

Https://db-engines.com/en/system/google+cloud+firestore%3bmongodb

基本上,如果站点的静态部分可以卸载到 appengine 环境中,这也许是个不错的主意。

更新: 2022-lib 再次维护。Firebase 现在有能力保持实例温暖,但仍然有潜在的性能和代码结构的好处。

更新/编辑: 新的语法和更新即将到来 MAY2020

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

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

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

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

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

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

这是来自 Firebase 的 Youtube 链接,它帮助我解决了这个问题

由于使用了 gRpc 库,所以在使用 firestore 库时,Cloud 函数的冷启动时间不一致。

我们最近做了一个完全兼容的 REST 客户端(@ Bountyrush/firestore) ,其目的是获得并行更新的官方 nodejs-firest 客户端。

幸运的是,冷启动现在好多了,我们甚至放弃了使用 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)