ヒントとアドバイス

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

正確性

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

べき等関数を作成する

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

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

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

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

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

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

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

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

一時ディレクトリ以外への書き込みはしないでください。また、プラットフォームや OS に依存しない方法でファイルパスを構築してください。

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

Functions Framework

関数をデプロイすると、Functions Framework がその現在のバージョンを使用して、自動的に依存関係として追加されます。異なる環境間で同じ依存関係が一貫してインストールされるように、関数を Functions Framework の特定のバージョンに固定することをおすすめします。

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

ツール

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

ローカルでの開発

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

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

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

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

パフォーマンス

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

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

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

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

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

関数の状態は、将来の呼び出しのために必ずしも保持されるわけではありません。ただし、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 クライアント オブジェクトをグローバル スコープでキャッシュに保存することが非常に重要です。例については、ネットワークの最適化の記事をご覧ください。

グローバル変数の遅延初期化を行う

グローバル スコープの変数を初期化する場合、初期化コードは常にコールド スタート呼び出しによって実行され、関数のレイテンシが長くなります。特定の場合では、try / catch ブロックで適切に処理されないと、これによって呼び出されるサービスの断続的なタイムアウトが発生します。一部のコードパスでのみ使用されるオブジェクトについては、必要に応じて遅延させて初期化することを検討してください。

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

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

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

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

参考情報

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