一般開發提示

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

編寫有效的服務

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

避免背景活動

Knative Serving 上的應用程式處理完要求後,容器執行個體對 CPU 的存取權會遭到停用或大幅限制。因此,您不應啟動在要求處理常式範圍外執行的背景執行緒或常式。

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

背景活動是指在 HTTP 回應送出後發生的任何活動。請審查您的程式碼,確認在您傳送回應之前,所有非同步作業皆已完成。

如果懷疑您的服務可能有無法立即得知的背景活動,您可以查看記錄:在 HTTP 要求項目後面尋找是否有記錄任何活動。

刪除暫存檔

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

最佳化效能

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

快速啟動服務

由於容器執行個體會視需要調度資源,常用的方法是將執行環境完整初始化。這種初始化方法稱為「冷啟動」。如果用戶端要求觸發了冷啟動,則容器執行個體的啟動會發生額外的延遲。

啟動例行程序包含:

  • 啟動服務
    • 啟動容器
    • 執行 entrypoint 指令來啟動伺服器。
  • 檢查開啟的服務通訊埠。

最佳化服務啟動速度可將延誤容器執行個體處理要求的延遲時間縮至最短。

謹慎使用依附元件

如果您使用動態語言搭配相依的程式庫,例如匯入使用 Node.js 的模組,這些模組的載入時間會增加冷啟動期間的延遲時間。您可以透過以下方式縮短啟動延遲時間:

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

使用全域變數

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

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

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();
  }
}

最佳化並行

Knative 服務執行個體可同時處理多個要求 (即「並行」),最多可達可設定的並行要求數量上限。這與使用 concurrency = 1 的 Cloud Run 函式不同。

除非程式碼有特定的並行要求,否則請保留預設的並行要求數量上限設定。

為您的服務調整並行

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

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

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

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

使記憶體與並行相配

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

避免可變動的全域狀態

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

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

容器安全性

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

如要改善容器安全性:

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

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

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

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

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

自動執行安全性掃描

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

您也可以使用二進位授權,確保只部署安全的容器映像檔。

建構最小容器映像檔

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

在 Knative 服務中,容器映像檔的大小不會影響冷啟動或要求處理時間,也不會計入容器的可用記憶體

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

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

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

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