全般的な開発のヒント

このガイドでは、Knative serving サービスを設計、実装、テスト、デプロイするためのベスト プラクティスについて説明します。その他のヒントについては、既存のサービスを移行するをご覧ください。

効率の良いサービスを作成する

このセクションでは、Knative serving サービスを設計および実装するための一般的なベスト プラクティスについて説明します。

バックグラウンド アクティビティを回避する

Knative serving で実行中のアプリケーションがリクエストの処理を終了すると、コンテナ インスタンスの CPU へのアクセスが無効になるか、厳しく制限されます。したがって、リクエスト ハンドラの範囲外で実行されるバックグラウンド スレッドやルーティンを開始しないでください。

バックグラウンド スレッドを実行すると、同じコンテナ インスタンスへの後続のリクエストが中断されたバックグラウンド アクティビティを再開するため、予期しない動作が発生することがあります。

バックグラウンド アクティビティは、HTTP レスポンスの送信後に発生します。コードを確認し、レスポンスの送信前にすべての非同期処理が完了するようにしてください。

サービスにすぐに実行されないバックグラウンド アクティビティがあると思われる場合は、ログを確認して、HTTP リクエストのエントリの後に記録されたものを調べてください。

一時ファイルを削除する

Cloud Run 環境では、メモリ内ファイル システムがディスク ストレージになります。 ディスクに書き込まれたファイルにより、サービスで本来使用されないメモリが次の呼び出しまで継続的に使用される可能性があります。これらのファイルを削除しないと、最終的にメモリ不足エラーにつながり、その結果コールド スタートが発生する可能性があります。

パフォーマンスを最適化する

このセクションでは、パフォーマンスを最適化するためのベスト プラクティスについて説明します。

サービスを迅速に開始する

コンテナ インスタンスは必要に応じてスケーリングされます。このため、実行環境を完全に初期化するのが一般的です。このような初期化をコールド スタートといいます。クライアント リクエストでコールド スタートがトリガーされると、コンテナ インスタンスの起動にレイテンシが発生します。

起動ルーティンでは次の処理が行われます。

  • サービスを開始する
    • コンテナを起動する
    • entrypoint コマンドを実行してサーバーを起動する
  • 開いているサービスポートを確認する

サービスの起動速度を最適化すると、コンテナ インスタンスのリクエスト処理を遅らせるレイテンシを最小限に抑えられます。

依存関係を上手に利用する

Node.js にモジュールをインポートするなど、依存ライブラリのある動的言語を使用する場合、こうしたモジュールの読み込み時間によってコールド スタート時のレイテンシが長くなります。起動時のレイテンシを短縮するには、次のような対策を行います。

  • 依存関係の数とサイズを最小限に抑えてリーンサービスを構築する。
  • 使用している言語でサポートされている場合は、使用頻度の低いコードの読み込みを延期する。
  • PHP の Composer オートローダーの最適化など、コード ローディングの最適化を行う。

グローバル変数を使用する

Knative serving では、リクエスト間でサービスの状態が維持されるとは限りません。ただし、Knative serving はコンテナ インスタンスを再利用してトラフィックの処理を継続するため、グローバル スコープで変数を宣言することで、その値を以降の呼び出しで再利用できます。個々のリクエストで値が再利用されるかどうかを事前に確認することはできません。

サービス リクエストごとに再利用を行うとコストが高くなる場合は、オブジェクトをメモリ キャッシュに保存することもできます。これをリクエストのロジックではなく、グローバル スコープで行うと、パフォーマンスが向上します。

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 serving インスタンスは、構成可能な最大同時実行数まで複数のリクエストを同時に処理できます。これは、concurrency = 1 を使用する Cloud Run functions とは異なります。

コードに特別な同時実行要件がある場合を除き、デフォルトの最大同時実行数の設定を維持する必要があります。

サービスの同時実行を調整する

各コンテナ インスタンスで処理できる同時リクエストの数は、テクノロジー スタックと、変数やデータベース接続などの共有リソースの使用によって制限される可能性があります。

