一般開發提示

本指南提供設計、實作、測試及部署 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 定價是按執行個體時間計費。如果您設定以執行個體為依據的計費,執行個體時間就是每個執行個體的總生命週期。如果您設定以要求為依據的計費模式,執行個體時間就是每個執行個體處理至少一項要求所花費的時間。

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

  • 降低延遲時間
  • 執行個體可更快完成工作
  • 即使需要更多執行個體,執行個體關閉速度也更快

但也可能相反:如果執行個體數量增加的幅度,不如因延遲時間縮短而減少的每個執行個體執行時間,則降低並行要求數量上限可能會增加帳單費用。

如要最佳化帳單費用,最佳做法是透過負載測試,使用不同的最大並行要求設定,找出可產生最短可計費執行個體時間的設定,如container/billable_instance_time 監控指標所示。

容器安全性

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

如要改善容器安全性:

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

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

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

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

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

  • 使用自訂機構政策,禁止使用預先發布版功能。

自動執行安全性掃描

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

建構最少的容器映像檔

大型容器映像檔可能會增加安全漏洞,因為它們包含的內容超出程式碼所需的範圍。

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

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

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

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

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