.NET 8 の GA が 2 カ月後に迫ってきたこのタイミングで、Azure Functions での .NET 8 向けアップデートが本格的に発表され始めました。そろそろ .NET 6/7 で Isolated を利用しているケースではアップデートを検討しても良いでしょう。.NET 6 の In-Process を利用しているケースは後述しますが急ぐ必要はありません。
そして .NET 8 とは直接関係ないですが、.NET 8 から主流になるはずだった Isolated Worker モデルの改善が同時に公開されました。ようやく In-Process に近い開発体験が得られるようになってきましたが、正直パフォーマンスと信頼性の面ではまだまだという印象です。
3 updates to the isolated worker model for .NET apps: GA of https://t.co/t2t4rHmXre Core integration, preview of new performance optimizations, and preview of .NET 8 support on Linux!https://t.co/ELa6Gs1avkhttps://t.co/5A1wAa3Fi7https://t.co/rfKuMrxkyJ
— Azure Functions (@AzureFunctions) 2023年8月30日
しかしアップデート内容としては興味深いものがあるので .NET 10 での Isolated への移行を見越して確認しておきました。.NET 10 がリリースされる頃には十分成熟したものになる予感です。
.NET 8 Isolated への対応が Preview
今回のアップデートのメインはこれです。Azure Functions v4 で .NET 8 Isolated への対応が Preview として公開されました。Azure Functions Core Tools のバージョン 4.0.5312
以降がインストールされていれば、プロジェクト作成時に net8.0
が選べるようになっています。
Visual Studio Code では最新の Azure Functions 拡張と .NET 8 SDK がインストールされていれば、コマンドパレットから新規プロジェクト作成時に .NET 8 Isolated Preview が選べるようになります。
Visual Studio 2022 ではバージョン 17.8 Preview と .NET 8 SDK がインストールされていて、最新の Azure Functions のツールセットとテンプレートに更新済みであれば、プロジェクト作成時に .NET 8.0 Isolated が選べるようになります。17.7 でも .NET 8 SDK が入っていれば選べるかもしれませんが未確認です。
Visual Studio 2022 の場合は WinGet などでインストールした Azure Functions Core Tools が使われず、Visual Studio が管理しているバージョンが使われるようになっているので、選択できない場合にはオプションから Azure Functions のツールセットとテンプレートを更新しておきます。
意外に知られていないので、ワークショップなどで Visual Studio 2022 を使って Azure Functions 開発を行う際には、事前にチェックしておいた方が良いですね。意外にダウンロードに時間かかることがあります。
Azure Functions チームのツイートでは Linux で .NET 8 Preview のサポートが追加されたとありますが、既に Early Access で Windows でも利用可能になっています。
これまでの Early Access と同様にコールドスタートのパフォーマンスはあまりよくないですが、GA すると自動的に最適化されるようになっています。
.NET 8 での In-Process サポートが追加予定
以前のロードマップでは .NET 8 からはこれまで主流だった In-Process のサポートがなくなり、既存の Azure Functions は全て Isolated Worker への移行が必要となるはずでしたが、最新のロードマップでは .NET 8 でも In-Process サポートが追加されることになりました。
但し .NET 8 が GA されてすぐに対応するのは Isolated Worker だけで、In-Process のサポートは少し間が空くようです。しかし .NET 6 のサポートは来年まで続くので多少の遅れは問題になりません。
Semantic Kernel が最新バージョンでは Isolated を選ばないと動作しないことは以前お話ししましたが、アセンブリのバージョン問題が解消されるため .NET 8 の In-Process がリリースされると問題なく利用できるようになります。.NET 8 が最新のうちは安定して動作するはずです。
ただし .NET 9 がリリースされると .NET 5 がリリースされた時と同様に、またバージョン問題が発生するので流石に .NET 10 では Isolated への移行を真剣に検討したいと思います。
少なくとも Isolated Worker のパフォーマンスと信頼性が高まり、本番での運用が可能なレベルに達するまでの時間は十分あるはずです。それを待ってからでも遅くないでしょう。
ASP.NET Core Integration が GA
Isolated でも In-Process と同じように ASP.NET Core 関連クラスを使い、簡単に HttpTrigger
を実装できる ASP.NET Core Integration が GA しました。
パッケージのバージョンは 1.0.0
になっていますが中身は 1.0.0-preview4
と同じです。
利用方法については以下のエントリで書いたので、気になる方はこちらを参照してください。
ASP.NET Core Integration と同時に Azure Functions Host 側にも新しい HTTP パイプラインが実装されたので、まだ安定性の面では不安が残る部分がありますが、ストリーミング周りが正しく動作するようになったので、パフォーマンス面では有利になってきます。
コールドスタートの最適化が Preview
Azure Functions の永遠の課題でもあるコールドスタートの高速化ですが、構造上 In-Process よりも不利な Isolated Worker について複数の最適化が追加されました。ドキュメントに利用方法がまとまっています。
今回追加されたのは Placeholders と Optimized executor の 2 つになります。ReadyToRun は以前から利用可能で、何なら In-Process でも有効化出来るものです。
- Placeholders (Preview)
- Optimized executor (Preview)
- ReadyToRun
ReadyToRun は既に何回も触れているので、詳細は以下のエントリを参照してください。
ここからは Placeholders と Optimized executor の 2 つについて確認した範囲で書いていきます。現時点でも効果の測定中ですが、今のところそんなに大きな違いはなさそうです。
Placeholders について
Placeholders は Azure Functions の Consumption Plan に用意されている機能で、予め Azure Functions の Runtime をユーザーコードが入っていない状態でプロビジョニングしておいて、コールドスタートにかかる時間を短縮するものです。
仕組みについて公式な説明はないのですが、Azure Functions チームの牛尾さんが Linux については仕組みを簡単に書いているので、気になる方はこちらを参照してください。Windows も同じような仕組みです。
既存の In-Process の Azure Functions ではデフォルトで有効化されているので、Consumption Plan を使っている場合には意識することなく恩恵に預かっています。ちなみに設定でオフにすることも可能です。
Placeholders が有効化されていると、環境変数などで特殊なパスに展開されているのが確認できます。Placeholders で起動されて、CPU bitness まで専用に確保されているのが分かりますね。
このような仕組みなので Isolated でも Placeholders を有効化することで、コールドスタートにかかる時間の短縮が期待できるわけです。ただし新しくプロセスを立ち上げる必要はあるので、その部分で時間がかかってしまうとあまり効果は出ないです。
Optimized executor について
今回のアップデートのメインは Optimized executor だと考えています。理由としては Isolated じゃないと不可能な最適化なのと、将来的には Native AOT によって劇的なコールドスタートの改善が期待できるからです。
具体的には C# の Source Generator を利用して、リフレクションを使って実行時に Function を初期化して呼び出すのではなく、ビルド時に Function の初期化と呼び出すコードを生成してしまう仕組みです。そのためリフレクションのコストを無くせるので効率的になります。
Optimized executor の設定を有効化してビルドしたアセンブリを dotPeek などで確認すると、DirectFunctionExecutor
というクラスが含まれていることが分かります。
実装を見るとリフレクションを使って Function の初期化と呼び出しを行うのではなく、特定の Function 名の場合は実装されたクラスをインスタンス化して、メソッドを直接呼び出すというコードが生成されています。
外部から文字列で与えられた値で、任意のメソッドを呼び出すのは結構な高コストなので、最近はこの分野では Source Generator を使うケースが増えていますね。
面白い点としては IFunctionExecutor
を実装して DI に登録すれば、自由に Function 実行部分をカスタマイズできるようなので、独自に最適化するという方法も取れそうです。
当然ながら開発チームとしても Native AOT も視野に入れているようなので、以下のような Issue で Native AOT に必要なタスクがまとめられています。
Native AOT が有効化されるとコールドスタートは劇的に改善するはずなので、出来れば .NET 10 ぐらいまでには実装されていて欲しいですね。