Lambda SnapStart を実現する MicroVM Snapshots の論文を読み解く

# 概要

本記事は、[aws Advent Calendar 2022](https://qiita.com/advent-calendar/2022/aws) の 3日目の記事となります。

* 今回は、先日(2022/11/28)発表された Lambda SnapStart を実現する MicroVM Snapshots について書かれた論文「Restoring Uniqueness in MicroVM Snapshots」を読み解いていくという内容になります。

https://aws.amazon.com/jp/blogs/aws/new-accelerate-your-lambda-functions-with-lambda-snapstart/

## Lambda SnapStart とは

* 一言でこの機能を表すのであれば、Lambda のコールドスタート、初期化処理によるレイテンシーを大幅に削減する仕組みです。

### 機能
#### Lambda の実行環境について

* Lambda は関数の実行が行われた場合、下記の図のような動きを行う。

1: S3バケットから実行するコードのダウンロードを行ったり、コンテナパッケージを使用している場合はECRよりダウンロード
2: 指定されたメモリ、ランタイム及び各種設定に基づいた環境を作成します。
3: Lambdaのハンドラ関数外の初期化コードを実行
4: Lambdaのハンドラ関数の実行

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3536333633352f63333333383234302d376638622d653332662d646366652d3562313066363363306133642e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/4dcfcd97-f4c5-0fd2-bbe5-77e5ed84269b.png)

* Lambda におけるコールドスタートとはこのコード準備と環境準備のステップを指します。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3536333633352f63303237613666622d643063392d346634372d323137652d6633656465366531356563632e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/d60758bf-3865-6a5a-2e8f-1d88c8e4c569.png)

##### ということは、毎回この一連の流れを実行している？？

**いいえ、そんなことはなく、実は、lambdaは必ずしもコールドスタートで始まるわけではありません。**

* Lambdaの実行が完了した後、Lambdaは実行環境を不特定期間保持します。
* その為、この保持されている期間内で同じ関数にリクエストがあった場合、Lambdaは環境の再利用を行うのです。
* これは、コールドスタートとは異なり、コールドスタートされていたステップ(コード準備と環境作成)が省略されるので、**ウォームスタート**となります。

つまり、**Lambdaは、関数実行後、その環境をすぐ破棄することなく保持します。**

**ただ、この環境破棄までの期間を指定することはできません。**

* そのため、Lambda を定常でウォームスタートさせるために例えば、 EventBridge を使い1分ごとに関数の呼び出しを ping で行うなどさせることで、常に Lambda を温めていた方もいるのではないでしょうか。
* これ、実は公式ブログ内でこの方法だとコールドスタートを減らす保証がないことが記載されています。
* あとお金もかかりますし・・


(※ 詳細は、下記をご一読ください。)

https://qiita.com/yoshii0110/items/18c15f7fd14ec18f8ad6

#### 改めて Lambda SnapStart の機能とは

* Lambda における「コード」の取得から「初期化」までの処理を予めやっておき、出来上がった実行環境のスナップショットを保存。Lambda の新規実行時、初回実行時にこの取得したスナップショットを使用することで処理時間を削減する機能になります。

* 旨味としては、**今回対応したランタイムが Java(Java11のみ) のみ**なのですが、特定の言語における初期化処理に時間がかかる場合などにとても便利なのではないでしょうか。(また、仕組み上特定のランタイムに依存するものではないので、今後他のランタイムも対応する気がしますね。)
* ちなみにこの機能どうやら利用料金は無料のようです。嬉しいですね！

##### 対応しているリージョン

* 米国東部 (オハイオ、バージニア北部)
* 米国西部 (オレゴン)
* アジアパシフィック (シンガポール、シドニー、東京)
* 欧州 (フランクフルト、アイルランド、ストックホルム)

## Restoring Uniqueness in MicroVM Snapshots

* さて、この発表があったときにやりたいことは分かったのですが、どうやって SnapStart を実現しているのか、とても興味が出てしまいました。
* Lambda SnapStart のリリースが発表された re:Invent の Keynote 中にさらっと説明がありましたが、どうやらこの仕組みについては Lambdaの実行基盤である、MicroVM の仕組みを使ったものであるようです。
* その仕組みについては、論文が出ていたため、読んでみました。

**※あくまで私個人の解釈で翻訳、記述、追加説明を行なっているので、その点はご容赦ください。※**

https://arxiv.org/pdf/2102.12892.pdf

## Abstract

* Lambda のコールドスタートでは、コードの初期化として実行するコードを読み込み、その実行環境の形成に支配されている。
* そんな初期化後のメモリスナップショットを起動時に複製、復元するソリューションが出たことにより、このコールドスタートに支配されていた問題が解決しました。
* なお、このソリューションは Firecracker などの VMM でサポートされています。

## Introduction

* Lambda がリクエストを受け取ると、実行する関数のコードのコピーが含まれた既に動いている microVM にリクエストを試みます。(ウォームスタート)
* ウォームスタートが成功しない場合は、システムはコールドスタートに戻ります。
* コールドスタート時は、Firecracker microVM の開始、実行するコードの読み込み、コードの初期化が行われます。
* 実は、コールドスタートにおける最後の処理が通常最も時間がかかるステップだとこの論文では伝えています。
* 下記の図1 は Lambda における microVM のライフサイクルにおける各ステップでかかる所要時間を表しています。