最も安定した状態で同時実行が行われるようにサービスを最適化するには:

  1. サービスのパフォーマンスを最適化します。
  2. コードレベルの同時実行に、予想される同時実行のサポートレベルを構成します。すべてのテクノロジー スタックでこのような設定が必要になるわけではありません。
  3. サービスをデプロイします。
  4. Knative serving の同時実行の設定で、コードレベルと同等またはそれより低い値をサービスに構成します。コードレベルの構成がない場合は、予想される同時実行の値を使用します。
  5. 同時実行を構成できる負荷テストツールを使用します。予想される負荷と同時実行の設定でサービスの動作が安定していることを確認します。
  6. サービスのパフォーマンスが低下している場合は、ステップ 1 に戻ってサービスを改善するか、ステップ 2 に戻って同時実行数を少なくします。サービスが正常に動作するようになったら、ステップ 2 に戻って同時実行数を増やします。

同時実行が最も安定した状態になるまで、この操作を繰り返します。

メモリを同時実行に合わせる

サービスがリクエストを処理するたびに、ある程度の追加メモリが必要になります。このため、同時実行を調整する場合には、メモリ制限も調整する必要があります。

変更可能なグローバル状態を避ける

同時実行のコンテキストで変更可能なグローバル状態を使用する場合は、この処理が安全に行われるように対策を行う必要があります。グローバル変数の初期化を 1 回に限定することで競合を最小限に抑え、上記のパフォーマンスで説明したように再利用します。

同時に複数のリクエストを処理するサービスで変更可能なグローバル変数を使用する場合は、ロックまたはミューテックスを使用して競合状態を防ぐ必要があります。

コンテナ セキュリティ

コンテナ化されたアプリケーションにも、多くのソフトウェアで使用されているセキュリティ プラクティスが適用されます。このような対策の中には、コンテナに固有のものや、コンテナの概念やアーキテクチャに合わせて調整が必要なものがあります。

コンテナのセキュリティを向上させるには:

  • Google のベースイメージなど、積極的にメンテナンスされている安全なベースイメージか、Docker Hub の公式イメージを使用します。

  • コンテナ イメージを定期的にビルドしてサービスをデプロイし直すことで、サービスにセキュリティ更新プログラムを適用します。

  • サービスの実行に必要なものだけをコンテナに含めます。余分なコード、パッケージ、ツールはセキュリティ上の脆弱性になる可能性があります。上記のパフォーマンスへの影響をご覧ください。

  • 特定のソフトウェアとライブラリのバージョンを含む確定的なビルドプロセスを実装します。これにより、コンテナに未検証のコードが追加されるのを防ぐことができます。

  • Dockerfile USER ステートメントを使用して、コンテナを root 以外のユーザーとして実行するように設定します。コンテナ イメージによっては、特定のユーザーが構成されている場合があります。

セキュリティ スキャンの自動化

Artifact Registry に格納されているコンテナ イメージにセキュリティ スキャンを実行するため、脆弱性スキャンを有効にします。

また、Binary Authorization を使用して、セキュアなコンテナ イメージのみがデプロイされるようにすることもできます。

最小コンテナ イメージの作成

コンテナ イメージのサイズが大きいと、コードに必要以上の情報が含まれているため、セキュリティ上の脆弱性が高まります。

Knative serving では、コンテナ イメージのサイズはコールド スタートやリクエストの処理時間に影響せず、コンテナの使用可能なメモリにはカウントされません。

最小サイズのコンテナをビルドするには、次のようなリーンベースのイメージの使用を検討してください。

Ubuntu は、サイズが大きくなりますが、すぐに利用できる完全なサーバー環境でよく使用されているベースイメージです。

サービスでツールに依存するビルドプロセスがある場合は、実行時のコンテナを軽量化するために、マルチステージ ビルドの使用を検討してください。

リーンコンテナ イメージの作成に関する詳細については、次のリソースをご覧ください。