提示与技巧

本文档介绍设计、实现、测试和部署 Cloud Functions 的最佳实践。

正确做法

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

编写幂等函数

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

请勿启动后台活动

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

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

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

务必删除临时文件

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

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

如果您需要访问长期存储空间,请考虑使用 Cloud Run 卷装载与 Cloud StorageNFS 卷

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

Cloud Functions 框架

为了确保在不同的环境中以一致的方式安装相同的依赖项,我们建议您在软件包管理系统中添加 Functions 框架库,并将依赖项固定到特定版本的 Functions 框架。

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

如果 Functions 框架未明确列为依赖项,系统会在构建过程中使用最新可用版本自动添加该框架。

工具

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

本地开发

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

Firebase 开发者可以使用 Firebase CLI Cloud Functions 模拟器

避免初始化期间发生部署超时

如果函数部署因超时错误而失败,则可能意味着函数的全局范围代码在部署过程中执行时间过长。

Firebase CLI 在部署函数时,有一个默认的函数发现超时时间。如果函数源代码中的初始化逻辑(例如加载模块、进行网络调用等)超出此超时时间,部署可能会失败。

为避免超时,请使用以下策略之一:

使用 onInit() 钩子可避免在部署过程中运行初始化代码。onInit() 钩子中的代码仅在将函数部署到 Cloud Run functions 之后运行,而不是在部署过程中运行。

const { onInit } = require('firebase-functions/v2/core');
const { onRequest } = require('firebase-functions/v2/https');

// Example of a slow initialization task
function slowInitialization() {
  // Simulate a long-running operation (e.g., loading a large model, network request).
  return new Promise(resolve => {
      setTimeout(() => {
          console.log("Slow initialization complete");
          resolve("Initialized Value");
      }, 20000); // Simulate a 20-second delay
  });
}
let initializedValue;

onInit(async () => {
  initializedValue = await slowInitialization();
});

exports.myFunction = onRequest((req, res) => {
  // Access the initialized value. It will be ready after the first invocation.
  res.send(`Value: ${initializedValue}`);
});
from firebase_functions.core import init
from firebase_functions import https_fn
import time

# Example of a slow initialization task
def _slow_initialization():
  time.sleep(20)  # Simulate a 20-second delay
  print("Slow initialization complete")
  return "Initialized Value"

_initialized_value = None

@init
def initialize():
  global _initialized_value
  _initialized_value = _slow_initialization()

@https_fn.on_request()
def my_function(req: https_fn.Request) -> https_fn.Response:
  # Access the initialized value. It will be ready after the first invocation.
  return https_fn.Response(f"Value: {_initialized_value}")

(备选)增加发现超时时间

如果您无法重构代码来使用 onInit(),可以使用 FUNCTIONS_DISCOVERY_TIMEOUT 环境变量增加 CLI 的部署超时时间:

$ export FUNCTIONS_DISCOVERY_TIMEOUT=30
$ firebase deploy --only functions

使用 Sendgrid 发送电子邮件

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

性能

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

避免低并发

由于冷启动成本较高,因此能够在高峰期间重用最近启动的实例,是处理负载的绝佳优化方式。限制并发数会限制可利用现有实例的方式,从而导致更多冷启动。

提高并发性有助于推迟每个实例的多个请求,从而更轻松地处理负载高峰。

谨慎使用依赖项

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

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

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

系统无法保证能保留函数的状态,以用于将来的调用。不过,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}`);
});
import time

from firebase_functions import https_fn

# Placeholder
def heavy_computation():
  return time.time()

# Placeholder
def light_computation():
  return time.time()

# Global (instance-wide) scope
# This computation runs at instance cold-start
instance_var = heavy_computation()

@https_fn.on_request()
def scope_demo(request):

  # Per-function scope
  # This computation runs every time this function is called
  function_var = light_computation()
  return https_fn.Response(f"Instance: {instance_var}; function: {function_var}")
  

此 HTTP 函数接受请求对象 (flask.Request),并返回响应文本,或是可使用 make_response 转换为 Response 对象的任何一组值。

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

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

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

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

有关冷启动和初始化的注意事项

全局初始化在加载时进行。如果没有进行全局初始化,第一个请求需要完成初始化并加载模块,从而导致更长的延迟时间。

不过,全局初始化也会对冷启动产生影响。为了尽量减少这种影响,请仅初始化第一个请求所需的内容,以尽可能缩短第一个请求的延迟时间。

如果您如上文所述为延迟敏感的函数配置了实例数下限,这一点就尤为重要。在这种情况下,在加载时完成初始化并缓存有用的数据可确保第一个请求无需执行这些操作,并能够以低延迟进行响应。

如果您在全局范围内初始化变量,那么根据所用语言的不同,较长的初始化时间可能会导致两种行为: - 对于某些语言和异步库的组合,函数框架可能异步运行并立即返回,导致代码在后台继续运行,这可能会导致无法访问 CPU 等问题。为避免这种情况,您应阻止模块初始化,具体如下文所述。这还可确保在初始化完成之前不会响应请求。- 另一方面,如果初始化是同步的,那么较长的初始化时间会导致更长的冷启动时间,这可能会成为一个问题,尤其是对于负载高峰期间的低并发函数。

异步 Node.js 库预热示例

使用 Firestore 的 Node.js 就是一个异步 Node.js 库的示例。为了充分利用 min_instances,以下代码会在加载时完成加载和初始化,并阻止模块加载。

使用了 TLA,这意味着需要 ES6,为 Node.js 代码使用 .mjs 扩展程序或向 package.json 文件添加 type: module

{
  "main": "main.js",
  "type": "module",
  "dependencies": {
    "@google-cloud/firestore": "^7.10.0",
    "@google-cloud/functions-framework": "^3.4.5"
  }
}
Node.js
import Firestore from '@google-cloud/firestore';
import * as functions from '@google-cloud/functions-framework';

const firestore = new Firestore({preferRest: true});

// Pre-warm firestore connection pool, and preload our global config
// document in cache. In order to ensure no other request comes in,
// block the module loading with a synchronous global request:
const config = await firestore.collection('collection').doc('config').get();

functions.http('fetch', (req, res) => {

// Do something with config and firestore client, which are now preloaded
// and will execute at lower latency.
});

全局初始化示例

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');
});
from firebase_functions import https_fn

# Always initialized (at cold-start)
non_lazy_global = file_wide_computation()

# Declared at cold-start, but only initialized if/when the function executes
lazy_global = None

@https_fn.on_request()
def lazy_globals(request):

  global lazy_global, non_lazy_global

  # This value is initialized only if (and when) the function is called
  if not lazy_global:
      lazy_global = function_specific_computation()

  return https_fn.Response(f"Lazy: {lazy_global}, non-lazy: {non_lazy_global}.")
  

此 HTTP 函数使用延迟初始化的全局变量。它接受请求对象 (flask.Request),并返回响应文本,或是可使用 make_response 转换为 Response 对象的任何一组值。

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

其他资源

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