Azure Functions のパフォーマンスと信頼性を向上させる
この記事では、サーバーレス関数アプリのパフォーマンスと信頼性を向上させるためのガイダンスを紹介しています。 Azure Functions のさらに一般的なベスト プラクティスについては、Azure Functions のベスト プラクティスに関する記事を参照してください。
Azure Functions を使用して、サーバーレス ソリューションを構築および設計する際のベスト プラクティスを以下に示します。
実行時間の長い関数を使用しない
サイズが大きく実行時間の長い関数では、予期しないタイムアウトの問題が発生する可能性があります。 特定のホスティング プランのタイムアウトの詳細については、「Function App タイムアウト期間」を参照してください。
関数は、Node.js 依存関係の多さからそのサイズが大きくなることがあります。 また、依存関係をインポートすると、読み込み時間が長くなり、予期しないタイムアウトが発生する可能性もあります。 依存関係は明示的にも暗黙的にも読み込まれます。 コードによって読み込まれる単一のモジュールによって、独自の追加モジュールが読み込まれることがあります。
可能な限り、大きな関数は、連携して高速な応答を返す、より小さな関数セットにリファクタリングしてください。 たとえば、webhook または HTTP トリガー関数では、一定の時間内に確認応答が必要になる場合があります。webhook は通常、即座に応答を必要とします。 この HTTP トリガー ペイロードは、キュー トリガー関数によって処理されるキューに渡すことができます。 このアプローチを使用すると、実際の作業を遅らせて、即座に応答を返すことができます。
バックグラウンド タスクが完了していることを確認する
関数がタスク、コールバック、スレッド、プロセスを開始するときは、関数コードが戻る前に完了する必要があります。 関数はこれらのバックグラウンド スレッドを追跡しないため、バックグラウンド スレッドの状態に関係なくサイトのシャットダウンが発生する可能性があります。これにより、関数で意図しない動作が発生する可能性があります。
たとえば、関数がバックグラウンド タスクを開始し、タスクが完了する前に成功応答を返した場合、Functions ランタイムは、バックグラウンド タスクの結果に関係なく、実行が正常に完了したと見なします。 このバックグラウンド タスクが重要な作業を実行している場合は、サイトのシャットダウンによって割り込まれる可能性があり、その作業は不明な状態のままになります。
関数間の通信
Durable Functions と Azure Logic Apps は、状態遷移と複数の関数間での通信を管理するように構築されています。
複数の関数と統合する際に Durable Functions も Logic Apps も使用しない場合、関数間通信にストレージ キューを使用することが推奨されます。 その主な理由は、他のストレージ オプションよりストレージ キューの方が安価であり、プロビジョニングがはるかに容易なためです。
ストレージ キュー内では、個々のメッセージのサイズが 64 KB に制限されます。 関数間でこれよりも大きなメッセージを渡す必要がある場合は、Azure Service Bus キューを使用して、Standard レベルで最大 256 KB、Premium レベルで最大 100 MB のメッセージ サイズをサポートできます。
メッセージの処理の前にフィルター処理を必要とする場合は、Service Bus のトピックが役立ちます。
イベント ハブは、大量の通信をサポートするのに便利です。
ステートレスな関数を記述する
可能であれば、関数はステートレスかつべき等である必要があります。 すべての必要な状態情報をデータに関連付けます。 たとえば、処理する注文には、state
メンバーが関連付けられている可能性があります。 関数は、それ自体がステートレスなまま、その状態に基づいて注文を処理できます。
べき等性を持つ関数は、特にタイマー トリガーと共に使用することをお勧めします。 たとえば、1 日に 1 回必ず実行する必要のある操作がある場合は、1 日の中の任意の時間にその操作を実行して同じ結果が得られるように記述します。 特定の日に操作を実行しない場合、この関数は終了することができます。 また、前回の実行が完了しなかった場合は、次の実行では中断した場所から操作を実行する必要があります。 これは、失敗時に再試行するメッセージベースのバインドでは特に重要です。 詳細については、「同一入力のための Azure Functions の設計」を参照してください。
防御的な関数を記述する
関数は時を選ばす例外に遭遇する可能性があることを想定する必要があります。 関数を設計する場合は、前回失敗した処理を、次回そのポイントから続行する機能を組み込みます。 次のアクションを必要とするシナリオを考えてみましょう。
- データベース内の 10,000 行を照会する。
- これらの行のそれぞれに対してキュー メッセージを作成して、最後まで処理を実行する。
システムの複雑さによっては、ダウンストリーム サービスが正しく動作しない、ネットワーキングが停止する、クォータ制限に達するなどの問題が発生する可能性があります。いずれも、時を選ばず関数に影響する可能性があります。 関数を設計する際は、このような状況を想定する必要があります。
これらのアイテムのうちの 5,000 個を処理のためにキューに入れた後で障害が発生する状況を想定した場合、どのように対応すればよいでしょうか。 完了したセット内のアイテムを追跡するようにしましょう。 そうでないと、次回同じアイテムを再び挿入することになります。 このように挿入が 2 回になると、ワークフローに深刻な影響が出ることがあります。そのため、関数はべき等にしてください。
キュー アイテムが既に処理されている場合は、関数による操作をなしにします。
Azure Functions プラットフォームで使用するコンポーネントに対して既に提供されている防御策を活用してください。 一例として、Azure Storage キューのトリガーとバインドに関するドキュメントの「有害キュー メッセージの処理」を参照してください。
HTTP ベースの関数では、Azure API Management を使用した API バージョン管理戦略を検討してください。 たとえば、HTTP ベースの関数アプリを更新する必要がある場合は、新しい更新プログラムを別の関数アプリにデプロイし、API Management リビジョンまたはバージョンを使用してクライアントを新しいバージョンまたはリビジョンに転送します。 すべてのクライアントがバージョンまたはリビジョンを使用していて、以前の関数アプリで実行が残らなくなったら、前の関数アプリのプロビジョニングを解除できます。
関数編成のベスト プラクティス
ソリューションの一部として、複数の関数を開発し、公開することができます。 このような関数は、多くの場合、1 つの関数アプリに結合されますが、別々の関数アプリ内で実行することもできます。 Premium および専用 (App Service) ホスティング プランでは、同じプランで実行することにより、複数の関数アプリが同じリソースを共有することもできます。 関数と関数アプリをグループ化する方法は、ソリューション全体のパフォーマンス、スケーリング、構成、デプロイ、セキュリティに影響する場合があります。 すべてのシナリオに適用される規則はないため、関数を計画および開発するときは、このセクションの情報を検討してください。
パフォーマンスとスケーリングを考慮して関数を編成する
作成する個々の関数にはメモリ占有領域があります。 通常、このメモリ占有領域はわずかですが、関数アプリ内の関数が多すぎると、新しいインスタンスでのアプリの起動が遅くなる場合があります。 また、関数アプリの全体的なメモリ使用量が高くなるおそれもあります。 1 つのアプリに含めるべき関数の数は、個々のワークロードによって異なるため、一概には言えません。 しかし、メモリに大量のデータを格納する関数の場合は、1 つのアプリに含まれる関数の数を少なくすることを検討してください。
1 つの Premium プランまたは専用 (App Service) プランで複数の関数アプリを実行する場合は、プランに割り当てられている同じリソースが、これらのすべてのアプリによって共有されます。 他のアプリよりもメモリ要件がはるかに高い 1 つの関数アプリがある場合、アプリのデプロイ先である各インスタンス上のメモリ リソースが過剰に消費されます。 その結果、各インスタンス上で他のアプリに使用できるメモリが少なくなるおそれがあるため、このようにメモリを大量に使用する関数アプリは、独自の個別のホスティング プランで実行することをお勧めします。
Note
従量課金プランを使用する場合、いずれにしてもアプリは個別にスケーリングされるため、常に各アプリを独自のプランに配置することをお勧めします。 詳細については、「同じプランでの複数のアプリ」をご覧ください。
負荷プロファイルが異なる関数をグループ化するかどうかを検討します。 たとえば、何千個ものキュー メッセージを処理する関数と、呼び出し頻度が低くメモリ要件が高い関数がある場合は、それらを個別の関数アプリにデプロイすることで、独自のリソース セットを取得し、互いに独立してスケーリングできるようにします。
構成とデプロイを考慮して関数を編成する
関数アプリには host.json
ファイルがあります。これは、関数トリガーと Azure Functions ランタイムの高度な動作を構成するために使用されます。
host.json
ファイルに対する変更は、アプリ内のすべての関数に適用されます。 カスタムの構成が必要な関数がある場合は、それらを独自の関数アプリに移動することを検討してください。
ローカル プロジェクトのすべての関数は、Azure Functions アプリに一連のファイルとしてまとめてデプロイされます。 必要に応じて、個々の関数を個別にデプロイしたり、デプロイ スロットなどの機能を一部の関数にのみ使用したりする場合があります。 このような場合は、これらの関数を (個別のコード プロジェクトで) 別の関数アプリにデプロイする必要があります。
関数を権限別に整理する
アプリケーションの設定に格納されている接続文字列やそれ以外の資格情報では、関数アプリに含まれるすべての関数に対して、関連リソース内で同一のアクセス許可一式が付与されます。 こうした資格情報を使用しない関数は別の関数アプリに移動して、特定の資格情報にアクセスできる関数の数を最小限に抑えることを検討してください。 異なる関数アプリの関数間でのデータの受け渡しは、関数チェーンなどの手法を使用すればいつでも行えます。
スケーラビリティのベスト プラクティス
関数アプリのインスタンスのスケーリングに影響を及ぼす多数の要素があります。 詳細については、関数のスケーリングに関するドキュメントをご覧ください。 関数アプリの最適なスケーラビリティを確保するためのベスト プラクティスを以下に示します。
接続の共有と管理
可能であれば、外部リソースへの接続を再利用します。 「Azure Functions で接続を管理する方法」をご覧ください。
ストレージ アカウントの共有を回避する
関数アプリを作成する場合は、ストレージ アカウントに関連付ける必要があります。 ストレージ アカウント接続は、AzureWebJobsStorage アプリケーション設定の中で管理されます。
パフォーマンスを最大化するには、関数アプリごとに個別のストレージ アカウントを使用します。 Durable Functions または Event Hub によってトリガーされる関数がある場合には、これは特に重要です。どちらも、大量のストレージ トランザクションを生成します。 アプリケーション ロジックが (Storage SDK を使用して) 直接、あるいは、ストレージ バインドの 1 つを経由して Azure Storage と対話する場合、専用のストレージ アカウントを使用する必要があります。 たとえば、Event Hub によってトリガーされ BLOB ストレージにデータを書き込む関数がある場合、2 つのストレージ アカウントを使用します。1 つは関数アプリ用、もう 1 つは関数によって格納されている BLOB 用になります。
同じ関数アプリにテスト コードと運用環境のコードを混在させない
Function App 内の関数はリソースを共有します。 たとえば、メモリは共有されます。 運用環境で Function App を使用している場合は、テストに関連する関数およびリソースを追加しないでください。 これが原因で、運用環境のコードの実行中に予期しないオーバーヘッドが発生する可能性があります。
運用環境の Function App で読み込むものに注意してください。 メモリは、アプリ内の各関数間で平均化されます。
複数の .NET 関数で参照される共有アセンブリがある場合は、それを共通の共有フォルダーに配置します。 そうしないと、関数間で動作が異なる同じバイナリの複数のバージョンを誤ってデプロイする可能性があります。
実稼働コードでは、詳細ログを使用しないでください。パフォーマンスに悪影響を及ぼします。
非同期コードを使用し、呼び出しのブロックは避ける
非同期プログラミングは、特に I/O 操作のブロックが関連している場合に、推奨されるベスト プラクティスです。
C# では、Task
インスタンス上の Result
プロパティを参照したり Wait
メソッドを呼び出したりすることは、常に避けるようにします。 この手法により、スレッドが枯渇する可能性があります。
ヒント
HTTP または Webhook のバインディングを使用する予定がある場合は、不適切な HttpClient
のインスタンス化によって生じるおそれのあるポートの枯渇を防止してください。 詳細については、「How to manage connections in Azure Functions」(Azure Functions で接続を管理する方法) を参照してください。
複数のワーカー プロセスを使用する
既定では、Functions のどのホスト インスタンスでも、単一のワーカー プロセスが使用されます。 特に Python などのシングルスレッド ランタイムによってパフォーマンスを向上させるには、FUNCTIONS_WORKER_PROCESS_COUNT を使用して、ホストあたりのワーカープロセス数を増やします (最大 10 まで)。 次に、Azure Functions は、これらのワーカー間で同時関数呼び出しを均等に分散しようとします。
FUNCTIONS_WORKER_PROCESS_COUNT は、需要に合わせてアプリケーションをスケールアウトするときに Functions によって作成される各ホストに適用されます。
可能な限りメッセージをバッチで受信する
イベント ハブなどの一部のトリガーでは、1 つの呼び出しでメッセージのバッチを受信できます。 メッセージをバッチ処理すると、パフォーマンスが大幅に向上します。
host.json のリファレンス ドキュメントで詳述するように、host.json
ファイルで最大バッチ サイズを構成できます。
C# 関数の場合、型を厳密に型指定された配列に変更できます。 たとえば、メソッド シグネチャに、EventData sensorEvent
ではなく EventData[] sensorEvent
を指定できます。 他の言語の場合、バッチ処理を有効にするには、こちらに示すように、function.json
のカーディナリティ プロパティを明示的に many
に設定する必要があります。
コンカレンシーを適切に処理するようにホストの動作を構成する
関数アプリの host.json
ファイルでは、ホストのランタイムとトリガーの動作を構成できます。 バッチ処理の動作に加え、多数のトリガーのコンカレンシーを管理できます。 多くの場合、これらのオプションの値を調整することで、呼び出された関数の要求に合わせて各インスタンスを適切にスケールできます。
host.json ファイルの設定は、アプリ内のすべての関数 (関数の "1 つのインスタンス") に適用されます。 たとえば、2 つの HTTP 関数が含まれ、maxConcurrentRequests
要求数が 25 に設定された関数アプリがある場合、一方の HTTP トリガーに対する要求は共有された 25 の同時要求にカウントされます。 その関数アプリが 10 インスタンスにスケーリングされた場合、10 個の関数は 250 個の同時要求に効果的に対応できます (10 インスタンス * インスタンスあたり 25 個の同時要求)。
他のホスト構成オプションについては、host.json 構成に関する記事をご覧ください。
次のステップ
詳細については、次のリソースを参照してください。