一般開發提示

本指南提供設計、實作、測試及部署 Cloud Run 服務的最佳做法。如要進一步瞭解秘訣,請參閱遷移現有的服務

編寫有效的服務

本節說明設計和實作 Cloud Run 服務的一般最佳做法。

背景活動

背景活動是指在 HTTP 回應送出後發生的任何活動。如要判斷服務中是否有無法立即得知的背景活動,請查看記錄,尋找 HTTP 要求項目後記錄的任何活動。

設定以執行個體為準的計費方式,即可使用背景活動

如要在 Cloud Run 服務中支援背景活動,請將 Cloud Run 服務設為以執行個體為準的計費方式,這樣您就能在要求以外執行背景活動,並繼續存取 CPU。

如果採用以要求為準的計費方式,請避免背景活動

如需將服務設為以要求為準的計費方式,Cloud Run 服務處理完要求後,系統會停用或大幅限制執行個體的 CPU 存取權。如果您使用這類計費方式,請勿啟動在要求處理常式範圍外執行的背景執行緒或常式。

請審查您的程式碼,確認在您傳送回應之前,所有非同步作業皆已完成。

啟用以要求為準的計費方式後,執行背景執行緒可能會導致非預期的行為,因為任何傳送到同一容器執行個體的後續要求都會恢復所有已暫停的背景活動。

刪除暫存檔

在 Cloud Run 環境中,磁碟儲存空間是記憶體內檔案系統。寫入磁碟的檔案會耗用用於服務的記憶體,而且會在叫用間持續存在。未刪除這些檔案最終可能會導致發生記憶體不足的錯誤,並造成後續容器啟動時間緩慢。

回報錯誤

請處理所有例外狀況,別讓您的服務因錯誤而當機。當機會導致容器啟動緩慢,此時會針對更換執行個體,將流量排入佇列。

請參閱錯誤報告指南,瞭解如何正確回報錯誤的相關資訊。

發揮最大效能

本節說明最佳化效能的最佳做法。

快速啟動容器

由於執行個體會視需要調度資源,因此啟動時間會影響服務延遲。Cloud Run 會將執行個體啟動和要求處理程序分離,因此在某些情況下,要求必須等待新執行個體啟動,才能進行處理。服務從零開始擴充時,通常會發生這種情況。

啟動例行程序包含:

  • 下載容器映像檔 (使用 Cloud Run 的容器映像檔串流技術)
  • 執行 entrypoint 指令來啟動容器。
  • 等待容器開始監聽設定的通訊埠

最佳化容器啟動速度可將要求處理延遲時間縮至最短。

使用啟動時 CPU 效能強化功能,縮短啟動延遲時間

您可以啟用啟動時 CPU 效能強化,在執行個體啟動期間暫時增加 CPU 分配量,以縮短啟動延遲時間。

設定執行個體數量下限來縮短容器啟動時間

您可以設定執行個體數量下限並行程度,盡量縮短容器啟動時間。舉例來說,如果將執行個體下限設為 1,表示服務已準備好接收最多與服務設定的並行要求數量相同的要求,而不需啟動新的執行個體。

請注意,等待執行個體啟動的要求會保留在佇列中,如下所示:

要求最多會等待這項服務的容器執行個體平均啟動時間的 3.5 倍,或 10 秒 (以較長者為準)。

謹慎使用依附元件

如果您使用動態語言搭配相依的程式庫,例如匯入使用 Node.js 的模組,這些模組的載入時間會增加啟動延遲時間。

您可以透過以下方式縮短啟動延遲時間:

  • 儘可能減少相依元件的數量和大小以建置精簡的服務。
  • 只有在必要時才載入不常用的程式碼 (如果您使用的語言支援)。
  • 使用程式碼載入的最佳化,例如 PHP 的 composer 自動載入器最佳化

使用全域變數

在 Cloud Run 中,您無法假設服務會保留要求之間的狀態。不過,Cloud Run 確實可重複使用個別執行個體來處理不間斷的流量,所以您可以在全域範圍內宣告變數,來允許後續叫用可重複使用其值。任何個別要求是否會因重複使用而獲益,無法事先得知。

如果每次收到服務要求時重新建立物件的作業成本高昂,您也可以快取記憶體內的物件。將這個作業從要求邏輯移至全域範圍會帶來更好的效能。

Node.js

const functions = require('@google-cloud/functions-framework');

// TODO(developer): Define your own computations
const {lightComputation, heavyComputation} = require('./computations');

// Global (instance-wide) scope
// This computation runs once (at instance cold-start)
const instanceVar = heavyComputation();

