本文說明設計、實作、測試及部署 Cloud Run 函式的最佳做法。
正確性
本節說明設計和實作 Cloud Run 函式的一般最佳做法。
編寫冪等函式
即使多次呼叫函式,這些函式也應該產生相同結果。如果先前的呼叫在程式碼執行到一半時失敗,您可以使用這項功能重試呼叫。詳情請參閱重試事件驅動函式。
確保 HTTP 函式會傳送 HTTP 回應
如果函式是透過 HTTP 觸發,請記得傳送 HTTP 回應,如下所示。否則函式可能會執行到逾時為止。如果發生這種情況,系統會收取整個逾時期間的費用。逾時也可能導致無法預測的行為,或在後續呼叫時發生冷啟動,進而導致無法預測的行為或額外延遲。
Node.js
Python
Go
Java
C#
Ruby
PHP
請勿啟動背景活動
背景活動是指函式終止後發生的任何活動。函式傳回或以其他方式發出完成信號 (例如在 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 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 等第三方服務傳送電子郵件。如要瞭解其他傳送電子郵件的選項,請參閱 Google Compute Engine 的「從執行個體傳送電子郵件」教學課程。
成效
本節說明最佳化效能的最佳做法。
避免低並行
由於冷啟動成本高昂,因此在尖峰期間重複使用最近啟動的執行個體,是處理負載的絕佳最佳化方式。限制並行會限制現有執行個體的運用方式,因此會導致更多冷啟動。
提高並行程度 有助於延遲每個執行個體的多個要求,更容易處理負載尖峰。謹慎使用依附元件
由於函式是無狀態的,因此執行環境通常是從頭開始初始化 (這期間就是所謂的「冷啟動」)。發生冷啟動時,會評估函式的全域背景資訊。
如果函式匯入模組,在冷啟動期間,這些模組的載入時間會增加叫用的延遲時間。您可以正確載入依附元件,而不載入函式不使用的依附元件,來減少這一延遲時間以及部署函式需要的時間。
使用全域變數在未來叫用中重複使用物件
無法保證 Cloud Run 函式的狀態會保留供日後叫用。不過,Cloud Run functions 通常會回收先前叫用的執行環境。如果您在全域範圍中宣告變數,後續呼叫時即可重複使用該變數的值,不必重新計算。
這樣一來,您便可以快取在每次叫用函式時重新建立起來費用可能比較高的的物件。將這類物件從函式主體移至全域範圍可能會使效能大幅提升。下列範例只會為每個函式執行個體建立一個重型物件,並在到達指定執行個體的所有函式叫用中共用這個物件:
Node.js
Python
Go
Java
C#
Ruby
PHP
請務必在全域範圍內快取網路連線、程式庫參照和 API 用戶端物件。 如需範例,請參閱網路最佳做法。
設定執行個體數量下限,減少冷啟動情形
根據預設,Cloud Run functions 會依據傳入要求的數量,調整執行個體數量。如要變更這項預設行為,請設定 Cloud Run 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. });
全域初始化的範例
如果您在單一檔案中定義多個函式,且不同函式使用不同變數,這個方法尤為重要。除非您使用延遲初始化,否則會浪費已初始化但從未使用的變數資源。
其他資源
如要進一步瞭解如何提升效能,請觀看「Google Cloud Performance Atlas」影片:Cloud Run 函式冷啟動時間。