← logs

Firecracker snapshot restore で Memory CoW の効果を PSS で確認した

2026年6月1日

HITSC の複数 VM 問題で Firecracker snapshot restore を使い、起動時間短縮と Memory CoW の効果を PSS で確認した記録

この記事に出てくる HITSC 関連の用語、構成名、設計上の呼び方は、執筆時点の暫定的なものです。実装や設計の進行に伴い、現在は変更されている場合があります。

HITSC / hitto Troubleshooting Contest は、参加者ごと・ticket ごとに Firecracker microVM を起動し、SSH で Linux サーバへ入って障害対応を練習できるようにするための検証用基盤である。

その検証の一環として、起動を速くするために Firecracker snapshot restore を組み込み始めた。

目的は大きく 2 つある。

1. 起動済み VM から restore して、systemd / sshd 起動待ちを短縮する
2. 同じ snapshot memory file を複数 VM で共有し、Memory CoW の恩恵を受ける

今回は、複数 VM 問題 docker-iptables-001 で実際に snapshot restore を使い、Memory CoW の効果が PSS 上で見えるかを確認した。

何を確認したいのか

Storage CoW と Memory CoW は別物である。

Storage CoW:

problem rootfs.base.ext4
  -> snapshot 用 rootfs.ext4
      -> ticket ごとの rootfs.ext4

これは HITSC 側の RootFSProvisioner で制御できる。

現状は HITSC_ROOTFS_BACKEND=reflink により、cp --reflink=always を使う。

Memory CoW:

snapshot.mem
  -> Firecracker process A
  -> Firecracker process B
  -> Firecracker process C

同じ snapshot memory file を複数 Firecracker process が読み、書き込まれた page だけ各 VM の private memory になる。

これは Firecracker snapshot restore と Linux の page cache / mmap まわりの挙動に乗る。

ここで注意が必要なのは、RSS だけ見ても効果が分かりにくいことだ。

RSS は共有 page も各 process に重複して見えやすい。そのため、今回の主指標は PSS にした。

PSS は共有 page を process 間で按分するため、Memory CoW の効果を見るには RSS より向いている。

HITSC の実装には今回、次の tool を追加した。

tools/measure-firecracker-memory.sh

これは /proc/<pid>/smaps_rollup を読み、Firecracker process ごとの RSS、PSS、Shared / Private の内訳を TSV で出力する。

benchmark 時には、tools/benchmark-hitsc.sh が次のファイルへ記録する。

firecracker-memory.tsv

実験対象

使った問題は docker-iptables-001

これは 3 台構成の複数 VM 問題である。

server:
  eth0 mgmt 10.36.0.2
  eth1 lan  192.168.36.1

client1:
  eth0 mgmt 10.36.0.3
  eth1 lan  192.168.36.2

client2:
  eth0 mgmt 10.36.0.4
  eth1 lan  192.168.36.3

1 ticket あたり Firecracker process は 3 つ。

今回は 3 ticket を並列起動したので、合計 9 Firecracker process で比較した。

snapshot prepare

まず、docker-iptables-001 の snapshot を作った。

sudo HITSC_ROOTFS_BACKEND=reflink \
  hitsc snapshot prepare docker-iptables-001 \
  --id snap-docker-multi-20260603-010651 \
  --timeout 240 \
  --force

作成された artifact は以下。

/var/lib/hitsc-firecracker/snapshots/docker-iptables-001/snap-docker-multi-20260603-010651/
  metadata.json
  server/
    rootfs.ext4
    snapshot.state
    snapshot.mem
  client1/
    rootfs.ext4
    snapshot.state
    snapshot.mem
  client2/
    rootfs.ext4
    snapshot.state
    snapshot.mem

複数 VM snapshot では、node ごとに rootfs.ext4snapshot.statesnapshot.mem を持つ。

実装中に 1 つ問題を踏んだ。

最初は snapshot prepare 中の Firecracker API socket を snapshot artifact 配下に置いていた。

/var/lib/hitsc-firecracker/snapshots/docker-iptables-001/snap-docker-multi-.../server/firecracker.socket

しかし、Unix domain socket path には長さ制限がある。

この path が長すぎて、Firecracker API socket が出てこなかった。

エラーはこうだった。

firecracker API socket did not appear:
/var/lib/hitsc-firecracker/snapshots/docker-iptables-001/snap-docker-multi-.../server/firecracker.socket

対策として、snapshot prepare 時だけ短い /tmp/hs-<hash>.sock を使うようにした。

ticket restore 時の API socket は従来通り instance 配下に置く。

benchmark 条件

direct boot と snapshot restore を同条件で比較した。

problem: docker-iptables-001
count: 3 tickets
nodes per ticket: server, client1, client2
parallel: 3
scenario: start-wait-ssh
rootfs_backend: reflink
cache_state: warm
settle_seconds: 10

