ステータス: ドラフト(2026-10-17)、ソースコード + 読み取り専用の実測に基づく。 基準は KubeVirt
v1.9.0(コミット79d34c17)です。実測は、稼働中の 2台の VM の virt-launcher Pod 内で読み取りコマンド(virsh dumpxml,qemu-img info -U,/proc·/sys読み取り)のみを使用したものです。
KubeVirt ソースコードリーディング 5: virt-launcher
目次
1. 概要: 翻訳機、運転手、見張りの役割
virt-launcher Pod には 3つの役割が含まれています。
- 翻訳機: VMI(Kubernetes の言葉)を libvirt ドメイン XML(仮想化の言葉)に変換します。この処理を行うのが converter です。
- 運転手: Pod 内に libvirt デーモンを起動し、そのデーモンに対してドメインの定義と起動を指示します。
- 見張り: QEMU が終了した際に Pod も終了させ、Kubernetes が Pod を停止しようとした際にはシグナルを VM 側に転送します。
第4回で紹介した virt-handler が gRPC で「起動せよ」と呼び出すと、その後の処理はすべてこの Pod の内部で行われます。
2. 図解: 1回の SyncVMI
3. コードパス
3.1 プロセスツリーを構築する部分: cmd/
Pod の PID 1 は virt-launcher-monitor です。 RunAndMonitor()
(virt-launcher-monitor.go:98)が
/usr/bin/virt-launcher を子プロセスとして起動し、 SIGCHLD を受け取るたびに Wait4(-1, …) でゾンビプロセスを回収します。QEMU は libvirt がデーモンのように起動するため、親が終了すると PID 1 の子プロセスになります。PID 1 がゾンビプロセスを回収しないと、プロセス テーブルが一杯になってしまいます。
virt-launcher の main()(virt-launcher.go:347)は
以下の順序で準備を行います。
- ディレクトリを作成し(
initializeDirs)、libvirt デーモンvirtqemudを起動します (:432)。ログデーモンvirtlogdも起動します。 v1.9.0 では以前のモノリシックなlibvirtdではなく、QEMU ドライバーのみを担当するモジュール型のデーモンを使用します。docs/components.mdはまだlibvirtdと記載されているため、コードを信用してください。 - libvirt に接続し、ドメインマネージャー
NewLibvirtDomainManager(...)を作成します。 - cmd サーバーを 一時的な名前のソケットでオープンします(
startCmdServer, :482). - ドメインイベントの監視を開始し、シグナルハンドラーを設定します。
markReady()(:80) — 一時ソケットの名前を本当の 名前(ズバリlauncher-sock)に 変更することが準備完了の合図です。virt-handler はその名前のソケットが表示されてから通信を開始します。waitForDomainUUID()(:525)によりドメインが定義されるのを 待ちます。第3回の--qemu-timeout(240秒 + ランダム)が経過してもドメインが存在しない場合は自動的に終了します。起動が停止した Pod が永遠に残らないようにするための仕組みです。
3.2 SyncVMI: 1つのリクエストが行う処理
cmd サーバーの SyncVirtualMachine()(cmd-server/server.go:250)は
JSON 形式で送られてきた VMI をデコードして LibvirtDomainManager.SyncVMI()
(manager.go:1380)に渡します。この関数が
virt-launcher の核心です。
domainModifyLockを取得します。handler が同じ VMI を複数回送信した場合でも(実測でも「Synced vmi」が 1秒間に 4回発生)、1度に 1つずつ処理されます。generateConverterContext()(:1150) — 変換に 必要な外部の情報を収集します。ディスクイメージの形式、ブロックデバイスであるかどうか、GPU や SR-IOV デバイスの PCI アドレス(第7回)、EFI ファームウェアの場所などです。 converter が直接ファイルシステムを探索しないようにすることで、converter を純粋な関数のようにテストできるようにした設計になっています。Convert_v1_VirtualMachineInstance_To_api_Domain()(以下の 3.3) — VMI からapi.Domain構造体へ変換します。lookupOrCreateVirDomain()(:1619) — 名前で 検索して存在しない場合はpreStartHook()を使用してディスクを準備し、XML を定義します。成功すると「Domain defined.」 (:1651).preStartHook()(:892) — cloud-init データの 準備、コンテナディスクおよび ephemeral ボリュームの 書き込みレイヤーの作成(3.4)、空のディスク(emptyDisk)の作成、ConfigMap・Secret ディスクの作成を行います。- ドメインが停止しており、かつ VMI がまだ Running でない場合は
startDomain()(:1596) — cloud-init の ISO を作成し、dom.CreateWithFlags()を使用して QEMU を起動します。「Domain started.」 - すでに稼働中のドメインである場合は、
syncDisks()とnetwork.Sync()を用いて、ホットプラグされたディスクやインターフェースの差分のみを調整します。
3.3 converter: 設定器を順番に適用するビルダー
converter/converter.go の Convert_v1_VirtualMachineInstance_To_api_Domain()
(:967)ではデバイスごとにコードを
長々と書くのではなく、 設定器(Configurator)のリストを作成し(ズバリ:993)、
ビルダーが順番に実行します(ズバリ:1062)。
リストには 28個の設定器があり(メタデータ、ネットワーク、TPM、vsock、時計、乱数デバイス、入力デバイス、メモリバルーン、グラフィックス、サウンド、ホストデバイス、ウォッチドッグ、コンソール、SMBIOS、OS、virtiofs、コントローラー、CPU、IO スレッド、メモリなど)、ハイパーバイザーに応じて KVM または MSHV の設定器がもう 1つ追加されて合計 29個になります。
1つの設定器は Configure(vmi, domain) メソッドを 1つ持つだけであるため、 特定のデバイスを修正する PR は 1つの設定器ファイルとそのテストのみを変更すればよい ようになっています。
converter/compute/ 以下のファイル名がそのままデバイス名を表しています(watchdog.go, balloon.go, host_device.go …).
ディスクは設定器の外側で、ビルダーの実行後に vmi.Spec.Domain.Devices.Disks を実行してボリュームの種類ごとの変換関数を呼び出します。3つだけ見てみましょう。
| ボリューム | 関数 | XML の構造 |
|---|---|---|
| PVC・DataVolume(ファイルシステム) | Convert_v1_FilesystemVolumeSource_To_api_Disk(:784) |
raw ファイル …/vmi-disks/<볼륨>/disk.img |
| コンテナディスク | Convert_v1_ContainerDiskSource_To_api_Disk(:898) |
qcow2 書き込みレイヤー + <backingStore> 内のイメージ |
| ephemeral(PVC 上の一時的な書き込み) | Convert_v1_EphemeralVolumeSource_To_api_Disk(:922) |
qcow2 書き込みレイヤー + <backingStore> 内の PVC |
ドメイン名はメタデータ設定器が <네임스페이스>_<이름> を使って決定します
(metadata/configurator.go:34).
3.4 書き込みレイヤー: pkg/ephemeral-disk/ephemeral-disk.go
コンテナディスクはイメージ内のファイルであるため読み取り専用です。ゲストが書き込むためには、その上に qcow2 の書き込みレイヤーが存在する必要があります。
containerdisk.CreateEphemeralImages()(container-disk.go:328)が
コンテナディスクのボリュームごとに CreateBackedImageForVolume()
(ephemeral-disk.go:96)を呼び出し、実際のコマンドはこれです
(ズバリ:143).
cmd := exec.Command("qemu-img",
"create",
"-f",
"qcow2",
"-b",
backingFile,
"-F",
backingFormat,
imagePath,
)
出典: kubevirt/kubevirt v1.9.0 pkg/ephemeral-disk/ephemeral-disk.go 145–154行目(Apache-2.0)。書き込みレイヤーの配置場所は GetFilePath()
(:91)が決定する <ephemeral-disk-dir>/<볼륨>/disk.qcow2
であり、そのディレクトリは Pod の emptyDir です。Pod が削除されると書き込みレイヤーも一緒に消滅します。すでにファイルが存在する場合は再作成しないため、同じ Pod 内で launcher が SyncVMI を複数回受信しても安全です。「学生 VM が起動するまで」の第4回で解説した「ゴールデンディスク上の薄い書き込みレイヤー」こそが、この関数の volume.Ephemeral の分岐(ズバリCreateEphemeralImages, :127)です。
3.5 ネットワーク: masquerade バインディング
Pod ネットワークを使用する VM のデフォルトのバインディングは masquerade です。処理は 2つに分かれています。
- virt-handler(Pod の外) は、Pod のネットワーク名前空間にブリッジ
k6t-eth0と tap を作成します。目指す構成はmasqueradeBindingSpec()(network/setup/netpod/netpod.go:415)が 描画します。ブリッジには固定 MAC02:00:00:00:00:00(link/mac.go:27)と VM 専用の小さなプライベートアドレス帯(ズバリapi.DefaultVMCIDR)のゲートウェイアドレスを割り当て、tap の所有者は QEMU の UID に設定します。そして、nftables に「ゲストアドレスから送信されるトラフィックは Pod アドレスに変換せよ」というルールを追加します(ズバリMasqPod.Setup, masquerade.go:99). - virt-launcher(Pod の中) は、ゲストにそのアドレス帯のIPアドレスを割り当てる小さな DHCP サーバーを起動し(ズバリ
network/setup/launcherのensureDHCP)、 ドメイン XML には<interface type='ethernet'>と<target dev='tap0' managed='no'/>を記述します (ズバリdomainspec/generators.go:106).managed='no')。ここで
4. 実測: Pod 内を覗く
2台の VM の virt-launcher Pod(ズバリcompute コンテナ)から読み取りました(2026-10-17)。VM A は 2 vCPU・2GiB・PVC ルート、VM B は 8 vCPU・8GiB・コンテナディスク ルートです。
プロセスツリーとメモリ(VM A)。 /proc/*/status の PPid と VmRSS です。
| PID | 親 | プロセス | RSS | 第3回の推定式の割り当て |
|---|---|---|---|---|
| 1 | — | virt-launcher-monitor | 8.7MiB | 25Mi |
| 13 | 1 | virt-launcher | 68.5MiB | 100Mi |
| 26 | 13 | virtqemud | 31.6MiB | 40Mi |
| 27 | 13 | virtlogd | 14.8MiB | 25Mi |
| 97 | 1 | qemu-kvm | 626MiB | ゲスト 2GiB + 30MiB など |
QEMU の親が PID 1 であるという点が、3.1節の「libvirt がデーモンのように起動した QEMU を monitor が回収する」という動作に該当します。QEMU を除く 4つのプロセスの RSS の合計は 123.6MiB であり、推定式の固定値(190MiB、QEMU の 30MiB を除く)の範囲内に収まりました。共有ライブラリをプロセスごとに計測する RSS の性質上、実際の余裕はさらに大きくなります。ゲストには 2GiB が割り当てられていますが、QEMU の RSS は 626MiB でした。ゲストがまだアクセスしていないメモリはホスト側で確保されません。
libvirt 接続。 launcher ログの Connecting to libvirt daemon: qemu+unix:///session?socket=/var/run/libvirt/virtqemud-sock
(libvirt.go:680)が非 root セッションモードで virtqemud に接続していることを示しています。
ドメイン XML(VM A、一部省略)。 virsh dumpxml default_fedora-vm の前半部分と一部のデバイスです。
<domain type='kvm' id='1'>
<name>default_fedora-vm</name>
<maxMemory unit='KiB'>8388608</maxMemory>
<memory unit='KiB'>2097152</memory>
<vcpu placement='static'>2</vcpu>
<os><type arch='x86_64' machine='pc-q35-rhel9.8.0'>hvm</type> …</os>
<cpu mode='custom' match='exact' check='full'> … 기능 70여 줄 줄임 … </cpu>
<devices>
<emulator>/usr/libexec/qemu-kvm</emulator>
<disk type='file' device='disk' model='virtio-non-transitional'>
<driver name='qemu' type='raw' cache='none' error_policy='stop' discard='unmap'/>
<source file='/var/run/kubevirt-private/vmi-disks/root/disk.img' index='2'/>
<target dev='vda' bus='virtio'/>
<alias name='ua-root'/>
</disk>
<interface type='ethernet'>
<target dev='tap0' managed='no'/>
<model type='virtio-non-transitional'/>
<alias name='ua-default'/>
</interface>
<serial type='unix'><source mode='bind' path='/var/run/kubevirt-private/<VMI UID>/virt-serial0'/> …</serial>
</devices>
</domain>
コードと突き合わせてみると以下のようになります。名前は <네임스페이스>_<이름>, maxMemory はゲストの 4倍(メモリ ホットプラグ上限のデフォルト比率
ズバリ
DefaultMaxHotplugRatio = 4)、ディスクは 3.3節の表の 1行目、ユーザーが付けた名前の前には ua- が付く(user alias)、シリアルコンソールと VNC は TCP ではなく kubevirt-private/<VMI UID>/ 配下の UNIX ソケットです。virt-handler がコンソールをこのソケットに接続します。その他には
pcie-root-port コントローラーが 10個存在していました。一部はディスクや NIC が使用し、残りは allocateHotplugPorts() がホットプラグ用にあらかじめ配置しておいた空きスロットです。
書き込みレイヤー(VM B)。 ディスク XML と qemu-img info -U です。
| ファイル | 形式 | 仮想サイズ | 実サイズ |
|---|---|---|---|
/var/run/kubevirt/container-disks/disk_0.img(ベース、読み取り専用) |
qcow2 | 3.5GiB | 596MiB |
/var/run/kubevirt-ephemeral-disks/disk-data/root/disk.qcow2(書き込みレイヤー) |
qcow2, backing file = 上記ファイル | 3.5GiB | 998MiB(約 2.5日使用) |
/var/run/libvirt/empty-disks/scratch.qcow2(emptyDisk) |
qcow2 | — | 1.6GiB |
書き込みレイヤーのパスが GetFilePath() の <볼륨 이름>/disk.qcow2、backing 形式が -F qcow2 のようになっています。ベースの 596MiB は「学生 VM が立ち上がるまで」第 3 編の実測と同じサイズです。同じ Pod の container-disks フォルダには disk_0.sock もありました。第 4 編で virt-handler がイメージを抱くコンテナを見つけてアタッチする際に使う目印です。
ネットワーク(VM A)。 Pod 内 /sys/class/net には eth0, k6t-eth0(MAC 02:00:00:00:00:00), tap0(ブリッジ k6t-eth0 に接続)、 lo
の 4 つがありました。ドメイン XML のゲスト MAC は Pod eth0 の MAC と同じでした。
時間。 launcher ログで、「Marked as ready」→「Domain defined.」が 1.20 秒 → 「Domain started.」が 0.19 秒でした(第 4 編の表)。前の 1.2 秒は大部分が handler 側の準備(ネットワーク・デバイス)の時間であり、定義と起動自体は短いです。
5. 制限事項
- GPU やホストデバイスがアタッチされたドメインは今回実行するものがなく、XML を読み取れませんでした(第 7 編、 未実測).
- DHCP でゲストが取得したアドレスはゲスト内部を確認する必要があるため確認していません。ゲストエージェントが報告した VMI status のアドレスは Pod アドレスでした。
- メモリの数値は特定時点の RSS です。時間経過による変化、ページキャッシュとの関係は 未実測です。
- CPU モデルは
host-modelを展開した名前付きモデルであり(機能リスト約 70 行)、ノードの CPU を露見させるため記事では省略しました。
一言コメント。 converter が設定ジェネレータのリストに変更されたおかげで、デバイスを 1 つ修正したい場合は
converter/compute/でそのデバイス名のファイル 1 つだけを開けばよくなりました。最初のコントリビューションにぴったりのサイズです!
参考
すべて 2026-10-17 に閲覧しました。
manager.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/virt-launcher/virtwrap/manager.goconverter.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/virt-launcher/virtwrap/converter/converter.goephemeral-disk.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/ephemeral-disk/ephemeral-disk.go- libvirt ドメイン XML フォーマット: https://libvirt.org/formatdomain.html
- libvirt モジュラーデーモン(virtqemud): https://libvirt.org/daemons.html
- 姉妹シリーズ「学生 VM が立ち上がるまで」第 3 編: ディスクは PVC である、第 4 編: PVC から見たオーバーレイ