TT Lab
ブログ

한국어 English 日本語 中文

はじめる
ブログ

ブログ

KubeVirt ソースコードリーディング 5: virt-launcher — ドメイン XML と QEMU

ステータス: ドラフト(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つの役割が含まれています。

第4回で紹介した virt-handler が gRPC で「起動せよ」と呼び出すと、その後の処理はすべてこの Pod の内部で行われます。

2. 図解: 1回の SyncVMI

virt-launcher — VMI 를 도메인 XML 로 바꿔 띄우기 SyncVMI() 한 번이 하는 일, 위에서 아래로 파드 안의 프로세스 (실측 RSS) monitor 8.7Mi launcher 68.5Mi virtqemud 31.6Mi qemu-kvm 626Mi virtlogd 14.8Mi ← QEMU 의 부모는 PID 1 cmd 서버 SyncVirtualMachine cmd-server/server.go generateConverterContext() 디스크 정보 · GPU · SR-IOV · EFI 를 모은다 Convert_v1_VirtualMachineInstance_To_api_Domain 설정기 29개를 차례로, 이어서 디스크 루프 lookupOrCreateVirDomain() 없으면 preStartHook → DomainDefineXML preStartHook() cloud-init ISO · qemu-img 쓰기 층 · 빈 디스크 startDomain() → CreateWithFlags QEMU 프로세스 시작 syncDisks · network.Sync 핫플러그와 차이 맞추기 실측: Ready → Domain defined 1.20초 → Domain started 0.19초
図: virt-launcher 内のプロセスと SyncVMI の 7つのステップ

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)は 以下の順序で準備を行います。

  1. ディレクトリを作成し(initializeDirs)、libvirt デーモン virtqemud を起動します (:432)。ログデーモン virtlogd も起動します。 v1.9.0 では以前のモノリシックな libvirtd ではなく、QEMU ドライバーのみを担当するモジュール型のデーモンを使用します。 docs/components.md はまだ libvirtd と記載されているため、コードを信用してください。
  2. libvirt に接続し、ドメインマネージャー NewLibvirtDomainManager(...) を作成します。
  3. cmd サーバーを 一時的な名前のソケットでオープンします(startCmdServer, :482).
  4. ドメインイベントの監視を開始し、シグナルハンドラーを設定します。
  5. markReady()(:80) — 一時ソケットの名前を本当の 名前(ズバリlauncher-sock)に 変更することが準備完了の合図です。virt-handler はその名前のソケットが表示されてから通信を開始します。
  6. 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 の核心です。

  1. domainModifyLock を取得します。handler が同じ VMI を複数回送信した場合でも(実測でも「Synced vmi」が 1秒間に 4回発生)、1度に 1つずつ処理されます。
  2. generateConverterContext()(:1150) — 変換に 必要な外部の情報を収集します。ディスクイメージの形式、ブロックデバイスであるかどうか、GPU や SR-IOV デバイスの PCI アドレス(第7回)、EFI ファームウェアの場所などです。 converter が直接ファイルシステムを探索しないようにすることで、converter を純粋な関数のようにテストできるようにした設計になっています。
  3. Convert_v1_VirtualMachineInstance_To_api_Domain()(以下の 3.3) — VMI から api.Domain 構造体へ変換します。
  4. lookupOrCreateVirDomain()(:1619) — 名前で 検索して存在しない場合は preStartHook() を使用してディスクを準備し、XML を定義します。成功すると「Domain defined.」 (:1651).
  5. preStartHook()(:892) — cloud-init データの 準備、コンテナディスクおよび ephemeral ボリュームの 書き込みレイヤーの作成(3.4)、空のディスク(emptyDisk)の作成、ConfigMap・Secret ディスクの作成を行います。
  6. ドメインが停止しており、かつ VMI がまだ Running でない場合は startDomain() (:1596) — cloud-init の ISO を作成し、 dom.CreateWithFlags() を使用して QEMU を起動します。「Domain started.」
  7. すでに稼働中のドメインである場合は、 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

도메인 XML 이 가리키는 파일과 장치 실측한 두 VM 의 <disk> · <interface> 를 그렸다 디스크 세 갈래 컨테이너 디스크 읽기 전용 바탕 disk_0.img 596Mi 쓰기 층 disk.qcow2 — 998Mi …/ephemeral-disks/disk-data/root/ qemu-img create -b 바탕 파드가 지워지면 emptyDir 와 함께 쓰기 층도 사라진다 PVC 디스크 — raw disk.img (10 GiB) /var/run/kubevirt-private/vmi-disks/root/disk.img 빈 디스크 emptyDisk scratch.qcow2 1.6G cloud-init noCloud.iso (raw) driver: cache=none · error_policy=stop · discard=unmap 네트워크 (masquerade 바인딩) eth0 파드 IP k6t-eth0 브리지 · NAT tap0 QEMU 가 연다 <interface type='ethernet'> target dev='tap0' managed='no' 브리지 MAC 은 고정값 02:00:00:00:00:00 (link/mac.go) 게스트 NIC 의 MAC 은 파드 eth0 의 MAC 을 그대로 쓴다(실측)
図: ドメイン XML が指すディスクの 3つの分岐と masquerade ネットワーク

コンテナディスクはイメージ内のファイルであるため読み取り専用です。ゲストが書き込むためには、その上に 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つに分かれています。

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. 制限事項

一言コメント。 converter が設定ジェネレータのリストに変更されたおかげで、デバイスを 1 つ修正したい場合は converter/compute/ でそのデバイス名のファイル 1 つだけを開けばよくなりました。最初のコントリビューションにぴったりのサイズです!

参考

すべて 2026-10-17 に閲覧しました。

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます