ヒントとアドバイス

このドキュメントでは、Cloud Functions の関数を設計、実装、テスト、デプロイする際のベスト プラクティスについて説明します。

正確性

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

べき等関数を作成する

関数は、何回呼び出されても結果が同じになることが必要です。これにより、前の呼び出しがコードの途中で失敗した場合は、呼び出しを再試行できます。詳細については、イベント ドリブン関数の再試行をご覧ください。

バックグラウンド アクティビティを開始しない

バックグラウンド アクティビティは関数の終了後に発生します。関数の呼び出しの終了とは、関数が結果を返したときです。または、Node.js イベント ドリブン関数で callback 引数を呼び出すなどして完了のシグナルを発生させることもあります。正常に終了した後は、あらゆるコードは CPU にアクセスできず、処理を続行できません。

加えて、同じ環境で次の呼び出しが実行されると、バックグラウンド アクティビティが再開され、新しい呼び出しが中断されます。これが、予期せぬ動作や診断が難しいエラーにつながる可能性があります。関数の終了後にネットワークにアクセスすると、通常は接続がリセットされます(ECONNRESET エラーコード)。

個々の呼び出しのログで呼び出し完了を示す行の後を見ると、バックグラウンド アクティビティが記録されていることがよくあります。特に、コールバックやタイマーなどの非同期処理が存在する場合は、バックグラウンド アクティビティがコードの中に埋もれている可能性があります。 コードを確認し、関数の終了前にすべての非同期処理が完了するようにしてください。

一時ファイルを常に削除する

一時ディレクトリ内のローカル ディスク ストレージは、メモリ内ファイル システムです。書き込んだファイルは関数で使用できるメモリを消費し、呼び出し後も維持される場合があります。これらのファイルを明示的に削除しないと、最終的にメモリ不足エラーにつながり、その結果コールド スタートが発生する可能性があります。

個々の関数で使用されるメモリの量を確認するには、Google Cloud コンソールの関数リストで関数を選択してから、[メモリ使用量] プロットを選択します。

長期ストレージにアクセスする必要がある場合は、Cloud Run のボリューム マウントを使用して Cloud Storage または NFS ボリュームをマウントすることを検討してください。

パイプラインを使用して、サイズの大きいファイルを処理する際のメモリ要件を減らすことができます。たとえば、Cloud Storage でファイルを処理するために、読み取りストリームを作成し、これをストリームベースのプロセスに渡してから、出力ストリームを Cloud Storage に直接書き込むことができます。

Functions Framework

異なる環境間で同じ依存関係が一貫してインストールされるように、Functions Framework ライブラリをパッケージ管理システムに含め、依存関係を Functions Framework の特定のバージョンに固定することをおすすめします。

これを行うには、該当するロックファイル(たとえば、Node.js の場合は package-lock.json、Python の場合は requirements.txt)に優先するバージョンを含めます。

Functions Framework が依存関係として明示的にリストされていない場合、利用可能な最新バージョンを使用して、ビルドプロセス中に自動的に追加されます。

ツール

このセクションでは、ツールを使用して Cloud Functions の関数を実装、テスト、操作する方法を説明します。

ローカルでの開発

関数のデプロイには時間がかかるため、多くの場合、関数のコードをローカルでテストするほうが時間を短縮できます。

Firebase デベロッパーは Firebase CLI Cloud Functions エミュレータを使用できます。

SendGrid を使ってメールを送信する

Cloud Functions はポート 25 での送信接続を許可しないため、保護されていない接続では SMTP サーバーに接続できません。メールを送信するには、SendGrid などのサードパーティ サービスを使用することをおすすめします。メールの送信に関するその他のオプションについては、Google Compute Engine のインスタンスからのメールの送信のチュートリアルをご覧ください。

パフォーマンス

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

同時実行数の低下を避ける

コールド スタートはコストがかかるため、負荷を処理するのに最適な方法は、負荷が急増したときに、最近起動したインスタンスを再利用できるようにすることです。同時実行数を制限すると、既存のインスタンスの利用方法が制限されるため、コールド スタートの発生数が増えます。

同時実行数を増やすと、1 つのインスタンスで複数のリクエストを延期できるようになるため、負荷の急増に対処しやすくなります。

依存関係を適切に使用する

関数はステートレスであるため、多くの場合、実行環境はゼロから初期化されます(これをコールド スタートといいます)。コールド スタートが発生すると、関数のグローバル コンテキストが評価されます。

関数によってモジュールがインポートされる場合、それらのモジュールの読み込み時間は、コールド スタート時の呼び出しレイテンシに加算されます。依存関係を正しく読み込み、関数が使用しない依存関係を読み込まないようにすることで、このレイテンシと関数のデプロイに必要な時間を短縮できます。

グローバル変数を使用して将来の呼び出しでオブジェクトを再利用する

関数の状態は、将来の呼び出しのために必ずしも保持されるわけではありません。ただし、Cloud Functions が以前の呼び出しの実行環境をリサイクルすることはよくあります。変数をグローバル スコープで宣言すると、その値は再計算せずに後続の呼び出しで再利用できるようになります。

この方法では、関数の呼び出しごとに再作成するためコストが高くなりがちなオブジェクトをキャッシュに保存できます。このようなオブジェクトを関数の本文からグローバル スコープに移動して、パフォーマンスを大幅に向上することができます。次の例では、heavy オブジェクトを関数のインスタンスにつき 1 回だけ作成し、指定されたインスタンスに到達するまですべての関数呼び出しで共有します。

