提示&技巧

本文檔介紹了設計、實作、測試和部署 Cloud Functions 的最佳實務。

正確性

本節介紹設計和實作 Cloud Functions 的一般最佳實務。

編寫冪等函數

即使多次呼叫您的函數,也應該產生相同的結果。如果前一個呼叫在程式碼中途失敗,您可以重試呼叫。有關詳細信息,請參閱重試事件驅動函數

不啟動後台活動

後台活動是指函數終止後發生的任何事情。一旦函數返回或以其他方式發出完成訊號(例如透過呼叫 Node.js 事件驅動函數中的callback參數),函數呼叫就會完成。優雅終止後運行的任何程式碼都無法存取 CPU,也不會取得任何進展。

此外,當在同一環境中執行後續呼叫時,您的背景活動將會恢復,從而乾擾新的呼叫。這可能會導致難以診斷的意外行為和錯誤。函數終止後存取網路通常會導致連線重置( ECONNRESET錯誤代碼)。

透過尋找呼叫完成行之後記錄的任何內容,通常可以在各個呼叫的日誌中偵測後台活動。後台活動有時可能會埋藏在程式碼中更深處,尤其是當存在回呼或計時器等非同步操作時。檢查您的程式碼以確保在終止函數之前完成所有非同步操作。

始終刪除暫存文件

暫存目錄中的本機磁碟儲存是記憶體中的檔案系統。您編寫的檔案會消耗函數可用的內存,有時會在呼叫之間持續存在。未能明確刪除這些檔案可能最終會導致記憶體不足錯誤和隨後的冷啟動。

您可以透過在 GCP Console 的函數清單中選擇單一函數並選擇記憶體使用圖來查看函數使用的記憶體。

不要嘗試在臨時目錄之外進行寫入,並確保使用與平台/作業系統無關的方法來建構檔案路徑。

使用管道處理較大檔案時,您可以減少記憶體需求。例如,您可以透過建立讀取流、將其傳遞到基於流的進程並將輸出流直接寫入 Cloud Storage 來處理 Cloud Storage 上的檔案。

功能框架

當您部署函數時,函數框架會使用其目前版本自動新增為相依性。為了確保在不同環境中一致安裝相同的依賴項,我們建議您將函數固定在特定版本的 Functions Framework。

為此,請在相關鎖定檔案中包含您的首選版本(例如,Node.js 的package-lock.json或 Python 的requirements.txt )。

工具

本部分提供如何使用工具來實作、測試 Cloud Functions 以及與 Cloud Functions 互動的指南。

當地發展

函數部署需要一些時間,因此在本地測試函數程式碼通常會更快。

Firebase 開發人員可以使用Firebase CLI Cloud Functions 模擬器

使用Sendgrid發送電子郵件

Cloud Functions 不允許在連接埠 25 上進行出站連接,因此您無法與 SMTP 伺服器建立非安全連接。發送電子郵件的建議方法是使用SendGrid 。您可以在從 Google Compute Engine 實例發送電子郵件教學中找到用於發送電子郵件的其他選項。

表現

本節介紹優化效能的最佳實務。

明智地使用依賴項

由於函數是無狀態的,因此執行環境通常是從頭開始初始化的(在所謂的冷啟動期間)。當發生冷啟動時,將評估函數的全域上下文。

如果您的函數導入模組,這些模組的載入時間可能會增加冷啟動期間的呼叫延遲。您可以透過正確載入依賴項而不載入函數不使用的依賴項來減少此延遲以及部署函數所需的時間。

使用全域變數在將來的呼叫中重複使用對象

無法保證雲端函數的狀態將被保留以供將來呼叫。但是,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 用戶端物件尤其重要。有關範例,請參閱優化網路

全域變數的延遲初始化

如果在全域範圍內初始化變量,初始化程式碼將始終透過冷啟動呼叫執行,從而增加函數的延遲。在某些情況下,如果在try / catch區塊中未正確處理這些服務,這會導致所呼叫的服務間歇性逾時。如果某些物件未在所有程式碼路徑中使用,請考慮按需延遲初始化它們:

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物件的值集。

如果您在單一文件中定義多個函數,並且不同的函數使用不同的變量,這一點尤其重要。除非使用延遲初始化,否則可能會在已初始化但從未使用過的變數上浪費資源。

透過設定最小實例數來減少冷啟動

預設情況下,Cloud Functions 根據傳入請求的數量縮放實例數量。您可以透過設定 Cloud Functions 必須保持準備好服務請求的最小實例數來變更此預設行為。設定最小實例數可以減少應用程式的冷啟動。如果您的應用程式對延遲敏感,我們建議設定最小實例數。

有關這些運行時選項的更多信息,請參閱控制縮放行為

其他資源

在「Google Cloud Performance Atlas」影片雲功能冷啟動時間中了解更多有關最佳化效能的資訊。