![スクリーンショット 2022-12-03 10.01.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/f757336b-7885-f325-de1e-cb8101b1ce95.png)

* microVM のブート処理、Firecracker 内の最小限の Linux カーネルのブート処理、最小限のユーザ空間デーモン、ネットワークの設定に 約 200 ミリ秒かかります。
* これはプリブートされた microVM のプールを使うことで我々ユーザからは隠されているようになっています。

* コールドスタート時
  * 次に、実際に実行される関数のコードを microVM 内にダウンロードする処理と初期化する処理に最大 60 秒かかることがよくあります。(小さなコードはもっと早い)
  * この内ダウンロードにかかる時間は早く、25Gb/s で Lambda の最大パッケージサイズ 250 MB でもダウンロードに 80 ms しか要しません。

* ウォームスタート時
  * 呼び出し時にルーティングによって起きる遅延で 10ms 以上かかります。
  * その後は同じ microVM が再利用されます。

**さて、ここで初期化にかかる時間に注目してみましょう。一連のライフサイクルの中で一番時間がかかっていますよね？**

* 初期化にかかる時間とリソースは、我々ユーザが選択した言語とフレームワーク、プロセス開始時の追加設定等に大きく依存します。
* 一般的な言語の中でも、Java は通常コールドスタートの時間がもっとも長くなります。
* これは、JVM 自体の起動時間、クラスコードの読み込み、読み込まれたクラスコードの実行によって引き起こされます。

**初期化後のスナップショット**

* この初期化の問題を解決するために、初期化後のスナップショットを使用します。
* 動きとしては、初期化が完了し、仮想マシンのスナップショットが作成され、コールドスタートが必要になると、スナップショットが複製され、環境が復元されます。
* 論文内の実験では、Firecracker スナップショットの復元にかかる時間は、わずか 4ミリ秒だったと書かれています。
* 下記の図は、スナップショットを使用したときの microVM のタイムラインを表しています。

![スクリーンショット 2022-12-03 11.07.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/0a48b9cb-8bd2-0ef1-0bba-8c30889e12e4.png)

## Challenges

* この仕組み、スナップショットから復元時、クローンを作成することでシステムレベルの課題がいくつか発生するようです。
　* ①: 仕組み上、スナップショットは RAM 内のデータをストレージ上のデータに変換します。
  * このデータには、慎重に取り扱いたい暗号化されたシークレットや、トークンなどが含まれている場合があります。
  * ②: クローンを作成するとメモリの状態も複製されます。
  * これは、リクエストID、UUID などの一意のデータを生成する必要があるアプリケーションにとって課題となります。
  * ③: クローンする処理は、TCP や TLS などのネットワークプロトコルと互換性がない。
  * なので、クライアントが単一の ID を持つ エンティティにより接続を再確立しようとするとリプレイ攻撃になりかねない動きとなります。

## The Value of Uniqueness

* 最新の分散システムでは、一意の値、ランダムな値の生成はノードの機能に依存する形となっています。
* UUID version 4 がよく利用されます。
  * UUID は、API 冪等性のためのトークンとしても使用され、同じトークンを使用した複数の呼び出しが単一の呼び出しとして動作します。
  * ID が重複すると、可用性や正確性の問題が発生する可能性がありますよね。
  * ID を変更せずに複製することは意図しない攻撃とみなされる可能性があるため、ほとんどの分散システムのプロトコルでは許容されません。

* 例えば、ストレージ暗号化などのアプリケーションが利用する寿命の長いキーであろうと TLS などのプロトコルの一時的なキーであろうと暗号化されたキーの生成に使用されるデータが予測可能だとよくないですよね。
* 同じキーで同じ データ(暗号化されたキーの生成に使用されるデータ) を複数回使用すると、これらのモードのセキュリティが損なわれます。
* なので、複製された MicroVM もこの特性を損なわないようにする手段が必要となります。
* VM の仕組み上、シードとなるデータ？仕組み？(entropyと論文では表現)は、ゲスト OS に注入され、/dev/urandom もしくは、システムコールを使用してアクセスできます。
* さて、ではスナップショットを取った時、この特性が担保されるのか。実は、既に昔からこの、VM スナップショットの再利用に依存する TLS 1.0 に対する成功した攻撃については発見されており、やはり危険性があるものだと言っています。

## Userspace Interfaces for Uniqueness

* 多くのアプリケーションは、カーネルまたはハードウェアからシードとなるデータ？(entropy)を直接取得せず、代わりにユーザー空間の疑似乱数ジェネレーター (PRNG) または決定論的ランダムビットジェネレーター (DRBG) を展開し利用します。
* しかし、サーバレスコンピューティングサービスを利用する場合、カーネルのランダム性の再シードなどのベストプラクティスに従い、顧客が暗号的に安全な PRNG (CSPRNG) の使用などのベストプラクティスに従っている場合でも、その組み合わせは安全ではありません。
* 暗号化の目的で一意性の問題を解決するには、復元時に新しい entropy でユーザー空間の PRNG を再実行できるメカニズムが必要です。