direct boot:

sudo HITSC_ROOTFS_BACKEND=reflink \
  HITSC_RUNTIME_BACKEND=direct \
  tools/benchmark-hitsc.sh docker-iptables-001 \
  --count 3 \
  --parallel 3 \
  --scenario start-wait-ssh \
  --rootfs-backend reflink \
  --runtime-backend direct \
  --arrival-pattern burst \
  --cache-state warm \
  --ready-timeout 240 \
  --settle-seconds 10 \
  --run-id memcow-docker-direct-20260603-021123 \
  --yes

snapshot restore:

sudo HITSC_ROOTFS_BACKEND=reflink \
  HITSC_RUNTIME_BACKEND=snapshot-restore \
  HITSC_SNAPSHOT_ID=snap-docker-multi-20260603-010651 \
  tools/benchmark-hitsc.sh docker-iptables-001 \
  --count 3 \
  --parallel 3 \
  --scenario start-wait-ssh \
  --rootfs-backend reflink \
  --runtime-backend snapshot-restore \
  --arrival-pattern burst \
  --cache-state warm \
  --ready-timeout 240 \
  --settle-seconds 10 \
  --run-id memcow-docker-snapshot-20260603-021203 \
  --yes

出力先は以下。

direct:
  /var/lib/hitsc-firecracker/metadata/benchmarks/memcow-docker-direct-20260603-021123

snapshot-restore:
  /var/lib/hitsc-firecracker/metadata/benchmarks/memcow-docker-snapshot-20260603-021203

起動時間

まず、SSH ready までの時間。

direct ssh_ready mean:
  15661 ms

snapshot-restore ssh_ready mean:
  2463 ms

この条件では、snapshot restore の方が約 6.4 倍速かった。

start_command も短くなった。

direct start_command mean:
  2458 ms

snapshot-restore start_command mean:
  1452 ms

snapshot_load_api は平均 25.98 ms だった。

snapshot_load_api mean:
  25.981 ms

今回の ssh_ready は、Firecracker restore そのものだけでなく、HITSC の network namespace、bridge、TAP、network verify、SSH 到達確認も含む。

したがって、Firecracker 単体の restore 時間としては見ない。

それでも、HITSC で想定している体験としては「ticket start から SSH ready まで」が重要なので、この数字は有用である。

PSS

次に Memory CoW を見る。

after-settle は start 後に 10 秒待った状態である。

direct:
  processes: 9
  total RSS: 3744.0 MiB
  total PSS: 3725.1 MiB
  shared_clean: 21.2 MiB
  private_dirty: 3722.7 MiB

snapshot-restore:
  processes: 9
  total RSS: 2226.7 MiB
  total PSS: 1775.9 MiB
  shared_clean: 670.3 MiB
  private_dirty: 1552.7 MiB

合計 PSS で見ると、snapshot restore は direct boot に比べて約 52% 少なかった。

node 別の after-settle PSS 平均はこう。

direct:
  server:  986.7 MiB
  client1: 127.5 MiB
  client2: 127.5 MiB

snapshot-restore:
  server:  493.7 MiB
  client1:  46.8 MiB
  client2:  51.4 MiB

server は Docker 入りで memory_mib も大きい。

direct では server が 1 台あたり約 987 MiB の PSS を持っていたが、snapshot restore では約 494 MiB になった。

client VM も、約 127 MiB から 50 MiB 前後まで下がっている。

この結果から、同じ snapshot memory file から複数 VM を restore した場合、Memory CoW の効果が PSS 上に見えることを確認できた。

after-start の数字

start 直後の after-start では、差がさらに大きく見える。

direct after-start:
  total RSS: 1745.3 MiB
  total PSS: 1726.4 MiB
  shared_clean: 21.2 MiB
  private_dirty: 1724.0 MiB

snapshot after-start:
  total RSS: 889.1 MiB
  total PSS: 470.7 MiB
  shared_clean: 622.5 MiB
  private_dirty: 257.8 MiB

snapshot restore 直後は共有されている page がかなり多い。

その後、guest が動き始めると書き込みが増え、private dirty が増えていく。

つまり、Memory CoW は「restore 直後が一番効きやすく、VM が動くほど private 化が進む」と見るのが自然である。

追加検証: 8 ticket / 24 VM

最初の検証は 3 ticket / 9 VM だった。

その後、同じ条件を大きくして 8 ticket / 24 VM でも比較した。

条件:

problem: docker-iptables-001
count: 8 tickets
nodes per ticket: server, client1, client2
parallel: 4
scenario: start-wait-ssh
rootfs_backend: reflink
cache_state: warm
settle_seconds: 20

run directory:

direct:
  /var/lib/hitsc-firecracker/metadata/benchmarks/memcow-docker-direct-8t-20260603-021920

