.NET Lambda(Amazon.Lambda.AspNetCoreServer)で SnapStart を試してみた
いわさです。
先月のアップデートで Lambda の SnapStart 機能が .NET でも遂に使えるようになりました。
Java で先行して使えるようになってから 2 年くらい経ちましたので、遂にという感じです。
.NET もやはり Java と同じように、他の Lambda ランタイムよりもコールドスタートが問題になりやすく、SnapStart をぜひ使いたいとずっと思っていました。
Java と異なり Python と .NET の SnapStart は追加料金が少し発生するのですが、まずはどの程度効果があるのか試してみたいところ。
Amazon.Lambda.AspNetCoreServer というものを導入すると、ASP.NET Core をモノリス Lambda 化して使う方法ことが出来ます。
今回はこれに SnapStart を効かせて何がどの程度変わるのか観察してみたいと思います。
素の Amazon.Lambda.AspNetCoreServer のコールドスタート時間を計測
まずは ASP.NET Core のモノリス Lambda を作成してデプロイし、SnapStart なしの状態で実行にどの程度の時間がかかるのか見てみます。
Amazon.Lambda.AspNetCoreServer については以前紹介したことがありまして、詳しくは以下の記事などを参考にしてください。
上記実行すると、.NET 8 の API Gateway + Lambda がデプロイされます。
アプリケーションコードでは、テンプレートから初期化するとデフォルトで次のようなコントローラーが用意されています。
どうやら四則演算をしてくれるようです。
using Microsoft.AspNetCore.Mvc;
namespace hoge1129dms.Controllers;
[ApiController]
[Route("[controller]")]
public class CalculatorController : ControllerBase
{
private readonly ILogger<CalculatorController> _logger;
public CalculatorController(ILogger<CalculatorController> logger)
{
_logger = logger;
}
/// <summary>
/// Perform x + y
/// </summary>
/// <param name="x">Left hand operand of the arithmetic operation.</param>
/// <param name="y">Right hand operand of the arithmetic operation.</param>
/// <returns>Sum of x and y.</returns>
[HttpGet("add/{x}/{y}")]
public int Add(int x, int y)
{
_logger.LogInformation($"{x} plus {y} is {x + y}");
return x + y;
}
/// <summary>
/// Perform x - y.
/// </summary>
/// <param name="x">Left hand operand of the arithmetic operation.</param>
/// <param name="y">Right hand operand of the arithmetic operation.</param>
/// <returns>x subtract y</returns>
[HttpGet("subtract/{x}/{y}")]
public int Subtract(int x, int y)
{
_logger.LogInformation($"{x} subtract {y} is {x - y}");
return x - y;
}
:
で、API Gateway 側ですが Proxy リソースで全リクエストを Lambda に投げてます。
そして AspNetCoreServer 側で、ひとつの Lambda でルーティングする感じになってます。モノリスです。
ということでデプロイ後に API Gateway の Prod ステージエンドポイントにリクエストを送ってみましょう。
% curl https://kzclc861ok.execute-api.ap-northeast-1.amazonaws.com/Prod/calculator/add/10/15
25
% curl https://kzclc861ok.execute-api.ap-northeast-1.amazonaws.com/Prod/calculator/subtract/10/15
-5
なるほど。計算結果を取得出来ました。
トレースを観察
AspNetCoreServer テンプレートをデプロイするとデフォルトでは有効になっていないのですが、API Gateway と Lambda の X-Ray アクティブトレースを有効化し、トレースデータを確認してみます。
コールドスタートが発生する初回は 1.265 秒かかっているみたいです。
その後のウォームな状態は 0.02 ~ 0.04 秒というところ。
コールドスタートが発生しているリクエストのセグメントタイムラインを確認してみましょう。
X-Ray のセグメントを確認するとリクエストタイムラインの内訳を確認出来ます。デフォルトだとこの粒度ですが、アプリコードをカスタマイズすることでより細かいサブセグメントの実装も可能です。
なるほど。Lambda 関数全体で 1.20 秒かかっており、そのうち 0.474 秒が Init フェーズです。
うまくセグメントとして表示されていないですが、Init 後の普通の実行部分で時間かかっていそうですね。
ウォーム状態だとこんなに時間がかかっていませんでしたので、おそらく JIT か DLL のロードあたりでしょうか。これは SnapStart で治らない気がしてきました。
SnapStart 化
とはいえ SnapStart の効果も見てみたいのでとりあえず試してみましょう。
SnapStart は Lambda の一般設定で有効化します。
$LATEST では SnapStart 設定がPublishedVersions
となっていますね。
SnapStart は、非バージョニングの $LATEST で動作するわけではなく、バージョンを発行することで対象バージョンが SnapStart ON として使えるようになります。
バージョン発行すると次のように SnapStart 最適化ステータスが On になっていることが確認出来ます。
ということで、API Gateway の統合先もバージョンあるいはエイリアスを指定する必要があります。
デフォルトではバージョン指定なしの $LATEST になっていましたのでここだけ直してステージにデプロイしましょう。
トレースを観察(SnapStart 版)
先ほどと同じようにトレースのセグメントタイムラインを観察してみましょう。
まず、Init フェーズがなくなり Restore フェーズが追加されていることがわかりますね。これは SnapStart 機能によって Init 済みのスナップショットから復元されていることを示しているので .NET Lambda で SnapStart がうまく動いていることは確認できました。
Init フェーズで 0.474 秒かかっていたので、Restore フェーズとなり 0.367 秒となっています。少しだけ...速くなってるけど。
Init 処理がモリモリで数秒かかるようなものであれば、もう少し効果出ると思います。
で、やはり実行の Invocation 部分は 0.559 秒と、先程と同じ程度かかっていますね。
.NET の場合は Init フェーズ以外でも初期実行時は時間がかかりますね。特に今回のようなモノリス Lambda だと、ルーティング処理などの共通処理がハンドラに含まれていそうなのでその分 .NET の初期実行のオーバーヘッドが発生していそうです。
当然ながら 2 回目以降は SnapStart 有効化前と同じで速いです。
さいごに
本日は .NET Lambda(Amazon.Lambda.AspNetCoreServer)で SnapStart を試してみました。
まず、.NET Lambda でも SnapStart が使えることを実際に確認することが出来ました。設定も簡単ですね。
ただし、必ずしも SnapStart で .NET のコールドスタートが改善できるかというとそうではないこともわかりました。
どうやら Init フェーズ以外でもコールドスタート時にはいくつか時間がかかる要素があるようです。
JIT あたりは、以前紹介させて頂いたことのある Native AOT を併用すると改善される気もします。re:Invent 終わったら試してみますね。