### Fork and MADV_WIPEONFORK

ここからは、がしがし Linux カーネルの仕組みを使いメカニズムについてを解説しています。

* まずは、madvise における MADV_WIPEONFORK という設定について。
* madvise()システムコール とは、アドレス addr からはじまる length バイトのメモリブロックのページング入出力をどう扱えば良いか、カーネルにアドバイスする役割をしています。
* このシステムコールに2017年に追加された MADV_WIPEONFORK を使うとサーバをforkした際に、機密性の高いプロセスごとのデータを確保するため PRNG シード、暗号化シークレットなどを子プロセスには渡さないようにできます。
* これは、fork() を実行した際に、プロセスのメモリが親プロセスと子プロセスの両方で複製されるため、ユーザ空間の PRNG にがっつり関係してくる問題となります。
* このようにシステムコールでの設定で回避するもしくは、fork() を実行した際に、カーネルもしくはハードウェアのランダム性から再シードさせる方法ができます。

### Suspend and MADV_WIPEONSUSPEND

* MADV_WIPEONFORK と同様に新しい madvise のフラグが提供されました。
* それが、MADV_WIPEONSUSPEND です。
* これは、VM が中断されたときにページを消去するようにマークするような動作となります。
* この時 PRNG と 暗号化ライブラリは状態変数として留まり、fork と VM clone の両方のケースで再シードするロジックすることを期待します。

* 復元ではなく、中断による消去によるメリットは2つあります。
  * 1: パフォーマンス的な側面。サーバレスのユースケースでは、中断時にメモリワイプを処理することで復元する際の遅延を削減できること。
  * 2: セキュリティ的な側面。キー管理サービスやハードウェア セキュリティ モジュール（HSM）から意図的にシークレットとなる情報を取得したい場合に有効的ということ。

### System Generation ID

* Hyper-V ハイパーバイザの Windows Server 2012 バージョンで、VM Generation ID (VmGenId) が導入されました。
* ゲスト OS は、VmGenId の変更によって、クローン作成や復元などの重要な変更が発生したことを検出します。
* その後、System Generation ID (SysGenId) と呼ばれる、Linux カーネルと同様のメカニズムのサポートを提供しました。
* SysGenId というのは、「system generation id」機能として、VM がスナップショットから復元されるたびに変化する単調に増加する u32 カウンターを提供するものです。

### Alternative approaches

* 実は、もう一つ別のアプローチとして、暗号化アルゴリズムのレベルで解決したものがあります。
* Rogaway と Shrimpton は、最も強力なセキュリティ特性を得るために、ランダムに依存しない認証済み暗号化モードを提案しました。
* Gueronらが提案し、標準化した AES-GCM-SIV は、Nonce の misuseresistant モードを提供します。
* これは、わずかなパフォーマンスオーバーヘッドで、IV の再使用の場合に機密性と認証プロパティを保持するそうです。

### Atomicity and TOCTOU

* ここまでに記載していった、MADV_WIPEONSUSPEND オプションと SysGenId オプションの両方が、RNG ライブラリが複数の複製された VM で重複した乱数を生成しないことを保証するための十分な仕組みを提供していることをお伝えしました。
* SysGenId の場合ですと、復元操作の前に SysGenId の更新を行う必要があります。
* この順序に違反しないようにするのは、VMM とゲストカーネルの役目となります。
* Lambda などは、実行中の VM で進行中のリクエストを追跡し、VM がリクエスト処理のコンテキスト外でアクションを実行できないようにしています。
* ただ、これらのメカニズムのいずれも一般に、作成時間(TOCTOU)の問題により、再使用を防止するのに十分ではないようです。
* 生成された値をアトミックに生成、使用、および破棄しないプログラムでは、生成と使用するまでの間、または使用から破棄の間のクローン操作によって再利用が発生する可能性があります。
* このような複雑な操作の原子性は、一般的にサポートされていません。
* 接続が確立または使用される前、ストレージが変更される前、またはシステムにその他の副作用が発生する前に再シードが完了していることを確認するには、環境によって提供される**外部の仕組み**が必要です。
* 外部の仕組みとしては、ロードバランサやコンテナオーケストレータのヘルスチェックが該当します。

## Conclusion

* スナップショット、クローン、および復元は、サーバーレスコンピューティングプラットフォームのコールドスタートを短縮するための便利で強力仕組みです。
* ただ、これらの仕組みは便利な反面問題もあることを述べてきました。
* そして、Linux と Lambda のコンテキストでこの問題に取り組むために提案した 2 つのカーネルインターフェイスについてを説明していました。
* そのうちの 1 つは Linux カーネルで提供されています。

# 参考文献

https://arxiv.org/pdf/2102.12892.pdf

https://aws.amazon.com/jp/blogs/aws/new-accelerate-your-lambda-functions-with-lambda-snapstart/