提示与技巧
本文档介绍了设计、实现、测试和部署 Cloud Run functions 函数的最佳做法。
正确做法
本部分介绍 Cloud Run functions 函数设计和实现方面的常规最佳做法。
编写幂等函数
即使您的函数被多次调用,也应产生相同的结果。这样,如果前面的代码调用中途失败,您可以重新调用。如需了解详情,请参阅重试事件驱动型函数。
确保 HTTP 函数发送 HTTP 响应
如果您的函数是由 HTTP 触发,请记得发送 HTTP 响应,如下所示。否则,函数的执行过程可能会发生超时。如果发生这种情况,您将需要为整段超时时间付费。 超时还可能使后续调用出现不可预测的行为或冷启动,从而导致意外行为或延时增加。
Node.js
Python
Go
Java
C#
using Google.Cloud.Functions.Framework; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System.IO; using System.Text.Json; using System.Threading.Tasks; namespace HelloHttp; public class Function : IHttpFunction { private readonly ILogger _logger; public Function(ILogger<Function> logger) => _logger = logger; public async Task HandleAsync(HttpContext context) { HttpRequest request = context.Request; // Check URL parameters for "name" field // "world" is the default value string name = ((string) request.Query["name"]) ?? "world"; // If there's a body, parse it as JSON and check for "name" field. using TextReader reader = new StreamReader(request.Body); string text = await reader.ReadToEndAsync(); if (text.Length > 0) { try { JsonElement json = JsonSerializer.Deserialize<JsonElement>(text); if (json.TryGetProperty("name", out JsonElement nameElement) && nameElement.ValueKind == JsonValueKind.String) { name = nameElement.GetString(); } } catch (JsonException parseException) { _logger.LogError(parseException, "Error parsing JSON request"); } } await context.Response.WriteAsync($"Hello {name}!", context.RequestAborted); } }
Ruby
PHP
<?php use Google\CloudFunctions\FunctionsFramework; use Psr\Http\Message\ServerRequestInterface; // Register the function with Functions Framework. // This enables omitting the `FUNCTIONS_SIGNATURE_TYPE=http` environment // variable when deploying. The `FUNCTION_TARGET` environment variable should // match the first parameter. FunctionsFramework::http('helloHttp', 'helloHttp'); function helloHttp(ServerRequestInterface $request): string { $name = 'World'; $body = $request->getBody()->getContents(); if (!empty($body)) { $json = json_decode($body, true); if (json_last_error() != JSON_ERROR_NONE) { throw new RuntimeException(sprintf( 'Could not parse body: %s', json_last_error_msg() )); } $name = $json['name'] ?? $name; } $queryString = $request->getQueryParams(); $name = $queryString['name'] ?? $name; return sprintf('Hello, %s!', htmlspecialchars($name)); }
请勿启动后台 activity
后台活动是指在函数终止后发生的任何活动。
一旦函数返回或以其他方式发出完成信号(例如通过调用 Node.js 事件驱动型函数中的 callback
参数),函数调用就会完成。在正常终止后运行的任何代码都无法访问 CPU,因而无法继续进行。
另外,当在同一环境中执行后续调用时,您的后台活动将继续进行,从而干扰新的调用。这可能会导致难以诊断的意外行为和错误。在函数终止后访问网络通常会导致连接重置(错误代码为 ECONNRESET
)。
通常可以在各调用产生的日志中检测到后台活动,相关信息记录在指示调用已完成的行的后面。后台活动有时可能会深藏在代码中,尤其是在存在回调函数或定时器等异步操作的情况下。 请检查您的代码,以确保所有异步操作都会在函数终止之前完成。
务必删除临时文件
临时目录中的本地磁盘存储是内存中的文件系统。您写入的文件会占用函数可以使用的内存,并且有时会在多次调用过程中持续存在。如果不明确删除这些文件,最终可能会导致内存不足错误,并且随后需要进行冷启动。
如需查看个别函数所使用的内存,您可以访问Google Cloud 控制台,在函数列表中选择相应的函数,然后选择“内存用量”图。
请勿试图在临时目录之外执行写入操作,并务必使用独立于平台/操作系统的方法构建文件路径。
您可以在使用流水线处理大型文件时减少内存要求。例如,要在 Cloud Storage 上处理文件,您可以创建读取流,通过基于流的进程传递读取流,然后将输出流直接写入 Cloud Storage。
Cloud Functions 框架
部署函数时,Functions 框架会使用其当前版本自动添加为依赖项。为了确保在不同的环境中以一致的方式安装相同的依赖项,我们建议您将函数固定到特定版本的 Functions 框架。
为此,请在相关锁定文件中添加您的首选版本(例如,Node.js 版为 package-lock.json
,Python 版为 requirements.txt
)。
工具
本部分指导您如何使用工具来实现和测试 Cloud Run functions 函数并与之互动。
本地开发
函数部署需要一些时间,因此在本地测试函数的代码通常会更快。
错误报告
在使用异常处理的语言中,不要抛出未捕获的异常,因为这些异常会导致在未来调用函数时强制执行冷启动。如需了解如何适当地报告错误,请参阅报告错误指南。
请勿手动退出
手动退出可能会导致出现意外行为。请改用以下特定语言的惯用语:
Node.js
请勿使用 process.exit()
。HTTP 函数应使用 res.status(200).send(message)
发送响应,事件驱动函数在返回(隐式或显式)后即退出。
Python
请勿使用 sys.exit()
。HTTP 函数应明确返回字符串形式的响应,事件驱动函数返回值(隐式或显式)后即退出。
Go
请勿使用 os.Exit()
。HTTP 函数应明确返回字符串形式的响应,事件驱动函数返回值(隐式或显式)后即退出。
Java
请勿使用 System.exit()
。HTTP 函数应使用 response.getWriter().write(message)
发送响应,事件驱动函数在返回(隐式或显式)后即退出。
C#
请勿使用 System.Environment.Exit()
。HTTP 函数应使用 context.Response.WriteAsync(message)
发送响应,事件驱动函数在返回(隐式或显式)后即退出。
Ruby
请勿使用 exit()
或 abort()
。HTTP 函数应明确返回字符串形式的响应,事件驱动函数返回值(隐式或显式)后即退出。
PHP
请勿使用 exit()
或 die()
。HTTP 函数应明确返回字符串形式的响应,事件驱动函数返回值(隐式或显式)后即退出。
使用 Sendgrid 发送电子邮件
Cloud Run functions 函数不允许在端口 25 上建立出站连接,因此您无法建立到 SMTP 服务器的非安全连接。推荐使用 SendGrid 发送电子邮件。如需了解发送电子邮件的其他方式,请参阅适用于 Compute Engine 的从实例发送电子邮件教程。
性能
本部分介绍性能优化方面的最佳实践。
谨慎使用依赖项
由于函数是无状态的,执行环境通常是从头开始初始化(称为“冷启动”)。当发生冷启动时,系统会对函数的全局环境进行评估。
如果您的函数导入了模块,那么在冷启动期间,这些模块的加载时间会造成调用延迟加重。正确加载依赖项而不加载函数不使用的依赖项,即可缩短此延迟时间以及函数部署时间。
使用全局变量,以便在日后的调用中重复使用对象
系统无法保证能保留函数的状态,以用于将来的调用。不过,Cloud Run functions 函数经常会回收利用先前调用的执行环境。如果您在全局范围内声明一个变量,就可以在后续的调用中再次使用该变量的值,而不必重新计算。
通过这种方式,您可以缓存在每次调用函数时重建的成本较高的对象。将此类对象从函数体移到全局范围可能会显著提升性能。以下示例会为每个函数实例创建一个重量级对象(每个实例仅限一次),并提供给连接指定实例的所有函数调用共用:
Node.js
Python
Go
Java
C#
using Google.Cloud.Functions.Framework; using Microsoft.AspNetCore.Http; using System.Linq; using System.Threading.Tasks; namespace Scopes; public class Function : IHttpFunction { // Global (server-wide) scope. // This computation runs at server cold-start. // Warning: Class variables used in functions code must be thread-safe. private static readonly int GlobalVariable = HeavyComputation(); // Note that one instance of this class (Function) is created per invocation, // so calling HeavyComputation in the constructor would not have the same // benefit. public async Task HandleAsync(HttpContext context) { // Per-function-invocation scope. // This computation runs every time this function is called. int functionVariable = LightComputation(); await context.Response.WriteAsync( $"Global: {GlobalVariable}; function: {functionVariable}", context.RequestAborted); } private static int LightComputation() { int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; return numbers.Sum(); } private static int HeavyComputation() { int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; return numbers.Aggregate((current, next) => current * next); } }
Ruby
PHP
use Psr\Http\Message\ServerRequestInterface; function scopeDemo(ServerRequestInterface $request): string { // Heavy computations should be cached between invocations. // The PHP runtime does NOT preserve variables between invocations, so we // must write their values to a file or otherwise cache them. // (All writable directories in Cloud Functions are in-memory, so // file-based caching operations are typically fast.) // You can also use PSR-6 caching libraries for this task: // https://packagist.org/providers/psr/cache-implementation $cachePath = sys_get_temp_dir() . '/cached_value.txt'; $response = ''; if (file_exists($cachePath)) { // Read cached value from file, using file locking to prevent race // conditions between function executions. $response .= 'Reading cached value.' . PHP_EOL; $fh = fopen($cachePath, 'r'); flock($fh, LOCK_EX); $instanceVar = stream_get_contents($fh); flock($fh, LOCK_UN); } else { // Compute cached value + write to file, using file locking to prevent // race conditions between function executions. $response .= 'Cache empty, computing value.' . PHP_EOL; $instanceVar = _heavyComputation(); file_put_contents($cachePath, $instanceVar, LOCK_EX); } // Lighter computations can re-run on each function invocation. $functionVar = _lightComputation(); $response .= 'Per instance: ' . $instanceVar . PHP_EOL; $response .= 'Per function: ' . $functionVar . PHP_EOL; return $response; }
尤为重要的是,您应在全局范围内缓存网络连接、库引用和 API 客户端对象。如需查看相关示例,请参阅优化网络。
对全局变量进行延迟初始化
如果您在全局范围内初始化变量,系统始终会通过冷启动调用执行初始化代码,而这会增加函数的延迟时间。在某些情况下,如果被调用的服务在 try
/catch
块中未得到正确处理,则会导致这些服务间歇性超时。如果某些对象并非在所有代码路径中都会用到,可以考虑按需对它们进行延迟初始化:
Node.js
Python
Go
Java
C#
using Google.Cloud.Functions.Framework; using Microsoft.AspNetCore.Http; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace LazyFields; public class Function : IHttpFunction { // This computation runs at server cold-start. // Warning: Class variables used in functions code must be thread-safe. private static readonly int NonLazyGlobal = FileWideComputation(); // This variable is initialized at server cold-start, but the // computation is only performed when the function needs the result. private static readonly Lazy<int> LazyGlobal = new Lazy<int>( FunctionSpecificComputation, LazyThreadSafetyMode.ExecutionAndPublication); public async Task HandleAsync(HttpContext context) { // In a more complex function, there might be some paths that use LazyGlobal.Value, // and others that don't. The computation is only performed when necessary, and // only once per server. await context.Response.WriteAsync( $"Lazy global: {LazyGlobal.Value}; non-lazy global: {NonLazyGlobal}", context.RequestAborted); } private static int FunctionSpecificComputation() { int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; return numbers.Sum(); } private static int FileWideComputation() { int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; return numbers.Aggregate((current, next) => current * next); } }
Ruby
PHP
PHP 函数无法在请求之间保留变量。上文中的范围示例使用延迟加载在文件中缓存全局变量值。
如果您在单个文件中定义多个函数,并且不同的函数使用不同的变量,这种做法尤其有用。如果不使用延迟初始化,您可能会因为初始化后永远不会再用到的变量而浪费资源。
通过设置实例数下限减少冷启动次数
默认情况下,Cloud Run functions 函数会根据传入请求的数量扩缩实例数量。您可以更改这种默认行为,只需设置 Cloud Run functions 函数必须保持就绪状态以处理请求的实例数下限即可。设置实例数下限可以减少应用的冷启动次数。如果您的应用对延迟时间较为敏感,我们建议您设置实例数下限。
如需了解如何设置实例数下限,请参阅使用最小实例数。
其他资源
如需详细了解如何优化性能,请观看“Google Cloud 性能指南”视频 Cloud Run 函数冷启动时间。