提示和秘訣
本文說明設計、實作、測試及部署 Cloud Run 函式的最佳做法。
正確性
本節說明設計和實作 Cloud Run 函式的一般最佳做法。
編寫冪等函式
即使多次呼叫函式,這些函式也應該產生相同結果。如果先前的呼叫在程式碼執行到一半時失敗,您可以使用這項功能重試呼叫。詳情請參閱「重試事件驅動函式」。
確保 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)); }
不要啟動背景活動
背景活動是指函式終止後發生的任何活動。函式傳回或以其他方式發出完成信號 (例如在 Node.js 事件驅動函式中呼叫 callback 引數) 後,函式呼叫就會完成。安全終止後執行的任何程式碼都無法存取 CPU,因此不會有任何進展。
此外,如果後續在相同環境中執行叫用作業,背景活動就會繼續執行,干擾新的叫用作業。這可能會導致發生難以診斷的非預期行為和錯誤。函式終止後存取網路通常會導致連線重設 (ECONNRESET 錯誤代碼)。
您通常可以在個別調用的記錄中偵測到背景活動,方法是找出調用完成後記錄的任何內容。有時,背景活動可能會埋藏在程式碼深處,特別是在有回呼或計時器等非同步作業時。請檢查程式碼,確認在終止函式前,所有非同步作業皆已完成。
一律刪除暫存檔案
暫存目錄中的本機磁碟儲存空間是一個記憶體內部檔案系統。您編寫的檔案會耗用用於函式的記憶體,而且有時會在叫用間持續存在。不明確刪除這些檔案最終可能會導致發生記憶體不足的錯誤,並造成後續冷啟動。
如要查看個別函式使用的記憶體,請在Google Cloud 控制台的函式清單中選取函式,然後選擇「記憶體用量」圖。
請勿嘗試寫入暫存目錄以外的位置,並務必使用與平台/作業系統無關的方法建構檔案路徑。
使用管道處理較大的檔案時,可以減少記憶體需求。 舉例來說,您可以建立讀取串流、透過以串流為基礎的程序傳遞串流,然後將輸出串流直接寫入 Cloud Storage,藉此處理 Cloud Storage 中的檔案。
Functions Framework
部署函式時,系統會自動將 Functions Framework 新增為依附元件,並使用目前版本。為確保不同環境安裝的依附元件相同,建議您將函式固定在特定版本的函式架構。
如要這麼做,請在相關鎖定檔案中加入偏好的版本 (例如 Node.js 的 package-lock.json 或 Python 的 requirements.txt)。
工具
本節提供指南,說明如何使用工具實作、測試及與 Cloud Run 函式互動。
本機開發
函式部署需要一些時間,因此在本機測試函式程式碼通常會比較快。
錯誤報告
在採用例外狀況處理的語言中,請勿擲回未捕捉到的例外狀況, 因為這會導致日後叫用時強制冷啟動。 請參閱錯誤報告指南,瞭解如何正確回報錯誤的相關資訊。
請勿手動結束
手動結束可能會導致非預期的行為。請改用下列語言專屬的慣用語:
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 Performance Atlas」影片:Cloud Run 函式冷啟動時間。