提示与技巧

本文档介绍了设计、实现、测试和部署 Cloud Functions 函数的最佳做法。

正确做法

本部分介绍 Cloud Functions 函数设计和实现方面的常规最佳做法。

编写幂等函数

即使您的函数被多次调用,也应产生相同的结果。这样,如果前面的代码调用中途失败,您可以重新调用。如需了解详情,请参阅重试事件驱动型函数

请勿启动后台活动

后台活动是指在函数终止后发生的任何活动。一旦函数返回或以其他方式发出完成信号(例如通过调用 Node.js 事件驱动型函数中的 callback 参数),函数调用就会完成。在正常终止后运行的任何代码都无法访问 CPU,因而无法继续执行。

另外,当在同一环境中执行后续调用时,您的后台活动将继续进行,因而会干扰新的调用。这可能会导致难以诊断的意外行为和错误。在函数终止后访问网络通常会导致连接重置(错误代码为 ECONNRESET)。

通常可以在各调用产生的日志中检测到后台活动,相关信息记录在指示调用已完成的行的后面。后台活动有时可能会深藏在代码中,尤其是在存在回调函数或定时器等异步操作的情况下。 请检查您的代码,以确保所有异步操作都会在函数终止之前完成。

务必删除临时文件

临时目录中的本地磁盘存储是内存中的文件系统。您写入的文件会占用函数可以使用的内存,并且有时会在多次调用过程中持续存在。如果不明确删除这些文件,最终可能会导致内存不足错误,并且随后需要进行冷启动。

如需查看单个函数所使用的内存,您可以访问 GCP 控制台,在函数列表中选择相应的函数,然后选择“内存用量”图。

请勿试图在临时目录之外执行写入操作,并务必使用独立于平台/操作系统的方法构建文件路径。

您可以在使用流水线处理大型文件时减少内存要求。例如,要在 Cloud Storage 上处理文件,您可以创建读取流,通过基于流的进程传递读取流,然后将输出流直接写入 Cloud Storage。

Cloud Functions 框架

部署函数时,系统会自动使用其当前版本将 Functions 框架添加为依赖项。为确保在不同环境中以一致的方式安装相同的依赖项,我们建议您将函数固定到特定版本的 Functions 框架。

为此,请在相关的锁定文件中添加您的首选版本(例如,对于 Node.js,为 package-lock.json,对于 Python,则为 requirements.txt)。

工具

本部分指导您如何使用工具来实现和测试 Cloud Functions 函数并与之互动。

本地开发

函数部署需要一些时间,因此在本地测试函数的代码通常会更快。

Firebase 开发者可以使用 Firebase CLI Cloud Functions Emulator

使用 Sendgrid 发送电子邮件

Cloud Functions 不允许在端口 25 上建立出站连接,因此您无法建立到 SMTP 服务器的非安全连接。推荐使用 SendGrid 发送电子邮件。如需了解发送电子邮件的其他方式,请参阅适用于 Google Compute Engine 的从实例发送电子邮件教程。

性能

本部分介绍性能优化方面的最佳实践。

谨慎使用依赖项

由于函数是无状态的,执行环境通常是从头开始初始化(称为“冷启动”)。当发生冷启动时,系统会对函数的全局环境进行评估。

如果您的函数导入了模块,那么在冷启动期间,这些模块的加载时间会造成调用延迟加重。正确加载依赖项而不加载函数不使用的依赖项,即可缩短此延迟时间以及函数部署时间。

使用全局变量,以便在日后的调用中重复使用对象

系统无法保证能保留 Cloud Functions 函数的状态,以用于将来的调用。不过,Cloud Functions 经常会回收利用先前调用的执行环境。如果您在全局范围内声明一个变量,就可以在后续的调用中再次使用该变量的值,而不必重新计算。

通过这种方式,您可以缓存在每次调用函数时重建的成本较高的对象。将此类对象从函数体移到全局范围可能会显著提升性能。以下示例会为每个函数实例创建一个重量级对象(每个实例仅限一次),以供指定实例处理的所有函数调用共用:

console.log('Global scope');
const perInstance = heavyComputation();
const functions = require('firebase-functions');

exports.function = functions.https.onRequest((req, res) => {
    console.log('Function invocation');
    const perFunction = lightweightComputation();

    res.send(`Per instance: ${perInstance}, per function: ${perFunction}`);
});

尤为重要的是,您应在全局范围内缓存网络连接、库引用和 API 客户端对象。如需查看相关示例,请参阅优化网络连接方式

对全局变量进行延迟初始化

如果您在全局范围内初始化变量,系统始终会通过冷启动调用执行初始化代码,而这会增加函数的延迟时间。在某些情况下,如果被调用的服务在 try/catch 块中未得到正确处理,则会导致这些服务间歇性超时。如果某些对象并非在所有代码路径中都会用到,可以考虑按需对它们进行延迟初始化:

const functions = require('firebase-functions');
let myCostlyVariable;

exports.function = functions.https.onRequest((req, res) => {
    doUsualWork();
    if(unlikelyCondition()){
        myCostlyVariable = myCostlyVariable || buildCostlyVariable();
    }
    res.status(200).send('OK');
});

如果您在单个文件中定义多个函数,并且不同的函数使用不同的变量,这种做法尤其有用。如果不使用延迟初始化,您可能会因为初始化后永远不会再用到的变量而浪费资源。

通过设置实例数下限减少冷启动次数

默认情况下,Cloud Functions 会根据传入请求的数量扩缩实例数量。您可以更改这种默认行为,只需设置 Cloud Functions 必须保持就绪状态以处理请求的实例数下限即可。设置实例数下限可以减少应用的冷启动次数。如果您的应用对延迟时间较为敏感,我们建议您设置实例数下限。

如需详细了解这些运行时选项,请参阅控制扩缩行为

其他资源

如需详细了解如何优化性能,请观看“Google Cloud 性能指南”视频 Cloud Functions 函数冷启动时间