snapshot-restore:
  /var/lib/hitsc-firecracker/metadata/benchmarks/memcow-docker-snapshot-8t-20260603-022054

起動時間は次のようになった。

direct ssh_ready mean:
  43734 ms

snapshot-restore ssh_ready mean:
  5785 ms

約 7.6 倍高速だった。

after-settle の PSS は以下。

direct:
  processes: 24
  total RSS: 7373.8 MiB
  total PSS: 7319.4 MiB
  shared_clean: 56.4 MiB
  private_dirty: 7316.8 MiB

snapshot-restore:
  processes: 24
  total RSS: 4858.9 MiB
  total PSS: 3298.8 MiB
  shared_clean: 1780.0 MiB
  private_dirty: 3076.6 MiB

合計 PSS は約 54.9% 少なくなった。

RSS の削減は約 34.1% なので、やはり Memory CoW を見るには PSS の方が分かりやすい。

node 別の after-settle PSS 平均はこう。

direct:
  server:  663.0 MiB
  client1: 126.7 MiB
  client2: 125.2 MiB

snapshot-restore:
  server:  338.2 MiB
  client1:  35.1 MiB
  client2:  39.0 MiB

3 ticket だけでなく、8 ticket でも同じ snapshot memory file 由来の page sharing が PSS 上に見えた。

これは完成なのか

Memory CoW の効果は確認できた。

ただし、これで本番安全な clone が完成したわけではない。

理由は、snapshot から復元した VM は、guest 内の一意値も複製しうるからである。

複製されうるもの:

/etc/machine-id
SSH host key
/var/lib/systemd/random-seed
/proc/sys/kernel/random/boot_id
application token
userspace PRNG state
snapshot 前に生成済みの session/cache

Firecracker 公式ドキュメントでも、snapshot clone 時の randomness や unique state の扱いは明確に注意されている。

参考:

hitsc-reunique.service とは何か

そこで必要になるのが、hitsc-reunique.service である。

これは snapshot restore 後の guest 内で一度だけ動く one-shot systemd service 案である。

常駐 agent ではない。

目的は、snapshot から復元された guest が自分自身の一意性を回復すること。

担当候補は以下。

/etc/machine-id 再生成
SSH host key 再生成
/var/lib/systemd/random-seed 削除または再生成
hostname を ticket/node 別に設定
ready marker を作成

HITSC 側の理想的な流れはこうなる。

1. snapshot restore
2. guest 内 hitsc-reunique.service 実行
3. ready marker 作成
4. worker が ready marker または SSH readiness を確認
5. ticket を RUNNING にする

ただし、これは簡単ではない。

特に、userspace アプリが snapshot 前に生成して memory 内に持っている token や PRNG state には一般解がない。

kernel RNG は VMGenID や entropy device で改善できる可能性があるが、アプリケーション内部の状態までは自動で直せない。

そのため、HITSC の設計では snapshot 取得タイミングも重要になる。

候補:

boot_complete
systemd_ready
ssh_ready
problem_ready

今は ssh_ready で snapshot を取っている。

起動短縮効果は大きいが、snapshot 前に起動した service が一意値を作っていれば、それも複製される。

本番運用では、問題ごとに「どの時点で snapshot を取ってよいか」を明確にする必要がある。

今回の検証では Storage CoW に reflink を使った。

HITSC_ROOTFS_BACKEND=reflink

一方で、LVM thin、dm-snapshot、OverlayFS も候補としてはある。

ただし、これは MVP の主経路にはまだ入れない。

理由:

LVM thin:
  pool 管理、枯渇、discard、cleanup が必要

dm-snapshot:
  COW store 管理と device cleanup が難しい

OverlayFS:
  Firecracker は block device rootfs を読むため、rootfs 管理モデルが変わる

現時点の方針は、rootfs.base.ext4 + reflink と Firecracker snapshot restore による Memory CoW を主経路にする。

まず benchmark で効果を測り、必要になったら LVM thin / dm-snapshot を PoC する。

OverlayFS は rootfs 管理モデルごと変わるので、最後に検討する。

まとめ

今回確認できたこと:

multi-VM snapshot prepare/restore が実 Firecracker で動いた
docker-iptables-001 の 3 ticket / 9 VM で snapshot restore できた
ssh_ready は約 15.7 秒から約 2.5 秒へ短縮した
after-settle 合計 PSS は約 3725 MiB から約 1776 MiB へ下がった
docker-iptables-001 の 8 ticket / 24 VM でも snapshot restore できた
8 ticket / 24 VM では ssh_ready が約 43.7 秒から約 5.8 秒へ短縮した
8 ticket / 24 VM では after-settle 合計 PSS が約 7319 MiB から約 3299 MiB へ下がった
Memory CoW の効果が PSS で観測できた

この検証で、Storage CoW と Memory CoW の基礎は見えてきた。

次の大きな論点は、速く clone することではなく、安全に clone することである。