みなさん、こんにちは。サイオステクノロジー武井です。今回はAzure Functionsのアーキテクチャーとスケーリングについてお話したいと思います。
Azure Functionsは、イベント駆動なサーバーレスコンピューティングサービスです。「サーバーレス」とは言っても、利用するユーザーがサーバーを意識する必要がないだけで、実際にサーバーは動いています。
そして、そのアーキテクチャを正しく理解しないと、スケール(今回はスケールアップではなくスケールアウトの方)の設定を正しく行えず、十分にリソースを活用できなかったり、無駄な課金が発生してしまうことがあります。
Azure Functionsのアーキテクチャ
Azure Functionsのアーキテクチャー概要は、以下の図のとおりです。
App Service Planは、Azure Functionsのスケールなど様々な設定を行う論理的な単位です。複数のFunction Appで共有が可能です(Consumption Planは除く)。
Instanceは、Function Appが稼働するVMになります。App Service Planの設定で指定した分だけ、インスタンス(=VM)が起動します。例えば、Azure FunctionsのApp Service Planで固定のインスタンス数を指定すると、その分だけインスタンスが起動します。Consumption PlanやPremium PlanだとScale Controllerというものがイベントのレートを監視して、Instanceの増減を行います。例えば、Azure Queue Storageを使って、Queue TriggerでFunction Appを起動するようにしていて、Queueに大量のメッセージが入ってイベントがたくさん起動して、一つのInstanceでさばききれないとScale Controllerが判断すると、Instanceがどんどん増えていきます。
Function Appは、複数の関数をまとめた論理的な単位です。Azure Functionsの実行単位はFunction Appではなく、Function Appの中に作成した関数です。開発者もこの関数単位で開発する必要があります。例えば関数functionA、functionB、functionCの3つを作った場合は以下のように一つのFunction Appの中に3つの関数が含まれる構成になります。
そして、Function Appは2種類のプロセス「Host Process」「Worker Process」で動いています。
Host ProcessはFunction Appの中核を担うプロセスです。Windowsで言えばw3wp.exe、Linuxで言えばdotnetプロセスになります。主な役割は、トリガーをリスニングして、トリガーの発生(例: HTTPリクエスト、キューメッセージ、タイマーイベントなど)に応じて、後述するWorker Processを起動します。
Worker Processは、Host Processから起動されて関数を実行します。Host Processはトリガーのリスニングなど全ての関数に共通のロジックと管理を担当し、Worker Processは特定の言語ランタイムに関連するロジックを担当します。このようにして、新しい言語ランタイムをサポートするためには新しいWorker Processを追加するだけで良く、Host Processは変更する必要がありません。Host ProcessとWorker ProcessはgRPCによって通信を行います。この仕組みによってAzure Functionsは柔軟な拡張性を実現しています。
ちなみに、上図ではJavaやPython、Nodeなど複数のランタイムのWorker Processがありますが、実際には一つのFunction Appで実行することができるランタイムは一つのみです。
Azure Functionsのスケーリング
本章では、Azure Functionsのスケーリングについてお話します。
Azure Functionsの関数の同時実行数は以下の通りとなります。
インスタンス数 × Function App単位での関数の同時実行数
インスタンス数は先程ご説明したように、Function AppがホストされるVMになります。
Function App単位での関数の同時実行数については、その記載の通り、Function Appごとに関数の同時実行数というのを設定することができます。
例えば、Function Appの設定ファイルであるhost.jsonに以下のように定義することで、Azure Queue Storageのキューから取得して処理するメッセージの最大数をbacthSizeで指定することができます(このあたりの設定方法はバインドの種類によって異なります)。
{
"version": "2.0",
"extensions": {
"queues": {
"maxPollingInterval": "00:00:02",
"visibilityTimeout" : "00:00:30",
"batchSize": 3,
"maxDequeueCount": 5,
"newBatchThreshold": 1,
"messageEncoding": "base64"
}
}
}
つまり、上記の設定(bacthSize: 3)を行い、インスタンス数が2つにスケールした場合、関数の同時実行数は3×2の6になります(bacthSizeはFunctions App内でQueueトリガーで動作する個々の関数全てに適用されるので、以下の例ではFunctions App内に関数は一つであることを前提にしています)。
基本的にAzure Functionsのスケールアウト時の関数の同時実行数の考え方はこのようになります。
ただ、このあたりはプランによって微妙に違うので、以降で説明していきます。
Consumption Planの場合
個人的にはあまり使わないConsumption Planなのですが、このプランの特徴は関数を実行した時間のみ課金されるという料金体系です。先程もお話したように以下のような感じで、Scale Controllerがそのイベントレートに応じてインスタンス数を増やしていきます。
Consumption Planでは以下の設定によって、Function App単位でインスタンスのスケール数を制限することができます。
そして、この場合の関数の最大同時実行数は、例えば先の例のようにQueue TriggerでbatchSizeを3、スケールアウト制限の上限を200にすると
200(インスタンス数) × 3(Function Appごとの関数の同時実行数) = 600
になります。
Premium Planの場合
利用する機会の多いPremium Planです。Consumption Planのようにイベントレートによりスケーリングしますが、仮想ネットワークに参加できたり、実行時間がConsumption Planより長かったりと使い勝手がよいです。
このプランはApp Service Planで指定した最大バーストを上限に、Function Appごとにスケールする最大インスタンスを設定できます。
以下のようにApp Service Planで最大バースト20を設定していた場合、Function App 1とFunction App 2という2つのFunction AppでApp Service Planを共有すると、それぞれのFunction Appは最大20までスケールすることができます。つまり、イベントレートによって最大40インスタンスまでスケールする可能性があります。
そして、この場合の関数の最大同時実行数は、例えばConsumption PlanのようにQueue TriggerでbatchSizeを3にすると
{20(Function App 1の最大インスタンス数) + 20(Function App 2の最大インスタンス数) }× 3(Function Appごとの関数の同時実行数) = 120
になります。
このあたりの仕組みを理解していないと、うっかり無駄にスケールして意図せぬ課金がかかることがあるかもしれません。
以下が設定画面になります。「プランのスケールアウト」というところにある「最大バースト」が先程説明した設定になります。
その下に「アプリのスケールアウト」というのがありまして、これがFunction App単位のスケールの設定です。以下の設定では、常に10個のインスタンスが稼働しており、イベントレートに応じて最大バーストの上限に設定した20までスケールする設定になっています。
ただ、RDBのように最大接続数に上限があるリソースや、SaaSサービスのAPIのようにスロットリング制限が設けられている外部サービスに対してAzure Functionsでアクセスする場合、無尽蔵にインスタンスをスケールしてはまずいので、このあたりはログやメトリクスを見ながら最適な値を調整する必要があります。
App Service Planの場合
App Service Planの場合は、もっとシンプルです。私は、Durable Functionsが利用できないような、ロングランのバッチ実行によくこのプランを使います。
App Service Planで設定したInstance Countで常に固定の数のインスタンスを起動しておくか、Azure Monitorのメトリクスベースでスケールすることができます。
例えば、以下の図のように、固定で3つのインスタンス数を起動するようApp Serviceを設定し、2つのFunction App(Function App 1とFunction App 2)がApp Service Planを共有している場合、その配置は以下のようになります。3つのインスタンスが起動し、各インスタンスにFunction App 1とFunction App 2が稼働します。
この場合の関数の最大同時実行数は、例えばConsumption PlanのようにQueue TriggerでbatchSizeを3にすると
3(インスタンス数) × { 3(Function App1の関数の同時実行数) + 3(Function App2の関数の同時実行数)} = 18
になります。
App Service Planの設定画面は以下です。「Manual」だと常に固定のインスタンス数が配置されます。「Rules Based」だとAzure Monitorのメトリクスベースでスケールアウトします。
まとめ
サーバーレスとはいえど、アーキテクチャを正しく理解して使って初めてリソースの有効活用が可能になります。この記事が、Azure Functionsのアーキテクチャ理解の一助になれば幸いです。