提示和秘訣
本文件說明設計、實作、測試及部署 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)); }
請勿啟動背景活動
背景活動是指在函式終止後發生的任何活動。函式傳回或以其他方式發出完成信號 (例如在 Node.js 事件導向函式中呼叫 callback 引數) 後,函式呼叫隨即結束。在安全終止後執行的任何程式碼都無法存取 CPU,且不會有任何進展。
此外,若相同環境執行後續叫用,背景活動會恢復並干擾新的叫用,可能導致難以診斷的非預期行為和錯誤。若在函式終止後存取網路,通常會導致連線重設 (ECONNRESET 錯誤代碼)。
要偵測背景活動,您可以檢查個別叫用的記錄檔,如果在標示叫用結束的記錄行之後發現任何內容,通常就是這類活動的跡象。背景活動有時在程式碼中埋藏得很深,尤其是在有回呼或計時器這類非同步作業時。請務必檢查程式碼,確認所有非同步作業皆完成後再終止函式。
一律刪除暫存檔案
暫存目錄中的本機磁碟儲存空間是一個記憶體內部檔案系統。您編寫的檔案會耗用用於函式的記憶體,而且有時會在叫用間持續存在。若未明確刪除這些檔案,最終可能導致記憶體不足的錯誤,並造成後續的冷啟動。
您可以在Google Cloud 控制台的函式清單中選取函式,並選擇「Memory usage」(記憶體用量) 圖,查看個別函式使用的記憶體。
請勿嘗試寫入暫時目錄以外的位置,並務必使用與平台/作業系統無關的方法建構檔案路徑。
處理大型檔案時,可採用管線化方法來降低記憶體需求,例如建立讀取串流、透過串流程序傳遞檔案,並直接將輸出串流寫回 Cloud Storage,並於其中處理檔案。
Functions Framework
部署函式時,系統會自動新增目前版本的 Functions Framework 作為依附元件。為確保不同環境一致安裝相同的依附元件,建議將函式固定於特定版本的 Functions Framework。
具體做法是將偏好的版本寫入相關的鎖定檔案中 (例如 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 Performance Atlas」系列影片:「Cloud Run functions 冷啟動時間」。