/**
 * HTTP function that declares a variable.
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
functions.http('scopeDemo', (req, res) => {
  // Per-function scope
  // This computation runs every time this function is called
  const functionVar = lightComputation();

  res.send(`Per instance: ${instanceVar}, per function: ${functionVar}`);
});

Python

import time

import functions_framework


# 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()


@functions_framework.http
def scope_demo(request):
    """
    HTTP Cloud Function that declares a variable.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """

    # Per-function scope
    # This computation runs every time this function is called
    function_var = light_computation()
    return f"Instance: {instance_var}; function: {function_var}"

Go


// h is in the global (instance-wide) scope.
var h string

// init runs during package initialization. So, this will only run during an
// an instance's cold start.
func init() {
	h = heavyComputation()
	functions.HTTP("ScopeDemo", ScopeDemo)
}

// ScopeDemo is an example of using globally and locally
// scoped variables in a function.
func ScopeDemo(w http.ResponseWriter, r *http.Request) {
	l := lightComputation()
	fmt.Fprintf(w, "Global: %q, Local: %q", h, l)
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;

public class Scopes implements HttpFunction {
  // Global (instance-wide) scope
  // This computation runs at instance cold-start.
  // Warning: Class variables used in functions code must be thread-safe.
  private static final int INSTANCE_VAR = heavyComputation();

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    // Per-function scope
    // This computation runs every time this function is called
    int functionVar = lightComputation();

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Instance: %s; function: %s", INSTANCE_VAR, functionVar);
  }

  private static int lightComputation() {
    int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    return Arrays.stream(numbers).sum();
  }

  private static int heavyComputation() {
    int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    return Arrays.stream(numbers).reduce((t, x) -> t * x).getAsInt();
  }
}

對全域變數執行延遲初始化

系統在啟動時一律會初始化全域變數,這會增加容器啟動時間。對不常用的物件使用延遲初始化,可推遲時間成本並縮短容器啟動時間。

延遲初始化的缺點之一,是新執行個體的首批要求延遲時間會增加。如果您部署的服務新修訂版本正在處理大量要求,可能會導致過度擴充,並捨棄要求。

Node.js

const functions = require('@google-cloud/functions-framework');

// Always initialized (at cold-start)
const nonLazyGlobal = fileWideComputation();

// Declared at cold-start, but only initialized if/when the function executes
let lazyGlobal;

/**
 * HTTP function that uses lazy-initialized globals
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
functions.http('lazyGlobals', (req, res) => {
  // This value is initialized only if (and when) the function is called
  lazyGlobal = lazyGlobal || functionSpecificComputation();

  res.send(`Lazy global: ${lazyGlobal}, non-lazy global: ${nonLazyGlobal}`);
});

Python

import functions_framework

# 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


@functions_framework.http
def lazy_globals(request):
    """
    HTTP Cloud Function that uses lazily-initialized globals.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """
    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 f"Lazy: {lazy_global}, non-lazy: {non_lazy_global}."

Go


// Package tips contains tips for writing Cloud Functions in Go.
package tips

import (
	"context"
	"log"
	"net/http"
	"sync"

	"cloud.google.com/go/storage"
	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)

// client is lazily initialized by LazyGlobal.
var client *storage.Client
var clientOnce sync.Once

func init() {
	functions.HTTP("LazyGlobal", LazyGlobal)
}

// LazyGlobal is an example of lazily initializing a Google Cloud Storage client.
func LazyGlobal(w http.ResponseWriter, r *http.Request) {
	// You may wish to add different checks to see if the client is needed for
	// this request.
	clientOnce.Do(func() {
		// Pre-declare an err variable to avoid shadowing client.
		var err error
		client, err = storage.NewClient(context.Background())
		if err != nil {
			http.Error(w, "Internal error", http.StatusInternalServerError)
			log.Printf("storage.NewClient: %v", err)
			return
		}
	})
	// Use client.
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;

public class LazyFields implements HttpFunction {
  // Always initialized (at cold-start)
  // Warning: Class variables used in Servlet classes must be thread-safe,
  // or else might introduce race conditions in your code.
  private static final int NON_LAZY_GLOBAL = fileWideComputation();

  // Declared at cold-start, but only initialized if/when the function executes
  // Uses the "initialization-on-demand holder" idiom
  // More information: https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
  private static class LazyGlobalHolder {
    // Making the default constructor private prohibits instantiation of this class
    private LazyGlobalHolder() {}

    // This value is initialized only if (and when) the getLazyGlobal() function below is called
    private static final Integer INSTANCE = functionSpecificComputation();

    private static Integer getInstance() {
      return LazyGlobalHolder.INSTANCE;
    }
  }

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    Integer lazyGlobal = LazyGlobalHolder.getInstance();

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Lazy global: %s; non-lazy global: %s%n", lazyGlobal, NON_LAZY_GLOBAL);
  }

  private static int functionSpecificComputation() {
    int[] numbers = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
    return Arrays.stream(numbers).sum();
  }

  private static int fileWideComputation() {
    int[] numbers = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
    return Arrays.stream(numbers).reduce((t, x) -> t * x).getAsInt();
  }
}

使用其他執行環境

使用其他執行環境可能會加快啟動速度。

最佳化並行

Cloud Run 執行個體最多可同時處理多個要求,也就是「並行」,上限為可設定的並行要求數量上限

Cloud Run 會自動調整並行數,最高可達設定上限。

預設的並行上限為 80,適用於許多容器映像檔。不過,請注意以下事項:

  • 如果容器無法處理多個並行要求,請調低此值。
  • 如果容器可以處理大量要求,請提高此值。

調整服務的並行

每個執行個體可處理的並行要求數量受限於技術堆疊和共用資源的使用,例如變數和資料庫連線。

如何最佳化您的服務來達到最大的穩定並行:

  1. 最佳化您的服務效能。
  2. 在任何程式碼層級並行設定中,設定您預期的並行支援層級。並非所有技術堆疊都需要這樣設定。
  3. 部署您的服務。
  4. 為您的服務設定等於或小於任何程式碼層級設定的 Cloud Run 並行。如果沒有程式碼層級設定,請使用您預期的並行。
  5. 使用支援可設定並行的負載測試工具。您必須確認您的服務在預期的負載和並行處理下是否仍可維持穩定。
  6. 如果服務效能不佳,請前往步驟 1 來改善服務,或前往步驟 2 來減少並行。如果服務執行效能很好,請回到步驟 2 增加並行。

繼續反覆操作,直到您找到最大的穩定並行。

使記憶體與並行相配

您的服務在處理每一個要求時,都需要使用額外少量的記憶體。 所以,當您上下調整並行時,請務必一併調整記憶體。

避免可變動的全域狀態

如果您想在並行情況下利用可變動的全域狀態,請在您的程式碼中採取額外的步驟來確保安全完成。將全域變數限制為一次性初始化,來儘可能避免爭用狀況,然後按照效能底下的說明重複使用。

如果您在一次處理多個要求的服務中使用可變動的全域變數,請務必使用鎖定或互斥鎖來避免發生競爭狀況。

總處理量與延遲時間和成本之間的取捨

調整並行要求數量上限設定,有助於在服務的輸送量、延遲時間和成本之間取得平衡。

一般來說,如果將並行要求上限調低,每個執行個體的延遲時間和輸送量就會降低。如果並行要求數量上限較低,每個執行個體內爭奪資源的要求就會較少,每個要求都能獲得較佳效能。但由於每個執行個體一次可處理的要求較少,因此每個執行個體的輸送量較低,服務需要更多執行個體才能處理相同的流量。

反之,如果將並行要求數量上限調高,通常會導致延遲時間變長,但每個執行個體的總處理量會增加。要求可能需要等待存取執行個體內的 CPU、GPU 和記憶體頻寬等資源,這會導致延遲時間增加。但每個執行個體一次可處理更多要求,因此服務整體上需要較少的執行個體,即可處理相同流量。

費用注意事項

Cloud Run 的定價是以執行個體時間計算。如果設定以執行個體為依據的計費模式,執行個體時間就是每個執行個體的總生命週期。如果您設定以要求為依據的計費方式,執行個體時間是指每個執行個體處理至少一項要求所花費的時間。

並行要求數量上限對帳單費用的影響,取決於您的流量模式。降低並行要求上限可能會導致帳單金額降低,前提是 設定較低的上限會導致

  • 減少延遲
  • 執行個體更快完成工作
  • 即使需要更多執行個體,執行個體也會更快關閉

但反過來說,如果延遲時間縮短,導致每個執行個體的執行時間減少,但執行個體數量增加,那麼降低並行要求上限反而會增加費用。

如要盡量減少帳單費用,最好的方式是使用不同的並行要求上限設定進行負載測試,找出可產生最低可計費執行個體時間的設定,如容器/可計費執行個體時間監控指標所示。

容器安全性

許多一般用途的軟體安全性做法都適用於經過容器化的服務。 有些做法專屬於容器或吻合容器的原理和架構。

如要改善容器安全性:

  • 使用主動維護的安全基本映像檔,如 Google 基本映像檔或 Docker Hub 的官方映像檔

  • 定期重新建構容器映像檔並重新部署您的服務,以將安全性更新套用到您的服務。

  • 在容器中僅包含執行服務所必要的項目。額外的程式碼、套件和工具都可能是安全漏洞。如要瞭解相關的效能影響,請參閱上文說明。

  • 實作確定性建構程序,其包含特定軟體和程式庫版本。這可以防止將未經驗證的程式碼納入容器中。

  • 使用 Dockerfile USER 陳述式,將容器設為以 root 以外的用者身分執行。某些容器映像檔可能已有設定的特定使用者。

  • 使用自訂機構政策,禁止使用預覽功能。

自動執行安全性掃描

啟用安全漏洞掃描,對儲存在 Artifact Registry 的容器映像檔進行安全性掃描。

建構最小的容器映像檔

大型容器映像檔可能增加安全漏洞,因為其中包含的內容超出程式碼需求。

由於 Cloud Run 採用容器映像檔串流技術,容器映像檔的大小不會影響容器啟動時間或要求處理時間。容器映像檔大小也不會計入容器的可用記憶體

如要建構最小的容器,請考慮使用精簡的基本映像檔,例如:

Ubuntu 比較大,但也是經常與立即可用的更完整伺服器環境搭配使用的基本映像檔。

如果您的服務具有需要大量使用工具的建構程序,請考慮使用多階段建構來確保您的容器在執行階段不會占用大量資源。

以下資源提供建立精簡容器映像檔的進一步資訊: