本文說明設計、實作、測試及部署 Cloud Functions 的最佳做法。
正確性
本節說明設計和實作 Cloud Functions 的一般最佳做法。
編寫冪等函式
即使多次呼叫函式,這些函式也應該產生相同結果。如果先前的呼叫在程式碼執行到一半時失敗,您可以使用這項功能重試呼叫。詳情請參閱重試事件驅動函式。
請勿啟動背景活動
背景活動是指函式終止後發生的任何活動。函式傳回或以其他方式發出完成信號 (例如在 Node.js 事件驅動函式中呼叫 callback
引數) 後,函式呼叫就會完成。安全終止後執行的任何程式碼都無法存取 CPU,也不會取得任何進展。
此外,如果後續的叫用作業在相同環境中執行,背景活動就會繼續執行,干擾新的叫用作業。這可能會導致發生難以診斷的非預期行為和錯誤。函式終止後存取網路通常會導致連線重設 (ECONNRESET
錯誤代碼)。
您通常可以在個別調用的記錄中偵測到背景活動,方法是找出調用完成後記錄的任何內容。有時,背景活動可能會埋藏在程式碼深處,特別是在有回呼或計時器等非同步作業時。請檢查程式碼,確認在終止函式前,所有非同步作業皆已完成。
一律刪除暫存檔案
暫存目錄中的本機磁碟儲存空間是一個記憶體內部檔案系統。您編寫的檔案會耗用用於函式的記憶體,而且有時會在叫用間持續存在。不明確刪除這些檔案最終可能會導致發生記憶體不足的錯誤,並造成後續冷啟動。
如要查看個別函式使用的記憶體,請在 Google Cloud 控制台的函式清單中選取函式,然後選擇「記憶體用量」圖表。
如需長期儲存空間,請考慮使用Cloud Run磁碟區掛接搭配 Cloud Storage 或 NFS 磁碟區。
使用管道處理較大的檔案時,可以減少記憶體需求。 舉例來說,您可以建立讀取串流、透過以串流為基礎的程序傳遞串流,然後將輸出串流直接寫入 Cloud Storage,藉此處理 Cloud Storage 中的檔案。
Functions Framework
為確保在各個環境中安裝的依附元件一致,建議您在套件管理員中加入 Functions Framework 程式庫,並將依附元件固定在特定版本的 Functions Framework。
如要這麼做,請在相關鎖定檔案中加入偏好的版本 (例如 Node.js 的 package-lock.json
或 Python 的 requirements.txt
)。
如果未明確將 Functions Framework 列為依附元件,建構程序會自動新增最新可用版本。
工具
本節提供準則,說明如何使用工具實作、測試及與 Cloud Functions 互動。
本機開發
函式部署需要一些時間,因此在本機測試函式程式碼通常會比較快。
Firebase 開發人員可以使用 Firebase CLI Cloud Functions 模擬器。避免在初始化期間發生部署作業逾時
如果函式部署作業因逾時錯誤而失敗,可能是因為函式的全域範圍程式碼在部署程序中執行時間過長。
Firebase CLI 在部署期間探索函式時,有預設逾時時間。如果函式原始碼中的初始化邏輯 (載入模組、發出網路呼叫等) 超過這個逾時時間,部署作業可能會失敗。
如要避免逾時,請採取下列其中一項策略:
(建議) 使用 onInit()
延後初始化
使用 onInit()
鉤子,避免在部署期間執行初始化程式碼。只有在函式部署至 Cloud Run functions 時,onInit()
hook 內的程式碼才會執行,部署程序本身不會執行。
Node.js
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}`); });
Python
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 的「從執行個體傳送電子郵件」教學課程。
Performance
本節說明最佳化效能的最佳做法。
避免低並行
由於冷啟動成本高昂,因此在尖峰期間重複使用最近啟動的執行個體,是處理負載的絕佳最佳化方式。限制並行會限制現有執行個體的運用方式,因此會導致更多冷啟動。
提高並行程度 有助於延遲每個執行個體的多個要求,更容易處理負載尖峰。謹慎使用依附元件
由於函式是無狀態的,因此執行環境通常是從頭開始初始化 (這期間就是所謂的「冷啟動」)。發生冷啟動時,會評估函式的全域背景資訊。
如果函式匯入模組,在冷啟動期間,這些模組的載入時間會增加叫用的延遲時間。您可以正確載入依附元件,而不載入函式不使用的依附元件,來減少這一延遲時間以及部署函式需要的時間。
使用全域變數在未來叫用中重複使用物件
無法保證函式的狀態會保留供日後呼叫。不過,Cloud Functions 通常會重複使用先前叫用的執行環境。如果您在全域範圍中宣告變數,後續呼叫時即可重複使用該變數的值,不必重新計算。
這樣一來,您便可以快取在每次叫用函式時重新建立起來費用可能比較高的的物件。將這類物件從函式主體移至全域範圍可能會使效能大幅提升。下列範例只會為每個函式執行個體建立一個重型物件,並在到達指定執行個體的所有函式叫用中共用這個物件:
Node.js
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}`); });
Python
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 程式庫的範例
Node.js 與 Firestore 是非同步 Node.js 程式庫的範例。如要運用 min_instances,下列程式碼會在載入時完成載入和初始化作業,並封鎖模組載入作業。
使用 TLA,表示需要 ES6,方法是為 node.js 程式碼使用 .mjs
擴充功能,或將 type: module
新增至 package.json 檔案。
{ "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. });
全域初始化的範例
Node.js
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'); });
Python
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 Performance Atlas」影片Cloud Functions「Cold Boot Time」。