本文說明設計、實作、測試及部署 Cloud Functions 的最佳做法。
正確性
本節說明設計和實作 Cloud Functions 的一般最佳做法。
編寫冪等函式
即使多次呼叫函式,這些函式也應該產生相同結果。這樣一來,如果先前的叫用在程式碼中失敗,您就可以重試叫用。詳情請參閱「重試事件驅動函式」。
請勿啟動背景活動
背景活動是指函式終止後發生的任何活動。函式傳回或以其他方式表示完成後,函式叫用就會結束,例如在 Node.js 事件驅動函式中呼叫 callback
引數。在安全終止後執行的任何程式碼都無法存取 CPU,也不會取得任何進展。
此外,如果後續叫用在相同環境中執行,背景活動就會恢復,並干擾新的叫用。這可能會導致發生難以診斷的非預期行為和錯誤。在函式終止後存取網路通常會導致連線重設 (ECONNRESET
錯誤代碼)。
您通常可以在個別叫用作業的記錄中偵測到背景活動,方法是尋找在表示叫用作業已完成的資料行後面記錄的任何內容。背景活動有時可能會埋藏在程式碼的較深層位置,尤其是在存在回呼或計時器等非同步作業時。請審查您的程式碼,確認在您終止函式之前,所有非同步作業皆已完成。
一律刪除暫存檔案
暫存目錄中的本機磁碟儲存空間是一個記憶體內部檔案系統。您編寫的檔案會耗用用於函式的記憶體,而且有時會在叫用間持續存在。不明確刪除這些檔案最終可能會導致發生記憶體不足的錯誤,並造成後續冷啟動。
如要查看個別函式使用的記憶體,請在 Google Cloud 控制台的函式清單中選取該函式,然後選擇「Memory usage」圖表。
如果您需要存取長期儲存空間,建議使用 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 Emulator。避免在初始化期間發生部署逾時
如果函式部署作業因逾時錯誤而失敗,表示函式的全域範圍程式碼在部署程序中執行的時間過長。
Firebase CLI 在部署期間會針對探索函式設有預設逾時時間。如果函式原始碼中的初始化邏輯 (載入模組、發出網路呼叫等) 超過這個逾時時間,部署作業可能會失敗。
如要避免逾時,請採用下列其中一項策略:
(建議) 使用 onInit()
延後初始化
請使用 onInit()
鉤子,避免在部署期間執行初始化程式碼。只有在函式部署至 Cloud Run 函式時,才會執行 onInit()
鉤子的程式碼,不會在部署程序本身期間執行。
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}`); });
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 通常會回收先前叫用的執行環境。如果您在全域範圍中宣告變數,則其值可在後續叫用中重複使用,而無需重新計算。
這樣一來,您便可以快取在每次叫用函式時重新建立起來費用可能比較高的的物件。將這類物件從函式主體移至全域範圍可能會使效能大幅提升。下列範例只會為每個函式執行個體建立一個重型物件,並在到達指定執行個體的所有函式叫用中共用這個物件:
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}`); });
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" } }
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. });
全域初始化的範例
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'); });
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 冷啟動時間。