全般的な開発のヒント

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

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

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

バックグラウンド アクティビティ

バックグラウンド アクティビティは、HTTP レスポンスの送信後に発生します。サービスにすぐに実行されないバックグラウンド アクティビティがあるかどうかを確認するには、HTTP リクエストのエントリの後にログに記録されたものがないか確認します。

バックグラウンド アクティビティを使用するために常に割り当てられるように CPU を構成する

Cloud Run サービスでバックグラウンド アクティビティをサポートする場合は、リクエストの外部でもバックグラウンド アクティビティを実行できて、CPU にアクセスできるように、Cloud Run サービスの CPU を常に割り当てます。

CPU がリクエスト処理中にのみ割り当てられる場合は、バックグラウンド アクティビティを回避する

リクエストの処理中にのみ CPU を割り当てるようにサービスを設定する必要がある場合、Cloud Run サービスがリクエストの処理を終了すると、インスタンスの CPU へのアクセスが無効になるか、非常に制限されます。このタイプの CPU 割り当てを使用する場合は、リクエスト ハンドラの範囲外で実行されるバックグラウンド スレッドやルーティンを開始しないでください。

コードを確認し、レスポンスの送信前にすべての非同期処理が完了するようにしてください。

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

一時ファイルを削除する

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

エラーの報告

すべての例外を処理し、エラー発生時にサービスをクラッシュさせないようにします。トラフィックがキューに入り、代わりのインスタンスを待機している間にクラッシュすると、コールド スタートが発生します。

エラーを適切に報告する方法については、Error Reporting ガイドをご覧ください。

パフォーマンスの最適化

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

コンテナをすばやく起動する

インスタンスは必要に応じてスケーリングされるため、起動時間がサービスのレイテンシに影響します。Cloud Run は、インスタンスの起動とリクエストの処理を切り離しますが、リクエストは新しいインスタンスが起動するまで待機してから処理されることがあります。これは特にゼロからスケーリングする場合に起こります。これは「コールド スタート」と呼ばれます。

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

  • コンテナ イメージをダウンロードする(Cloud Run のコンテナ イメージ ストリーミング テクノロジーを使用)
  • entrypoint コマンドを実行してコンテナを起動する。
  • コンテナが構成されたポートでリッスンを開始するのを待つ。

コンテナの起動速度を最適化することで、リクエスト処理のレイテンシを最小限に抑えることができます。

起動時の CPU ブーストを使用して起動レイテンシを短縮する

起動時の CPU ブーストを有効にすることで、インスタンスの起動時に一時的に CPU 割り当てを増やして起動レイテンシを短縮できます。

最小インスタンスを使用してコールド スタートを減らす

最小インスタンス同時実行を構成して、コールド スタートを最小限に抑えることができます。たとえば、最小インスタンス数が 1 の場合、新しいインスタンスを起動しなくても、サービスに構成されている同時リクエストの最大数までサービスを使用できる状態になっています。

インスタンスの起動を待機しているリクエストは、次のようにキュー内で保留状態になります。

  • スケールアウト中など、新しいインスタンスが起動されると、少なくともこのサービスのコンテナ インスタンスの平均起動時間の間はリクエストが保留されます。これには、ゼロからのスケーリングなど、リクエストでスケールアウトが開始されるタイミングも含まれます。
  • 起動時間が 10 秒未満の場合、リクエストは最大で 10 秒間保留されます。
  • 起動プロセスにインスタンスがなく、リクエストがスケールアウトを開始しない場合、リクエストは最大で 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 インスタンスは、構成可能な最大同時実行数まで複数のリクエストを同時に処理できます。これは、concurrency = 1 を使用する Cloud Run functions とは異なります。

Cloud Run は、構成された最大値まで同時実行を自動的に調整します。

デフォルトの最大同時実行数である 80 は、多くのコンテナ イメージに適しています。ただし、次の点に注意してください。

  • コンテナで多数の同時リクエストを処理できない場合は、同時実行数を減らします。
  • コンテナで大量のリクエストを処理できる場合は、同時実行数を増やします。

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

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

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

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

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

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

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

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

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

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

スループット、レイテンシ、費用のトレードオフ

最大同時リクエスト数の設定を調整すると、サービスのスループット、レイテンシ、費用のトレードオフのバランスを取ることができます。

一般に、最大同時リクエスト数を低く設定すると、レイテンシが減少し、インスタンスあたりのスループットが低下します。最大同時リクエスト数を小さくすると、各インスタンス内でリソースの競合を生じさせるリクエストが少なくなり、各リクエストのパフォーマンスが向上します。ただし、各インスタンスが同時に処理できるリクエスト数が少なくなるため、インスタンスあたりのスループットが低下し、同じトラフィックを処理するためにサービスに多くのインスタンスが必要になります。

逆に、最大同時リクエスト数を大きく設定すると、通常、レイテンシが増加し、インスタンスあたりのスループットが上昇します。リクエストは、インスタンス内の CPU、GPU、メモリ帯域幅などのリソースへのアクセスを待機する必要があるため、レイテンシが増加する可能性があります。ただし、各インスタンスで一度に処理できるリクエスト数が多くなるため、同じトラフィックを処理するためにサービス全体で必要なインスタンス数は少なくなります。

費用に関する考慮事項

Cloud Run の課金はインスタンス時間単位です。CPU が常に割り当てられている場合、インスタンス時間は各インスタンスの合計存続期間となります。CPU が常に割り当てられるわけではない場合、インスタンス時間は、各インスタンスが 1 つ以上のリクエストの処理に費やす時間となります。

最大同時リクエスト数が課金に与える影響は、トラフィック パターンによって異なります。最大同時リクエスト数の設定を小さくすると、設定を下げたことで次のような効果が得られる場合、請求額が下がる可能性があります。

  • レイテンシが短縮される
  • インスタンスの作業完了が早くなる
  • 必要なインスタンスの総数が多くなるにもかかわらず、インスタンスのシャットダウンが速くなる

ただし、その逆もあり得ます。レイテンシの改善により各インスタンスの実行時間が短縮されても、インスタンス数が増えることで実行時間全体の短縮には至らない場合は、最大同時リクエスト数を小さくすると課金が増える可能性があります。

課金を最適化するためには、さまざまな最大同時リクエスト数の設定を使用して負荷テストを行い、container/billable_instance_time モニタリング指標に表示される課金対象インスタンス時間が最も短い設定を見極めることをおすすめします。

コンテナ セキュリティ

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

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

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

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

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

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

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

  • カスタム組織のポリシーを使用して、プレビュー機能の使用を禁止します。

セキュリティ スキャンを自動化する

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

最小コンテナ イメージをビルドする

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

Cloud Run のコンテナ イメージ ストリーミング テクノロジーのため、コンテナ イメージのサイズは、コールド スタートやリクエストの処理時間に影響しません。また、コンテナ イメージのサイズは、コンテナの使用可能なメモリにはカウントされません。

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

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

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

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