以前に Azure Functions のアップデートで ReadyToRun が使えるようになったと書きましたが、64bit OS 上では win-x64
向けの ReadyToRun コンパイルしか行えなかった問題が直ったので真面目に計測しました。
ReadyToRun の説明は既に何回か書いたので省略しますが、単純に言うと AOT コンパイルです。
Tiered Compilation や ReadyToRun の効果があるかどうかを Azure Functions で計測するのは簡単ではないのですが、コールドスタートにかかる時間を短縮する方法としては有効だろうと思っていたので試しています。
単純な Function では差が付きにくいことは分かっていたので、依存関係が比較的多くなる Durable Functions と DI の組み合わせで試します。2020 年の Function 開発では DI は当たり前に使われるようになっています。
ReadyToRun を有効化する
最近行われた修正によって win-x86
向けでの ReadyToRun のコンパイルでエラーが発生しなくなっていますが、Function SDK 自体はまだアップデートされていないので、更新された Extensions Metadata Generator を直接プロジェクトに追加して対応します。
これでコンパイルが出来るようになります。SDK がアップデートされたら消してよいです。
公式ドキュメントでは直接 csproj
に ReadyToRun の設定を追加していましたが、デプロイ時のみ実行すればよいので Visual Studio からデプロイする場合には pubxml
に追加すれば実現できます。
<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <WebPublishMethod>ZipDeploy</WebPublishMethod> <PublishProvider>AzureWebSite</PublishProvider> <!-- 省略 --> <RuntimeIdentifier>win-x86</RuntimeIdentifier> <PublishReadyToRun>true</PublishReadyToRun> </PropertyGroup> </Project>
GitHub Actions や Azure Pipelines からデプロイする場合は .NET Core CLI にパラメータを追加します。
dotnet publish -c Release -r win-x86 -p:PublishReadyToRun=true
.NET Core CLI の -p
オプションは MSBuild のプロパティの指定なので覚えておいて損はないです。
今回は Visual Studio を使って Zip Deploy + Run From Package を行っているので、ReadyToRun の設定は pubxml
に追加しました。Function App 自体を 2 つ作成して、それぞれに ReadyToRun を有効・無効にしたものをデプロイすることで比較します。
計測に使用した環境
計測に使用した環境は以下の通りになります。ReadyToRun 設定の有無以外は全く同じです。
- Azure Functions Runtime: v3.0.14492
- リージョン: East US 2
- インスタンスの種類: Consumption
- デプロイ方法: Run From Package
- OS: Windows
- プラットフォーム: 32bit (win-x86)
- テストに使った Function: Durable Functions v2.3.0 と DI
- その他: 同一リソースグループ、同一 Webspace
今回 win-x86
を選んでいる理由は Consumption は 1 コア 1.5GB メモリにリソースが制限されるため、64bit を使うメリットが基本的に存在しないからです。当然ながら 32bit の方がメモリ使用量も少なくなります。
計測結果
Application Insights の Webtest 機能を使って、一定間隔でそれぞれの Function の HttpTrigger に対してリクエストを投げて、HTTP のレスポンス時間と Function Host の起動時間を調べています。
ウォームスタートにならないように大きく時間を空けて、1 リージョンからのみ送信します。
HTTP レスポンス時間
Function Host 起動時間
グラフを見ると分かるように、ReadyToRun の有無ではっきりと差が出ました。多少の外れ値はありますが、Consumption は共有型のホスティングかつファイルシステムを Azure Files を SMB でマウントしているので、その辺りの影響をダイレクトに受けるはずです。
これだけだとたまたま早いインスタンスに当たっただけという可能性もあるので、ReadyToRun の設定を逆転させてデプロイした結果が以下になります。
切り替えを行った後には値も逆転しているので、ReadyToRun が影響していることは明らかです。
Azure Functions はコールドスタートへの最適化が継続的に行われているので、1,2 年前と比べると何もしなくてもかなり早くなっていますが、ReadyToRun はそこから更に一押しする効果はあるようです。
物事には当然ながらメリットとデメリットが存在するので、最後にその辺りを軽くまとめます。
メリット
- 良いケースでは 20% 近くコールドスタートにかかる時間を削減できる
- Function Host だけを見ると 35% 近く削減できている
- 規模の大きな Function App ではさらに効果が期待できる
- 逆に依存関係が少なく、アセンブリサイズが小さい Function App では効果は薄そう
デメリット
- AOT コンパイルを行うためにビルド時間が若干が無くなる
- AOT コンパイル後のアセンブリサイズがかなり大きくなる
- アセンブリに IL とネイティブコードの両方を含むため
- Function App のプラットフォーム (32bit / 64bit) を変更するとリビルドが必要
検証のまとめ
ReadyToRun を有効にするデメリットは確かに存在していますが、大体は AOT コンパイルに関連する内容なので、Azure Functions がスケールアウト時に毎回 JIT で行っていたことを削減していると考えると、AOT で行うのは効率的だと言えます。
一番のデメリットはアセンブリサイズがかなり大きくなることだと思います。依存するライブラリにもよりますが、圧縮前で平均して 2 倍ぐらいになるようです。.NET Native や CoreRT とは仕組みが異なるので仕方ないですが、URL を設定する Run From Package の場合は問題になりやすいでしょう。
個人的には Consumption と Pre-Warming を無効化した Premium Plan の時に有効化すると思います。Pre-Warming が有効の場合はコールドスタートが原理上発生しないので、有効化しても意味がないです。