本文說明設計、實作、測試及部署 Cloud Functions 的最佳做法。
正確性
本節說明設計和實作 Cloud Functions 的一般最佳做法。
編寫冪等函式
即使多次呼叫函式,這些函式也應該產生相同結果。這樣一來,如果之前叫用程式碼的作業中途失敗,您便可以重試叫用。詳情請參閱「重試事件導向函式」。
請勿啟動背景活動
背景活動是指在函式終止後發生的任何活動。函式傳回或以其他方式發出完成信號 (例如在 Node.js 事件導向函式中呼叫 callback 引數) 後,函式呼叫隨即結束。在安全終止後執行的任何程式碼都無法存取 CPU,且不會有任何進展。
此外,若相同環境執行後續叫用,背景活動會恢復並干擾新的叫用,可能導致難以診斷的非預期行為和錯誤。若在函式終止後存取網路,通常會導致連線重設 (ECONNRESET 錯誤代碼)。
要偵測背景活動,您可以檢查個別叫用的記錄檔,如果在標示叫用結束的記錄行之後發現任何內容,通常就是這類活動的跡象。背景活動有時在程式碼中埋藏得很深,尤其是在有回呼或計時器這類非同步作業時。請務必檢查程式碼,確認所有非同步作業皆完成後再終止函式。
一律刪除暫存檔案
暫存目錄中的本機磁碟儲存空間是一個記憶體內部檔案系統。您編寫的檔案會耗用用於函式的記憶體,而且有時會在叫用間持續存在。若未明確刪除這些檔案,最終可能導致記憶體不足的錯誤,並造成後續的冷啟動。
您可以在 Google Cloud 控制台的函式清單中選取函式,並選擇「Memory usage」(記憶體用量) 圖,查看個別函式使用的記憶體。
如需長期儲存空間,請考慮使用Cloud Run磁碟區掛接搭配 Cloud Storage 或 NFS 磁碟區。
處理大型檔案時,可採用管線化方法來降低記憶體需求,例如建立讀取串流、透過串流程序傳遞檔案,並直接將輸出串流寫回 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() 勾點內的程式碼才會執行,部署程序本身不會執行。
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 冷啟動時間。