ステータス: ドラフト(2026-10-17)、ソースコード + 読み取り専用の実測。 基準は KubeVirt
v1.9.0(コミット79d34c17)です。実測は v1.9.0 が導入された小規模クラスターのログ・オブジェクト・ポッドスペックを読み取ったものであり、VM の新規作成や変更は行っていません。
KubeVirt ソースコード読み取り 3: virt-controller
目次
- 1. 直感: キューには名前だけ、判断は常に最初から
- 2. 図
- 3. コードパス: リーダーになって初めて仕事をする
- 4. VM コントローラー: pkg/virt-controller/watch/vm/vm.go
- 5. VMI コントローラー: ポッドを作成して引き渡す
- 6. ポッドテンプレート: pkg/virt-controller/services/template.go
- 7. 実測
- 8. 制限事項
- 参考
1. 直感: キューには名前だけ、判断は常に最初から
Kubernetes のコントローラーは「何が起きたか」ではなく「現在の状態はどうなっているか」によって動作します。イベントハンドラーは変更されたオブジェクトの
名前(네임스페이스/이름)のみ 作業キューに入れ、ワーカーがその名前を取り出してキャッシュから最新のオブジェクトを読み込んだ後、「望ましい状態と現在の状態の
差分」を最初から再計算します。同じ名前が10回来ても1回しか処理されず、失敗した場合は名前をキューに遅延させてから再度入れます。
virt-controller における1つの VM は、これら2つのループを通過します。
- VM コントローラー: VM を監視して VMI を作成または削除する(実行戦略)。
- VMI コントローラー: VMI を監視して virt-launcher ポッドを作成し、ポッドの準備ができたら VMI をノードに引き渡す。
2. 図
3. コードパス: リーダーになって初めて仕事をする
virt-controller は2つのポッドが起動していても、 1つのリーダーのみ がコントローラーを動かします。 application.go のリーダー選挙コールバック
onStartedLeading()(:609)でインフォーマーを開始し、 go vca.vmController.Run(...) のようにコントローラーを立ち上げます。ワーカー数のデフォルト値は、VMI コントローラーが 10
(:128)、VM コントローラーを含む大半が
3(:125)です。
4. VM コントローラー: pkg/virt-controller/watch/vm/vm.go
4.1 Execute — キューから1つ取り出す
vm.go:336 の Execute() はすべてのコントローラーで同じ形をしています。
if err := c.execute(key); err != nil {
log.Log.Reason(err).Infof("re-enqueuing VirtualMachine %v", key)
c.Queue.AddRateLimited(key)
} else {
log.Log.V(4).Infof("processed VirtualMachine %v", key)
c.Queue.Forget(key)
}
出展: kubevirt/kubevirt v1.9.0 vm.go 347–353行(Apache-2.0)。失敗時は AddRateLimited により再試行の間隔が徐々に長くなり、成功すると Forget によって遅延の記録がクリアされます。
4.2 execute → sync
execute()(:357)はキャッシュ(インデクサー)から VM を読み込んで
コピーを作成し、同名の VMI を検索して所有権(ownerReference)を確認し(ClaimVirtualMachineInstanceByName)、DataVolume のリストを収集した後に sync() と updateStatus() を呼び出します。キャッシュ内のオブジェクトを直接修正すると他のワーカーが見るキャッシュが汚染されるため、KubeVirt のコードでは
ほとんどの場合 DeepCopy() から始めます。コードレビューで頻繁に見られる指摘事項です。
sync()(:3215)の順序は以下の通りです。
- 期待値の確認。
satisfiedExpectations()が偽である場合は、何もせずに戻ります。直前に VMI の作成を要求したものの、その VMI がまだインフォーマーに反映されていない状態であるためです。この仕組みがない場合、キャッシュの反映が遅れた際に VMI が重複して作成されてしまいます。 - 削除中の場合、 VMI を停止してファイナライザー(finalizer)を外します。そうでなければファイナライザー
kubevirt.io/virtualMachineControllerFinalizeを付与します。 vm.RunStrategy()(第2回)により実行戦略を1つにまとめ、instancetype の同期、DataVolume の作成・準備完了の確認を行います。syncRunStrategy()を用いて VMI を作成または削除します。
4.3 syncRunStrategy — Always 分岐
vm.go:970 の switch runStrategy がこのコントローラーの心臓部です。 Always(:979)の分岐を言葉に置き換えると以下のようになります。
- VMI が存在する状態で 停止要求があるか、終了状態(Succeeded・Failed)である場合 は、
stopVMI()(:1658)によって削除します。新しく作成する処理は 次回の reconcile が行います(「削除して一旦戻り、次のステップで開始させるようにする」というコメント)。 - VMI が存在して稼働している場合は、何も行いません。
- VMI が存在しない場合、起動失敗が繰り返された VM についてはバックオフ(backoff)時間を遵守し、そうでない場合は
startVMI()を呼び出します。
RerunOnFailure は Failed の場合のみ再起動し Succeeded ならそのままにするという違い、 Halted は VMI があれば削除のみ行う点、 Manual はサブリソースが投入した要求(第2回)のみに従うという違いが、それぞれの分岐にそのまま記載されています。
4.4 startVMI — VMI を作成する前に行うこと
startVMI()(:1257)は VMI を直ちには作成せず、まず5つの処理を行います。
SetupVMIFromVM()(:1868) — VM のテンプレートをコピーし、名前・ネームスペース・所有者(VM)を設定し、テンプレートにファームウェア UUID がない場合は VM 名から算出して挿入します(CalculateLegacyUUID)。VM を再起動してもゲストから見える UUID が変わらない理由がここにあります。createVMRevision()— 今回の起動に使用した VM スペックをControllerRevisionとして保存します。名前はrevision-start-vm-<VM UID>-<generation>(getVMRevisionName, :1698)です。後で VM スペックが変更された際に「再起動が必要である」(RestartRequired)という条件を計算するための基準となります。- VMI にファイナライザーを付与します。VMI が削除される前に VM コントローラーが必ず一度確認できるようにするためです。
- instancetype およびネットワークのデフォルトインターフェースを適用し、ネットワークを再検証します。
c.expectations.ExpectCreations(vmKey, 1)(:1333)により「1つ作成する予定」であることを記録してから VMI を作成します。インフォーマーが新しい VMI を検知すると、addVirtualMachineInstance()がCreationObservedに向けて期待値を引き下げます。
5. VMI コントローラー: ポッドを作成して引き渡す
5.1 lifecycle.go の sync
VMI コントローラーの判断は pkg/virt-controller/watch/vmi/lifecycle.go の sync()
(:66)にあります。VMI が削除中の場合はポッドを削除し、終了した場合はホットプラグ用のポッドのみを片付けます。ポッドがまだ存在せず、VMI が初めて処理される段階である場合は、いくつかの条件(DataVolume の準備完了、進行中のマイグレーションがないこと、バックエンドストレージの準備完了)を確認した上で、 RenderLaunchManifest()
(:156)でポッドを描画し、 createPod()
(:1076)で作成します。ポッドセキュリティ基準に抵触して拒否された場合は、「privileged ネームスペースが必要である」という分かりやすいエラーメッセージに書き換えてイベントに記録します。
5.2 updateStatus — 引き渡す瞬間
updateStatus()(:256)はフェーズ(phase)を移行させます。ポッドを作成すると Pending → Schedulingになり、続いて Scheduling において ポッドが Ready になったとき にボリュームやネットワークのステータスを埋め、フェーズを Scheduled に変更しながら VMI にラベル kubevirt.io/nodeName=<노드> と status.nodeName を付与します
(:421–426)。このラベルこそが第4篇の virt-handler が自身の処理対象を選択する基準となります。第2篇で確認した Scheduled のコメントの通り、ここでバトンが渡されます。
6. ポッドテンプレート: pkg/virt-controller/services/template.go
RenderLaunchManifest()(:355)はまずメモリーオーバーヘッドを計算し、実際の組み立ては renderLaunchManifest()
(:395)に委譲します。400行近いこの関数の中で注目すべき点は5つあります。
- ユーザー。 非root VMI の場合は UID 107(
qemu)で動作します。非root であるかどうかは VMI のstatus.runtimeUserによって決まり(vmitrait.IsNonRoot)、実測した VMI はすべて 107 でした。 - 猶予時間。 VMI の猶予時間(デフォルト30秒)に15秒を加算して virt-launcher に渡し、ポッドにはそこにさらに15秒を加算します (:411)。コメントの意図は、「VM のシャットダウン処理は virt-handler に行わせ、Kubernetes が先にポッドを強制終了させて VM を強制停止させないようにする」というものです。
- コマンド。 コンテナ
computeのコマンドはvirt-launcher-monitorとフラグのセットです (:460).--qemu-timeoutはデフォルト240秒 (application.go:115)に 0–119秒のランダムな値を加算します(qemuTimeoutJitterRange = 120, :101)。 複数の VM が同時にタイムアウトで終了しないように分散させるためです。 - サイドカー。 シリアルコンソールログを出力する
guest-console-logは initContainers にrestartPolicy: Alwaysとして組み込まれます (serialconsolelog.go:45)。Kubernetes の ネイティブサイドカーであるため、メインコンテナよりも先に起動し、後に終了します。コンテナディスクを使用する VM には、ディスクイメージを事前に取得する初期化コンテナが追加で付与されます(第5篇)。 - リソース。 CPU リクエストは vCPU 数 ÷
cpuAllocationRatio(デフォルト10)、メモリーリクエストはゲストメモリー + オーバーヘッドとなります。KVM のオーバーヘッド推定式はpkg/hypervisor/kvm/hypervisorbackend.goのGetMemoryOverhead()(:69)にあります。固定値5つ(virt-launcher-monitor 25Mi、virt-launcher 100Mi、virtlogd 25Mi、virtqemud 40Mi、QEMU 30Mi — :38)に対し、ゲストメモリーの 1/512(ページテーブル)、vCPU 当たり 8Mi、IO スレッド 8Mi、グラフィックスデバイス 32Mi が加算され、VFIO デバイスが存在する場合はさらに 1Gi が追加されます(第7篇)。
7. 実測
v1.9.0 が導入された小規模クラスターにおいて、実測により確認しました(2026-10-17)。
リーダー。 リース kubevirt/virt-controller の保持者は2つのポッドのうちのいずれかであり(リース期間15秒)、もう一方のポッドのログは起動時の「attempting to acquire leader lease」という1行で停止していました。待機中のポッドはリースのみを監視し、コントローラーは一切実行しません。
1回分の再起動ログ。 2026-10-13 に特定の VM(実行戦略 Always)の VMI が Failed で終了しました(終了した理由は本記事のスコープ外です)。リーダーのログから該当 VM の行のみを抽出すると、セクション4.3の分岐がそのまま確認できます。 pos の行番号はいずれも v1.9.0 のソースと一致しています。
| 時刻(UTC) | pos | メッセージ |
|---|---|---|
| 00:42:32.011 | vm.go:1005 |
Stopping VM with VMI in phase Failed and VM runStrategy: Always |
| 00:42:32.042 | vm.go:1685 |
Dispatching delete event for vmi … with phase Failed |
| 00:42:32.179 | vm.go:1032 |
Starting VM due to runStrategy: Always |
| 00:42:32.196 | vm.go:1341 |
Started VM by creating the new virtual machine instance … |
最初の判断から新しい VMI の作成まで 0.19秒です。削除と作成が 異なる reconcile で行われていることも確認できます(間に同 VM の Stopping の行がもう一度存在していました)。
新しい VMI に残された痕跡。 当該 VMI の status.virtualMachineRevisionName は revision-start-vm-<VM UID>-1 であり(セクション4.4の2)、
metadata.finalizers は kubevirt.io/virtualMachineControllerFinalize と kubevirt.io/foregroundDeleteVirtualMachine、注釈
kubevirt.io/vm-generation: "1"、ラベル kubevirt.io/nodeName がついていました。フェーズの記録(phaseTransitionTimestamps)は
Pending·Scheduling 00:42:32 → Scheduled 00:42:35 → Running 00:42:36 でした。
ポッドの仕様と計算式。 と同様の VMI の virt-launcher ポッド(2 vCPU·ゲスト 2Gi、ルートは PVC)と、実習用 VM 1つ(8 vCPU·8Gi、コンテナディスク)を 確認しました。
| 項目 | 2 vCPU · 2Gi | 8 vCPU · 8Gi | コード |
|---|---|---|---|
| CPU リクエスト | 200m | 800m | vCPU ÷ 10 |
| メモリリクエスト | 2328Mi | 8532Mi | ゲスト + オーバーヘッド |
オーバーヘッド(注釈 kubevirt.io/memory-overhead-bytes) |
293,601,280 B = 280Mi | 356,515,840 B = 340Mi | 以下の計算 |
--grace-period-seconds / ポッドの猶予 |
45 / 60 | 45 / 60 | 30+15, +15 |
--qemu-timeout |
283s | 309s | 240 + 0–119 |
| ユーザー | runAsUser 107, runAsNonRoot | 同等 | 非rootデフォルト |
オーバーヘッドの式を再計算すると、2Gi VM は固定 220Mi + ページテーブル 4Mi + vCPU 16Mi + IO スレッド 8Mi + グラフィックス 32Mi = 280Mi、 8Gi VM は 220 + 16 + 64 + 8 + 32 = 340Mi となり、実測の注釈とバイト単位で一致します。「学生 VM が起動するまで」第2篇の「約 280Mi」 が どこから来たのかはこれが理由です。
その他、ポッドには readiness gate kubevirt.io/virtual-machine-unpaused(VM を一時停止するとポッドが Ready から外れる)、デバイスリソース
devices.kubevirt.io/kvm·tun·vhost-net のリクエスト、そして guest-console-log 初期化コンテナ(restartPolicy: Always、CPU 5m·メモリ 35M)が
ありました。
8. 制限
- オーバーヘッドの式は リクエスト値です。実際に使用するメモリは第5篇で別途計測しました(プロセス別 RSS)。式の固定値 220Mi が実際と どれほど異なるかについてはそちらをご覧ください。
- 再起動ログは1回限りのイベントです。期待値が防いでくれた「二重作成」を実際に再現してはいません(未計測).
- マイグレーション、ホットプラグ、instancetype の同期は同じファイルに存在しますが、この記事の範囲からは除外しました。
Titi の一言。 コントローラーのコードを読むときは、「この関数が失敗したらどうなるのか?」を常に問いかけてください。大抵の答えは「名前をキューに 再度追加し、次回最初からやり直す」です。そのため、関数に多少の誤りがあっても最終的に正しい状態へと収束します!
参考
すべて 2026-10-17 に閲覧しました。
vm.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/virt-controller/watch/vm/vm.golifecycle.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/virt-controller/watch/vmi/lifecycle.gotemplate.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/virt-controller/services/template.go- KubeVirt ユーザーガイド、実行戦略: https://kubevirt.io/user-guide/compute/run_strategies/
- Kubernetes ドキュメント、ネイティブサイドカンテテナ: https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/
- 姉妹シリーズ「学生 VM が起動するまで」第2篇: KubeVirt の内部、第5篇: ライフサイクルと回収