Node.js

console.log('Global scope');
const perInstance = heavyComputation();
const functions = require('firebase-functions');

exports.function = functions.https.onRequest((req, res) => {
  console.log('Function invocation');
  const perFunction = lightweightComputation();

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

Python

import time

from firebase_functions import https_fn

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

@https_fn.on_request()
def scope_demo(request):

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

この HTTP 関数は、リクエスト オブジェクト(flask.Request)を引数とし、レスポンス テキストを返します。または、make_response を使用して Response オブジェクトに変換できる値のセットを返します。

ネットワーク接続、ライブラリ参照、および API クライアント オブジェクトをグローバル スコープでキャッシュに保存することが非常に重要です。 例については、ネットワークの最適化をご覧ください。

インスタンスの最小数を設定してコールド スタートを減らす

デフォルトでは、Cloud Functions は受信リクエスト数に基づいてインスタンス数をスケールします。このデフォルトの動作は、Cloud Functions がいつでもリクエストを処理できるために必要なインスタンスの最小数を設定して変更できます。インスタンスの最小数を設定すると、アプリケーションのコールド スタートが減少します。レイテンシの影響を受けやすいアプリケーションの場合は、インスタンスの最小数を設定し、読み込み時に初期化を完了することをおすすめします。

これらのランタイム オプションの詳細については、スケーリング動作を制御するをご覧ください。

コールド スタートと初期化に関する注意事項

グローバル初期化は読み込み時に行われます。グローバル初期化を行わなかった場合は、最初のリクエストで初期化とモジュールの読み込みを行う必要が生じるため、レイテンシが増加します。

ただし、グローバル初期化の影響はコールド スタートにも及びます。この影響を最小限に抑えるには、最初のリクエストに必要な要素のみを初期化して、最初のリクエストのレイテンシを可能な限り低く抑えます。

これは、上記の手順に沿って、レイテンシの影響を受けやすい関数の最小インスタンス数を構成した場合に特に重要です。この場合、読み込み時に初期化を完了し、有用なデータをキャッシュに保存しておくと、最初のリクエストで初期化を行う必要がなくなり、処理するときのレイテンシを低く抑えることができます。

グローバル スコープで変数を初期化する場合、言語によっては初期化に要する時間が長くなるため、2 つの動作が発生する可能性があります。1 つ目の動作として、言語と非同期ライブラリを組み合わせて使用している場合は、関数フレームワークが非同期で実行され、すぐに返されます。その結果、コードはバックグラウンドで継続的に実行されようになり、CPU にアクセスできないなどの問題が発生することがあります。この問題を回避するには、以下のようにモジュールの初期化をブロックする必要があります。同様に、初期化が完了するまでリクエストが処理されないようにする必要があります。一方、初期化が同期的に行われる場合は、初期化時間が長くなると、コールド スタートに要する時間も長くなります。そのため、負荷が急増したときに関数の同時実行数が低下するなどの問題が発生することがあります。

node.js 非同期ライブラリをプリウォーミングする例

node.js 非同期ライブラリの例としては、Firestore で node.js を使用する場合があります。min_instances を活用するには、次のコードを使用して読み込み時に読み込みと初期化を完了し、モジュールの読み込みをブロックします。

TLA が使用されるため、ES6 が必要になります。node.js コードに .mjs 拡張子を使用するか、package.json ファイルに type: module を追加してください。

{
  "main": "main.js",
  "type": "module",
  "dependencies": {
    "@google-cloud/firestore": "^7.10.0",
    "@google-cloud/functions-framework": "^3.4.5"
  }
}

Node.js

import Firestore from '@google-cloud/firestore';
import * as functions from '@google-cloud/functions-framework';

const firestore = new Firestore({preferRest: true});

// Pre-warm firestore connection pool, and preload our global config
// document in cache. In order to ensure no other request comes in,
// block the module loading with a synchronous global request:
const config = await firestore.collection('collection').doc('config').get();

functions.http('fetch', (req, res) => {

// Do something with config and firestore client, which are now preloaded
// and will execute at lower latency.
});

グローバル初期化の例

Node.js

const functions = require('firebase-functions');
let myCostlyVariable;

exports.function = functions.https.onRequest((req, res) => {
  doUsualWork();
  if(unlikelyCondition()){
      myCostlyVariable = myCostlyVariable || buildCostlyVariable();
  }
  res.status(200).send('OK');
});

Python

from firebase_functions import https_fn

# 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

@https_fn.on_request()
def lazy_globals(request):

  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 https_fn.Response(f"Lazy: {lazy_global}, non-lazy: {non_lazy_global}.")
  

この HTTP 関数は、遅延初期化された globals を使用します。リクエスト オブジェクト(flask.Request)を引数とし、レスポンス テキストを返します。または、make_response を使用して Response オブジェクトに変換できる値のセットを返します。

これは特に、複数の関数を 1 つのファイルに定義し、関数ごとに異なる変数を使用する場合に重要です。遅延初期化を使用しないと、初期化されたが使用されない変数にリソースを浪費することがあります。

参考情報

パフォーマンスの改善について詳しくは、「Google Cloud Performance Atlas」の動画 Cloud Functions Cold Boot Time をご覧ください。