FirecrackerでのFreeBSD
(usenix.org)- AWS Lambda向けの Linux 中心の microVM である Firecracker VMM 上で FreeBSD 14 カーネルを起動する取り組みは、最小限の仮想化環境が OS 初期化経路に潜む前提やボトルネックをあぶり出した事例だった
- 最初の起動は、PVH boot mode の ELF Note 形式の違い、Xen 専用ハイパーコール、Firecracker のメモリ配置の違いによって阻まれたが、FreeBSD の PVH コード修正で解決した
- Firecracker が ACPI を提供しないため、CPU・割り込み情報を取得する経路が変わり、Linux の MPTable 処理バグに合わせるための
MPTABLE_LINUX_BUG_COMPATオプションが追加された - シリアルコンソールと Virtio デバイス接続の過程で UART 動作、カーネルコマンドライン解析、非整列ディスク I/O 制約が明らかになり、FreeBSD は
hw.broken_txfifo、FIFO クリア回避、busdmaベースの bounce 処理を適用した - 未コミットのパッチを含めると、1 CPU・128MB RAM の VM で FreeBSD カーネルは 20ms 未満で起動し、残る課題は PVH サポートのマージ、Xen コードの分離、より小さなカーネル構成、Firecracker の FreeBSD 移植検討である
Firecracker に FreeBSD を載せようとした理由
- Firecracker は AWS Lambda のようなサーバーレス実行環境向けに、Linux KVM 上で低オーバーヘッドの microVM を作成・管理する VMM である
- FreeBSD への移植作業は 2022 年 6 月に開始された
- 取り組みの動機は FreeBSD と Firecracker の両方の限界をあわせて確認することにあった
- FreeBSD の起動速度改善作業を続ける中で、最小ハイパーバイザー上でどこまで速くできるかを確かめたかった
- FreeBSD を新しいプラットフォームへ移植すると、FreeBSD 側とそのプラットフォーム側の両方のバグが露呈する
- AWS Lambda は現在 Linux のみをサポートしており、Lambda を採用するかどうかとは別に、FreeBSD が Firecracker をサポートすることは必要な前提条件である
- Firecracker 自体が興味深いプラットフォームなので、実際に動作するかを確認したかった
カーネル実行までの最初の障害
- Firecracker はもともと Linux カーネル実行向けに設計されたが、2020 年に
linuxbootに加えて PVH boot mode をサポートするパッチがあった - FreeBSD は Xen で PVH 起動をサポートしていたため、同じ経路を Firecracker で試した
- 最初の問題は、Firecracker が FreeBSD カーネルをメモリに載せた後で kernel entry point を見つけられなかったことだった
- PVH ブートプロトコルはこの値を ELF Note に格納する
- ELF Note には
PT_NOTEとSHT_NOTEがあり、FreeBSD は Firecracker が探していた形式を提供していなかった - FreeBSD カーネルのリンカスクリプトを少し修正すると、Firecracker は FreeBSD カーネルの実行を開始できるようになった
- しかしカーネル実行は約 1 マイクロ秒後に再び停止した
初期デバッグと Xen 依存の除去
- カーネルデバッガやシリアルコンソールが初期化される前にカーネルが落ちると、FreeBSD のデバッグ機能はほとんど役に立たなかった
- Firecracker プロセスから得られた情報は、FreeBSD ゲストが triple-fault を起こしたという事実だけだった
hlt命令をカーネル開始コードの途中に挿入して、クラッシュ位置を絞り込む「kernel bisection」を行ったhltに到達すると Firecracker は動き続けるが、ホスト CPU 使用率は 0% になる- Firecracker が終了した場合、その地点より前でクラッシュしたと判断できた
- 最初の原因は Xen hypercall だった
- FreeBSD の PVH エントリポイントは実際には Xen 上で起動するためのコードであり、Xen 内で実行されることを前提にしていた
- Firecracker が利用する KVM は Xen ハイパーコールを提供しないため、呼び出すと VM がクラッシュした
- 当初は Xen ハイパーコールをコメントアウトし、その後
CPUIDで Xen シグネチャを確認した場合のみ呼び出すよう修正した
- 物理メモリマップの取得は、本来 Xen ハイパーコールが担っていた必須機能だった
- PVH version 1 以降では、メモリマップポインタは
start_infoページを通じて渡される - FreeBSD は Xen ハイパーコールの代わりに PVH version 1 のメモリマップを使うよう変更された
- PVH version 1 以降では、メモリマップポインタは
- Firecracker と Xen のメモリ配置の違いも問題を引き起こした
- Xen はまずカーネルをロードし、その末尾に
start_infoページを配置する - Firecracker は
start_infoページを固定の低位アドレスに置き、その後でカーネルをロードする - FreeBSD の PVH コードは
start_infoの直後の領域を scratch space と仮定しており、Firecracker では初期カーネルスタックを上書きしてしまった - ハイパーバイザーが初期化したすべてのメモリ領域の後ろに scratch space を割り当てるよう修正して解決した
- Xen はまずカーネルをロードし、その末尾に
ACPI 不在と MPTable 互換性
- x86 上の FreeBSD は通常、ACPI を通じてディスク、ネットワークアダプタ、CPU、割り込みコントローラの情報を取得する
- Firecracker は意図的に最小実装を採用しており、ACPI を提供しない
- その代わり FreeBSD は、旧来の Intel MultiProcessor Specification の MPTable 構造を利用できる
GENERICカーネル構成には標準では含まれていない- Firecracker 向けの軽量カーネル構成では
device mptableを追加して利用できた
- Firecracker が提供する MPTable は標準どおりではなく、Linux が受け入れる方式に合わせられていた
- Linux には MPTable の探索と解析方法にバグがあった
- Firecracker は Linux の起動を目標に設計されているため、Linux が対応する非標準配置を提供する
- 標準に沿って独立実装された FreeBSD は、不正な場所に置かれた MPTable を見つけられず、見つけても無効な MPTable を解析できなかった
- FreeBSD には
options MPTABLE_LINUX_BUG_COMPATカーネルオプションが追加された- Linux の MPTable 処理との bug-for-bug 互換性が必要なときに使う
- このオプションにより Firecracker 上の FreeBSD 起動はさらに先へ進んだ
シリアルコンソールと Virtio デバイス対応
- Firecracker が提供する数少ないエミュレーションデバイスのひとつが シリアルポート である
- 一般的な構成では、Firecracker プロセスの標準入出力が VM のシリアルポート入出力になる
- FreeBSD カーネルはルートディスクをカーネルイメージに含めた状態で起動し、カーネルコンソール出力までは読めるようになった
- ユーザー空間の起動に移ると、コンソール出力が 16 文字で止まった
- これは過去の QEMU の UART バグと同じ症状だった
- UART の transmit FIFO が空になっても割り込みが来ず、FreeBSD は 16 バイト以降を書き込めなかった
- FreeBSD カーネルの既存の回避策
hw.broken_txfifo="1"をカーネル環境変数としてコンパイルして解決した
- コンソール入力も動作しなかった
- Firecracker は、エミュレーション UART の receive FIFO が満杯だと判断してコンソールを読まなかった
- FreeBSD は UART 初期化時に FIFO サイズを測るため、receive FIFO をゴミ値で満たしてから FIFO Control Register でクリアする
- Firecracker は FIFO Control Register を実装していないため、FIFO は満杯のまま残った
- FreeBSD は FIFO クリア後にも
LSR_RXRDYが残っている場合、文字を 1 つずつ読み捨てて FIFO を空にするよう修正された
- ディスクとネットワークを使うには Virtio block/network デバイスが必要だった
- Firecracker はこれらを
mmioデバイスとして公開する - FreeBSD の Firecracker 用カーネル構成には
device virtio_mmioが追加された
- Firecracker はこれらを
- FreeBSD はもともと
mmioデバイスを FDT で発見することを想定していたが、Firecracker はカーネルコマンドラインでvirtio_mmio.device=4K@0x1001e000:5のような指定子を渡す- FreeBSD はこの指定子を解析して
virtio_mmioデバイスノードを作成するコードを追加した - デバイスノードができると、既存のデバイス probe 処理が Virtio デバイス種別を判定し、適切なドライバを接続する
- FreeBSD はこの指定子を解析して
- 複数の Virtio デバイスがある場合、カーネルコマンドライン解析に問題があった
- Firecracker は Linux 方式どおりに
key=valueの組を複数渡す - FreeBSD はカーネルコマンドラインを環境変数として解析するため、同名の
virtio_mmio.device=が 2 つあると 1 つしか残らなかった - 初期カーネル環境解析コードは、重複変数に番号サフィックスを付けて
virtio_mmio.device=とvirtio_mmio.device_1=のように保持するよう修正された
- Firecracker は Linux 方式どおりに
- 異常終了の次の起動で
fsckが実行されると、カーネルパニックが発生したfsckは FreeBSD でページ整列されていないディスク I/O を発生させる珍しいケースのひとつである- Firecracker の Virtio 実装は単一のデータバッファしか受け付けず、ページ境界をまたぐバッファを複数セグメントに分割する一般的な Virtio 方式をサポートしない
- FreeBSD の Virtio block ドライバは
busdmaを使うよう修正され、非整列リクエストは一時バッファを経由する bounce 処理で Firecracker の制約に合わせた
Firecracker があぶり出した起動最適化
- FreeBSD が Firecracker 上で動き始めると、起動時間とメモリ使用量を減らせる箇所が明確になった
- 128MB RAM の VM では、システムメモリのほぼ半分が
wired状態になり、プロセスが頻繁に終了した- 調査の結果、
busdmaが bounce page 用に 32MB を予約していた - Firecracker の制約では、各ディスク I/O に必要なのは最大でも 4KB の bounce page 1 枚だけだった
- 少数の I/O セグメントしかサポートしないデバイス向けに bounce page 予約を制限するパッチにより、メモリ使用量は 512KB に減少した
- 調査の結果、
- カーネル乱数生成器の最適化で起動時間が短縮された
- VM ではハードウェアデバイス由来のエントロピーが効果的でないことがある
- x86 の
RDRANDをバックアップのエントロピー源として使っていたが、1 回あたりのエントロピー量が少なく、100ms ごとに 1 回しか要求していなかった - Fortuna 乱数生成器を完全に seed できる量を要求するよう変更し、2.3 秒短縮した
- Host ID の処理も高速化された
- 通常はブートローダが BIOS または UEFI 情報に基づいて
smbios.system.uuidを設定する - Firecracker にはブートローダがないため、ID は提供されない
- ハードウェアが不正な ID を提供した場合は警告後に 2 秒待機し、ID がまったくない場合は静かにすばやく進むよう修正された
- 通常はブートローダが BIOS または UEFI 情報に基づいて
- IPv6 DAD の待機条件も絞り込まれた
- FreeBSD はネットワークインターフェースで IPv6 が有効だと Duplicate Address Detection を待っていた
- loopback インターフェースでは常に IPv6 が有効になっていた
- loopback 以外のインターフェースで IPv6 がある場合のみ DAD を待つよう変更し、2 秒短縮した
- 再起動と終了処理の固定待機時間も取り除かれた
- 再起動時に
Rebooting...メッセージの後、printf完了と読み取り時間のために 1 秒待っていた動作はkern.reboot_wait_timesysctl に変更され、デフォルト値は 0 になった - 終了または再起動時に BSP が他 CPU の停止シグナル受信後、追加で 1 秒待っていた動作も削除された
- 再起動時に
- TSLOG を使って起動 flame chart を解析した
- Firecracker の最小環境はノイズが少なく、残るボトルネックを見つけやすかった
- VM の実行が非常に速いため、新カーネルのビルド、実行、flame chart 生成までがしばしば 30 秒未満で可能だった
- TSLOG 解析により、複数のミリ秒単位ボトルネックが削減された
lapic_initの 100000 回の較正ループを 1000 回に減らして 10ms短縮したns8250_drainが文字ごとにDELAYを呼んでいた部分を、LSR_RXRDY確認後に必要な場合のみ遅延するよう変更し、27ms短縮した- Firecracker が TSC と local APIC のクロック周波数を通知する CPUID leaf を実装することで 20ms短縮した
kern.nswbufを常に 256 にしていた方式を32 * mp_ncpusに変更し、1 CPU VM で 5ms短縮したmi_startupの bubblesort を quicksort に置き換えると 2ms短縮でき、2023 年 8 月 22 日時点ではまだコミットされていないvm_memが全物理メモリのvm_page構造体を即時初期化していた方式を lazy 初期化に変えると 2ms短縮でき、同日時点ではまだコミットされていない- Firecracker の guest memory
mmapにMAP_POPULATEを追加すると、最初のページアクセス時に Linux がページ構造を作るコストを減らせて 2ms短縮でき、同日時点ではまだコミットされていない
現在の状況と残る作業
- FreeBSD は Firecracker 上で起動し、非常に高速に動作する
- FreeBSD と Firecracker の未コミットパッチを含めれば、1 CPU・128MB RAM の VM で FreeBSD カーネルは 20ms 未満で起動できる
- 残る作業は PVH サポートの整理とカーネル構成の縮小に集中している
- 上で触れたパッチをコミットする必要がある
- PVH boot mode サポートを Firecracker mainline にマージする必要がある
- PVH booting コードは Xen サポートと混在しているため、これを分離する必要がある
- FreeBSD arm64 カーネルは現状 PCI または ACPI サポートなしではビルドできず、この誤った依存関係を除去すれば FreeBSD/Firecracker カーネルをさらに小さくできる
- Intel GPU 向けメモリ予約の必要性確認に 25µs を使っているため、より小さなカーネル構成なら起動時間をさらに数マイクロ秒削減できる
- より長期的には、Firecracker 自体を FreeBSD 上で動かすよう移植する可能性もある
- Firecracker は Linux KVM の利用を前提に書かれている
- FreeBSD の bhyve ハイパーバイザーのカーネル部分を使うようにできない根本的理由はないと考えられている
- 実験するには、FreeBSD 14.0 カーネルを amd64 の
FIRECRACKERカーネル構成でビルドし、Firecracker プロジェクトのfeature/pvhブランチ を使える- もしそのブランチがすでに存在しないなら、コードが mainline Firecracker ツリーにマージされたことを意味する
1件のコメント
Hacker News のコメント
Firecracker VM が単なる Linux コンテナ技術ではなく、完全な仮想マシンだということをよく分かっていなかった
最初は非効率に聞こえるかもしれないが、fly.io のような実際のユースケースを見ると、マイクロ VM が非常に小さく、それでいて十分に強力なので驚く
KVM と最小限のハードウェアサポートのおかげで(PCI、ACPI なしなど)、Firecracker のソースはかなり単純で、専門家でなくても比較的読める
Firecracker が存在する理由はまさにそこにある
記事で挙げられている ACPI が一例。それでもカーネルではなくハードウェアを仮想化しているのは確かで、初期化が非常に速いので、たいていのユーザーは通常の containerd を firecracker-containerd に置き換えても違いに気づかないと思う
Firecracker 以外にも crosvm、cloud-hypervisor、Kata の Dragonball など、いくつものマイクロ VM が今 KVM 上で開発されている
プログラミング言語に新しいターゲットを実装するのがそこまで難しいという印象はないので、WASI/WASM のような疑似 OS ターゲットを作り、対応言語に PR を送れば、オーバーヘッドの大半を減らせそうだ。最も難しい部分は Linux ユーザー空間を十分正確に模倣することだろうが、表面積が非常に広いので、むしろ疑似 OS ターゲットを作る経路がいちばん良さそうに見える
Colin のパッチが FreeBSD と Firecracker に入れば、カーネル全体の起動時間が 20ms 未満になる
本当に信じがたい時代に生きている
少し検索すれば数字は出てくるが、数年前の資料で、起動時間の測定方法が同じなのか、「起動時間」の定義が同じなのかも不明なので、比較できるのかよく分からない
数日前に上がっていた Colin の最近の BSDCan 発表
https://youtu.be/MT3cdeuRTzs?si=l6baNriUjcvy0ZOE
BSDCan の発表後、FreeBSD Journal から「良い発表だったので記事にしてもらえないか」と言われ、FreeBSD Journal の記事が出た後には、;login: から再掲載してよいか尋ねられた
qemu には firecracker に影響を受けた microvm がある
https://qemu.readthedocs.io/en/latest/system/i386/microvm.ht...
もちろん一部は FreeBSD のバグ修正なので、必要にならざるを得ない
1秒待機のかなり多くが、実は必ずしも必要ではないと分かるのが面白い
間違ったマシン UUID のせいでシステムが停止したとき、実際に意味のある対応をしたシステム管理者がどれほどいたのだろうと思う
一方で「ユーザーに再起動するとメッセージを表示し、コンソールを読めるように 1 秒待ってから再起動する」という方は少し違う
偉そうに聞こえたいわけではないのだが、Firecracker インスタンスのようなものはどんなユースケースに合うのか気になる
FreeBSD をコロケーションサーバーから個人 PC まで全部で使っていて、開発者というよりは古参の Unix 管理者に近い。ベアメタルを好むが、OS に貢献する未来技術は歓迎する。ただ、Lambda や Firecracker のようなバズワードを聞いても、実際にどこで使われるのかよく分からない。Docker とコンテナは理解しており、k8s は何とか理解している程度だが、普通に VM を立ち上げて必要なときに使えばいいのに、なぜ VM を立ち上げてすぐ消す必要があるのか分からない。純粋にクラウド体験やコスト削減のためなのだろうか?
こうすると、コンピュートプレーンのどのノードでも、どのアプリケーショントラフィックでも処理できる。1 つのアプリケーションは、トラフィックパターンの変化に応じてプレーンの余剰コンピュートリソースを動的に消費する形で大きくなれ、トラフィックを処理していないときはリソースを使わない。コンピュートプレーンの容量を増やすとは、より多くのノードをオンラインにすることだ。大規模デプロイを大量に管理すること以外には、特にユースケースは思い浮かばず、「規模」のない環境ではベンダーの境界の下に隠れている技術だろう
API が頻繁には使われないが、呼び出されたときには素早く応答する必要があるサービスを運用しているなら、Lambda や似た方式がよく合う。実際、携帯電話アプリ向け API のかなり多くがこの範疇に入り、そうした API 呼び出しに応答するために、99% の時間アイドル状態のマシンを置き続けたくはない
たいていは節約額より手間の方が大きいのでやらないだけで、潜在力はある。Lambda や Firecracker のようなものが、それをはるかに簡単にしてくれる
AWSもARM版macOSもネストされた仮想化をサポートしていないのが惜しい
サポートしていれば、Firecrackerベースの技術を開発してデプロイするのがはるかに簡単だったはず
実際にはどのインスタンスタイプでも仮想化自体は可能だが、ハードウェアアクセラレーションを使えるのは.metalインスタンスだけだと理解している
Firecrackerは驚くべきものだが、ドキュメント化が必要な例外ケースが多い
Colin Percivalがこれを共有してくれたことに大いに感謝している。特に「低く実った果実を全部摘み取った後」という一文が気に入っているが、Colinにとってそれはカスタムのbus_dmaパッチ群を意味する。今では誰もが「CPU 1個とRAM 128MBでFreeBSDカーネルが20ms未満で起動する」ことを無料で享受できる。k8sクラスタや大量のDockerを使うDevOpsに慣れているなら、本当に驚くべきことだ
Firecrackerを少し触ってみたが、起動時間は約束どおり出る一方で、利用体験はかなり険しい
例えば起動に成功して歓喜した後、ネットワーク設定をするにはまた長いチュートリアルに従う必要があると知って気力が削がれた
単一バイナリをダウンロードして実行すれば、WebインターフェースとAPIの両方が立ち上がり、素早く設定でき、必要なものを自動でダウンロードしてくれるなら本当に良さそう
「CPU 1個とRAM 128MBの仮想マシンで、FreeBSDカーネルは20ms未満で起動できる」
なんてことだ、VMなしで実ハードウェア上で同じことをどう達成すればいいんだろう ;)
遅いのはそれ以外のすべて。例えば自分のマシンでは
Startup finished in 14.552s (firmware) + 2.885s (loader) + 741ms (kernel) + 23.116s (initrd) + 11.191s (userspace) = 52.488sとなっている