この記事に出てくる HITSC 関連の用語、構成名、設計上の呼び方は、執筆時点の検証用基盤に関するものです。実装や設計の進行に伴い、現在は変更されている場合があります。
HITSC / hitto Troubleshooting Contest を作っていると、いろいろな単語が出てくる。
KVM
QEMU
Proxmox
Firecracker
microVM
kernel
rootfs
TAP
bridge
network namespace
snapshot
CoW
仮想化に慣れていないと、この時点でかなりつらい。
しかも、これらは全部「仮想マシンを起動する技術」と雑にまとめられがちだが、実際には担当している場所が違う。
この記事では、HITSCを題材にして、仮想化初学者向けに全体像を整理する。
まず全体像
HITSCの現在の構成は、大きく見るとこうである。
physical server
|
| Proxmox
v
worker VM: hitsc-worker-01
|
| Firecracker + KVM
v
ticketごとのmicroVM
|
| SSH
v
参加者がトラブルシューティングするLinux環境
物理サーバに直接Firecrackerを入れるのではなく、Proxmox上にworker VMを作り、その中でFirecracker microVMを動かしている。
つまり、HITSCでは仮想化が2段になっている。
第1段:
Proxmox VM
物理サーバ上にworker VMを作る
第2段:
Firecracker microVM
worker VM内に問題環境を作る
この構成を初めて見ると「VMの中でVMを動かしているの?」となる。
そうである。 いわゆるnested virtualizationである。
HITSCでは、Proxmoxは大きなVM管理レイヤ、Firecrackerは問題環境を大量に発行する小さなVM実行エンジンとして使っている。
VMとは何か
VMは、ざっくり言うと「OSから見ると本物のコンピュータっぽく見える箱」である。
普通のLinuxサーバには、次のような部品がある。
CPU
memory
disk
NIC
serial console
kernel
filesystem
process
VMでは、このうちCPU、memory、disk、NICなどを仮想的に用意する。
guest OSは、それを本物の機械だと思って起動する。
host machine
|
+-- virtual CPU
+-- virtual memory
+-- virtual disk
+-- virtual NIC
|
v
guest OS
HITSCで重要なのは、参加者が入る問題環境が「コンテナ」ではなく「VM」だという点である。
参加者はrootでログインする。
systemctl を見たり、journalctl を見たり、Dockerやiptablesを壊したり直したりする。
この用途では、host kernelを共有するコンテナより、guest kernelを持つVMの方が自然である。
コンテナとVMの違い
かなり大雑把に言うと、違いはkernelを共有するかどうかである。
container:
host kernelを共有する
processやfilesystemを分離する
VM:
guest kernelを起動する
OSごと別のmachineとして動く
図にするとこう。
container:
host Linux kernel
├── container A process
├── container B process
└── container C process
VM:
host Linux kernel
├── guest VM A kernel -> process
├── guest VM B kernel -> process
└── guest VM C kernel -> process
コンテナは軽い。 ただし、host kernelを共有する。
VMは重くなりやすい。 ただし、guest kernelを持つので、より本物のサーバに近い。
HITSCはトラブルシューティング練習基盤なので、次のような体験を重視している。
SSHでログインする
systemd PID 1が動く
systemctlが使える
journalctlが使える
iptablesやDockerも問題にできる
rootで壊してもticket単位で隔離される
このため、HITSCではmicroVMを採用している。
KVMとは何か
KVMは、Linux kernelが持っている仮想化機能である。
正確な説明はLinux kernelのKVM documentationにあるが、HITSCを理解するうえでは、まず次の理解でよい。
KVM:
Linux kernel側でCPU仮想化を支える仕組み
KVMを使うと、Intel VT-xやAMD-VのようなCPUの仮想化支援機能を使って、guest OSを高速に動かせる。
HITSC workerで確認した /dev/kvm は、ユーザー空間のVMMがKVMへアクセスするための入口である。
ls -l /dev/kvm
lsmod | grep kvm
egrep -c '(vmx|svm)' /proc/cpuinfo
FirecrackerはKVMを使う。 QEMUもKVMを使える。 ProxmoxのVMも基本的にQEMU/KVMで動く。
つまりKVMは、HITSCの下の方でかなり重要な土台になっている。
QEMUとは何か
QEMUは、広い意味ではmachine emulator / virtualizerである。
QEMUのsystem emulationは、CPU、memory、deviceを持つ仮想machineを作り、guest OSを動かす。
KVMと組み合わせると、CPU実行の重い部分をKVMに任せ、QEMUはmachine全体の構成やdevice emulationを担当する。
よく見る言葉として、QEMU/KVMがある。
KVM:
CPU仮想化をLinux kernel側で支える
QEMU:
仮想machine、device、disk、NICなどを扱う
QEMU/KVM:
QEMUがKVMを使って高速にVMを動かす構成
Proxmoxは、VM実行の土台としてQEMU/KVMを使う。
HITSCでは、Proxmox上のworker VMを作るところでQEMU/KVMにお世話になっている。
Firecrackerとは何か
Firecrackerは、KVMを使ってmicroVMを作るVMMである。
VMMはVirtual Machine Monitorの略で、仮想マシンを作って動かすプログラムだと思えばよい。
Firecrackerの特徴は、汎用PCっぽいVMを作るのではなく、必要なdeviceをかなり絞ったmicroVMを作ることにある。
QEMU:
汎用的
多くのmachine/deviceを扱える
とても柔軟
Firecracker:
microVM向け
device modelを小さくする
速い起動と小さい攻撃面を狙う
HITSCでは、参加者ごと、ticketごとに問題環境を発行したい。
tkt-000001 -> microVM
tkt-000002 -> microVM
tkt-000003 -> microVM
この用途では、全部を重い汎用VMとして起動するより、Firecracker microVMの方が相性がよい。
microVMは「小さいVM」
microVMはコンテナではない。 VMである。
ただし、普通の汎用VMより小さく、シンプルに作られている。
HITSCでのmicroVMは、だいたいこういう部品で動く。
Firecracker process
|
+-- guest kernel: vmlinux-6.1-firecracker
+-- rootfs: rootfs.ext4
+-- virtual block device: /dev/vda
+-- virtual network device: eth0
+-- API socket
+-- stdout/stderr log
参加者から見ると、普通のLinuxサーバにSSHしているように見える。
ssh root@10.41.0.2
systemctl status nginx
journalctl -xeu nginx
しかし、HITSC worker側から見ると、それはFirecracker processが動かしているmicroVMである。
kernelとrootfs
FirecrackerでLinux guestを起動するには、最低限kernelとrootfsが必要になる。
kernel:
Linux kernel本体
例: /var/lib/hitsc-firecracker/kernels/vmlinux-6.1-firecracker
rootfs:
guest OSのfilesystem
例: /var/lib/hitsc-firecracker/problems/nginx-001/rootfs.base.ext4
kernelは「OSの中核」である。 rootfsは「/etc や /usr や /var が入っているdisk image」である。
Firecrackerには、普通のPCのBIOS画面やinstallerを使うような雰囲気はない。 HITSCでは、あらかじめ作ったkernelとrootfsを渡して起動する。
{
"boot-source": {
"kernel_image_path": "/var/lib/hitsc-firecracker/kernels/vmlinux-6.1-firecracker",
"boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
},
"drives": [
{
"drive_id": "rootfs",
"path_on_host": "/var/lib/hitsc-firecracker/instances/tkt-xxx/rootfs.ext4",
"is_root_device": true,
"is_read_only": false
}
]
}
HITSCでUbuntu 24.04 rootfsを作ったとき、最初は debootstrap --variant=minbase のrootfsに udev や kmod が足りず、guestのNICが出ない問題に当たった。
この経験から分かるのは、Firecrackerが動くだけでは十分ではないということである。
Firecracker:
仮想machineを起動する
guest kernel:
virtio-net / virtio-blkなどを認識する
rootfs:
systemd, udev, sshd, networkdなどを持つ
この3つが揃って、ようやくSSHできる問題環境になる。
TAPとは何か
VMにネットワークを持たせるには、host側にも接続口が必要になる。
HITSCでは、Firecrackerのguest NICとhost側のTAP deviceをつなぐ。
guest microVM
eth0: 10.41.0.2
|
| virtio-net
|
host / namespace
tapxxxx: 10.41.0.1
TAPは、Linux上に作る仮想的なL2 network interfaceである。
Firecrackerはguest側に仮想NICを見せる。 host側では、その通信がTAP deviceに出てくる。
そのため、HITSCの起動処理では次のようなことをする。
ip tuntap add dev tapxxxx mode tap
ip addr add 10.41.0.1/24 dev tapxxxx
ip link set tapxxxx up
実際には現在のHITSCではticketごとのnetwork namespace内にTAPやbridgeを作る。
network namespaceとは何か
network namespaceは、Linuxのネットワークスタックを分ける仕組みである。
namespaceを分けると、interface、route、iptablesなどを別々に持てる。
root namespace:
workerの通常ネットワーク
ticket A namespace:
br/tap/route/iptables
ticket B namespace:
br/tap/route/iptables
HITSCではticketごとにnetwork namespaceを作る。
これにより、複数ticketで同じguest IPを使い回せる。
ticket A:
guest: 10.41.0.2
ticket B:
guest: 10.41.0.2
ticket C:
guest: 10.41.0.2
root namespaceだけでTAPを作ると、同じIPを同時に使い回せない。
ticketごとにnamespaceを切ることで、HITSCは「同じ問題環境を何個も複製する」ことができる。
bridgeとは何か
複数VM問題では、1つのticket内に複数のmicroVMを起動する。
たとえば docker-iptables-001 は、server、client1、client2の3 VM構成である。
ticket namespace
mgmt bridge
├── server eth0
├── client1 eth0
└── client2 eth0
lan bridge
├── server eth1: 192.168.36.1
├── client1 eth1: 192.168.36.2
└── client2 eth1: 192.168.36.3
bridgeは、ざっくり言うと仮想スイッチである。
複数のTAPを同じbridgeにつなぐことで、microVM同士が同じL2 network上にいるようにできる。
これにより、HITSCでは次のような問題を作れる。
client1からserverへcurlする
client2からは遮断する
server上のiptablesを調査する
DockerのDOCKER-USER chainを直す
これはコンテナ単体の演習では出しにくい、かなり物理っぽいトラブルシューティングになる。
ticketとは何か
HITSCでは、問題環境の発行単位をticketと呼ぶ。
Problemはテンプレートである。
Problem:
nginx-001
docker-iptables-001
smoke-001
Ticketは、Problemから発行された1回分の環境である。
Problem: nginx-001
|
+-- Ticket: tkt-20260605-103227-9ce797
|
+-- Instance: Firecracker microVM
同じuserが同じProblemを何度も解けるように、HITSCでは user + problem を主キーにしていない。
user + problem:
同じ問題を1回しか持てない
ticket:
同じ問題を何度でも発行できる
この考え方は、HITSCの設計でかなり重要である。
HITSCのticket startで何が起きるか
hitsc ticket start <ticket-id> を実行すると、内部では多くのことが起きる。
単純化するとこう。
1. problem.yamlを読む
2. ticketに対応するinstance directoryを作る
3. rootfs.base.ext4からticket用rootfs.ext4を作る
4. network namespaceを作る
5. bridge/TAP/vethを作る
6. rootfsへIP設定やcredentialを注入する
7. firecracker.jsonを生成する
8. Firecracker processを起動する
9. API socketへmachine/drives/network設定を入れる
10. InstanceStartする
11. SSH readinessを確認する
12. SQLiteとstate.jsonへ状態を書く
ユーザーから見ると1コマンドだが、実際にはLinux、Firecracker、filesystem、network、SQLiteが全部動く。
だからHITSCのバグ調査では、どの層で壊れているかを分けて見る必要がある。
Firecracker processは起動しているか
guest kernelはbootしているか
eth0はguest内にあるか
TAPにpacketは出ているか
namespaceのrouteは正しいか
sshdは起動しているか
SQLiteの状態だけ壊れていないか
この分解ができると、HITSCのログがかなり読みやすくなる。
snapshotとは何か
snapshotは、ある時点のVM状態を保存する仕組みである。
HITSCではFirecracker snapshot restoreを使って、起動済みVMに近い状態から復元する検証をしている。
direct boot:
kernel boot
systemd boot
sshd起動
SSH ready
snapshot restore:
保存済みsnapshotをload
途中状態から再開
SSH readyまで短縮
snapshot restoreは速い。 実測でもdirect bootよりかなり短くなった。
ただし、snapshotには注意点がある。
VMのmemory stateを保存するので、guest内の一意値も複製されうる。
/etc/machine-id
SSH host key
random seed
application token
userspace PRNG state
そのため、HITSCではsnapshotを単なる高速化機能としてだけでなく、clone後の一意性再生成もセットで考えている。
ここで出てくるのが、snapshot-fast、snapshot-reunique、snapshot-strict のようなprofileである。
CoWとは何か
CoWはCopy-on-Writeの略である。
最初は同じデータを共有し、書き込みが起きた部分だけコピーする、という考え方である。
HITSCでは、2種類のCoWを考えている。
Storage CoW:
rootfs.ext4をreflinkなどで共有する
書き込まれたblockだけ別物になる
Memory CoW:
snapshot.memを複数VMで共有する
書き込まれたmemory pageだけ別物になる
Storage CoWは、ticketごとのrootfs作成を速くし、ディスク使用量を減らす。
rootfs.base.ext4
-> ticket-a/rootfs.ext4
-> ticket-b/rootfs.ext4
-> ticket-c/rootfs.ext4
Memory CoWは、snapshot restoreした複数VMのmemory使用量を減らす。
ただし、CoWは高速化・効率化の話であって、安全なclone運用そのものではない。 一意性再生成やsnapshot取得位置の設計は別に必要である。
HITSCでよく出るファイル
HITSCを触っていると、以下のpathをよく見る。
/var/lib/hitsc-firecracker/
kernels/
vmlinux-6.1-firecracker
rootfs/
ubuntu-24.04-base.ext4
problems/
nginx-001/
problem.yaml
rootfs.base.ext4
scripts/check.sh
tickets/
tkt-...
ticket.json
instances/
tkt-.../
rootfs.ext4
firecracker.json
firecracker.socket
stdout.log
stderr.log
state.json
metadata/
hitsc.sqlite3
それぞれの役割はこう。
kernels:
guest kernel置き場
rootfs:
共通rootfs置き場
problems:
問題テンプレート
tickets:
発行された問題環境の管理情報
instances:
実際に起動するVMごとのartifact
metadata:
SQLiteやbenchmark/load-test log
この対応関係が分かると、HITSCのdebugがだいぶ楽になる。
よくある勘違い
Firecrackerは問題管理システムではない
FirecrackerはmicroVMを起動するVMMである。
HITSCのような問題管理、ticket管理、score管理はFirecrackerの責務ではない。
Firecracker:
microVMを起動する
HITSC:
problem.yamlを読む
rootfsを作る
networkを作る
Firecrackerを起動する
scoreする
reset/destroyする
microVMはコンテナではない
Firecracker microVMはVMである。
guest kernelがある。
このため、systemdやjournalctl、Dockerなどを含む「実サーバっぽい問題」を作りやすい。
速いVMでもSSH readyまでは時間がかかる
Firecracker processの起動自体は軽い。
しかし、HITSCの ticket start で見ている時間には、以下が含まれる。
rootfs copy / reflink
network namespace作成
TAP/bridge作成
Firecracker起動
Linux kernel boot
systemd boot
sshd起動
SSH readiness確認
SQLite/state.json更新
そのため、「Firecrackerは速い」と「HITSCの問題環境がSSH可能になるまで1秒未満」は同じ意味ではない。
snapshot restoreやStorage CoWを入れる理由は、この差を縮めるためである。
localhost-onlyでもnetwork ruleが甘いと漏れる
HITSCではpublished portをworker localhost限定にしている。
しかし、実際にpacketを転送するiptables ruleが甘いと、別ticket namespaceから到達できる可能性があった。
これは別の記事にまとめた。
ここから分かるのは、仮想化基盤では「設定値」だけでなく、「packetが実際に通る経路」を見る必要があるということだ。
HITSCを読む順番
仮想化初学者がHITSCを読むなら、次の順番がよいと思う。
1. このblog
仮想化の地図を掴む
2. docs/architecture/
HITSC全体の構造を読む
3. docs/problem-authoring/
問題作成方法を読む
4. docs/design/current/
現在の設計判断を読む
5. docs/blog/
失敗ログと調査記事を読む
6. internal/
実装コードを読む
コードリーディングなら、最初はこのあたりがよい。
internal/model/
Ticket, Instance, Problemのデータ構造
internal/ticket/
ticket issue/start/score/closeの流れ
internal/instance/
rootfs, network, runtime起動の流れ
internal/network/
namespace, bridge, TAP, NAT, port forward
internal/runtime/
Firecracker起動
internal/store/
SQLiteとstate.json
まとめ
HITSCは、単にFirecrackerを起動するCLIではない。
HITSCは、Firecracker microVMを使って、トラブルシューティング用のLinux問題環境をticket単位で発行する基盤である。
そのため、いくつもの層が重なっている。
Proxmox:
worker VMを管理する
KVM:
Linux kernel側で仮想化を支える
Firecracker:
microVMを起動する
rootfs:
guest OSの中身
network namespace:
ticketごとのnetworkを分離する
SQLite/state.json:
HITSCの状態を保存する
ticket:
問題環境の発行単位
初学者にとって大事なのは、全部を一度に理解しようとしないことである。
まずは、どの技術がどの層を担当しているかを見る。
その地図があると、No route to host、eth0がない、database is locked、nginxが起動しない のような問題を、どの層から調べるべきか判断しやすくなる。
HITSCの実装はまだ育っている途中だが、この「層を分けて考える」感覚は、仮想化基盤を作るうえでかなり大事だと思う。
参考
- Firecracker repository: https://github.com/firecracker-microvm/firecracker
- Firecracker documentation: https://github.com/firecracker-microvm/firecracker/tree/main/docs
- Linux KVM documentation: https://www.kernel.org/doc/html/latest/virt/kvm/index.html
- QEMU documentation: https://www.qemu.org/docs/master/
- QEMU system emulation: https://www.qemu.org/docs/master/system/introduction.html
- Linux network namespaces: https://man7.org/linux/man-pages/man7/network_namespaces.7.html