TT Lab
ブログ

한국어 English 日本語 中文

はじめる
ブログ

ブログ

KubeVirt ソースコード読み取り 3: virt-controller — VM と VMI の reconcile

ステータス: ドラフト(2026-10-17)、ソースコード + 読み取り専用の実測。 基準は KubeVirt v1.9.0(コミット 79d34c17)です。実測は v1.9.0 が導入された小規模クラスターのログ・オブジェクト・ポッドスペックを読み取ったものであり、VM の新規作成や変更は行っていません。

KubeVirt ソースコード読み取り 3: virt-controller

目次

1. 直感: キューには名前だけ、判断は常に最初から

Kubernetes のコントローラーは「何が起きたか」ではなく「現在の状態はどうなっているか」によって動作します。イベントハンドラーは変更されたオブジェクトの 名前(네임스페이스/이름)のみ 作業キューに入れ、ワーカーがその名前を取り出してキャッシュから最新のオブジェクトを読み込んだ後、「望ましい状態と現在の状態の 差分」を最初から再計算します。同じ名前が10回来ても1回しか処理されず、失敗した場合は名前をキューに遅延させてから再度入れます。

virt-controller における1つの VM は、これら2つのループを通過します。

2. 図

virt-controller — 두 컨트롤러가 이어 달린다 VM 컨트롤러가 VMI 를 만들고, VMI 컨트롤러가 파드를 만든다 VM 컨트롤러 pkg/virt-controller/watch/vm · 일꾼 3 인포머 VM·VMI·DV·PVC 작업 큐 키 = ns/이름 Execute() 실패 → 재시도 sync() — 종료자 · DataVolume 준비 · 리비전 syncRunStrategy() — Always · Halted · Manual … startVMI() → VMI 생성 stopVMI() → VMI 삭제 VMI 컨트롤러 watch/vmi · 일꾼 10 sync() 파드가 없으면 만든다 RenderLaunchManifest() services/template.go createPod() → virt-launcher 파드 updateStatus(): Pending → Scheduling → Scheduled 파드가 Ready 면 노드 이름을 적고 virt-handler 에 넘긴다 실측: 리스 virt-controller 는 한 파드가 쥐고, 다른 하나는 대기한다. 실측: Failed 로 끝난 VMI 를 Always 가 지우고(vm.go:1005) 0.19초 뒤 새 VMI 를 만들었다(vm.go:1341). 로그의 줄 번호가 v1.9.0 소스와 같다.
図: VM コントローラーと VMI コントローラーがバトンタッチする reconcile

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)の順序は以下の通りです。

  1. 期待値の確認。 satisfiedExpectations() が偽である場合は、何もせずに戻ります。直前に VMI の作成を要求したものの、その VMI がまだインフォーマーに反映されていない状態であるためです。この仕組みがない場合、キャッシュの反映が遅れた際に VMI が重複して作成されてしまいます。
  2. 削除中の場合、 VMI を停止してファイナライザー(finalizer)を外します。そうでなければファイナライザー kubevirt.io/virtualMachineControllerFinalize を付与します。
  3. vm.RunStrategy()(第2回)により実行戦略を1つにまとめ、instancetype の同期、DataVolume の作成・準備完了の確認を行います。
  4. syncRunStrategy() を用いて VMI を作成または削除します。

4.3 syncRunStrategy — Always 分岐

vm.go:970 の switch runStrategy がこのコントローラーの心臓部です。 Always(:979)の分岐を言葉に置き換えると以下のようになります。

RerunOnFailure は Failed の場合のみ再起動し Succeeded ならそのままにするという違い、 Halted は VMI があれば削除のみ行う点、 Manual はサブリソースが投入した要求(第2回)のみに従うという違いが、それぞれの分岐にそのまま記載されています。

4.4 startVMI — VMI を作成する前に行うこと

startVMI()(:1257)は VMI を直ちには作成せず、まず5つの処理を行います。

  1. SetupVMIFromVM()(:1868) — VM のテンプレートをコピーし、名前・ネームスペース・所有者(VM)を設定し、テンプレートにファームウェア UUID がない場合は VM 名から算出して挿入します(CalculateLegacyUUID)。VM を再起動してもゲストから見える UUID が変わらない理由がここにあります。
  2. createVMRevision() — 今回の起動に使用した VM スペックを ControllerRevision として保存します。名前は revision-start-vm-<VM UID>-<generation> (getVMRevisionName, :1698)です。後で VM スペックが変更された際に「再起動が必要である」(RestartRequired)という条件を計算するための基準となります。
  3. VMI にファイナライザーを付与します。VMI が削除される前に VM コントローラーが必ず一度確認できるようにするためです。
  4. instancetype およびネットワークのデフォルトインターフェースを適用し、ネットワークを再検証します。
  5. 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

template.go 가 그린 virt-launcher 파드 실측: 2 vCPU · 2Gi VM 하나(PVC 루트) — 괄호는 컨테이너 디스크 VM 에만 Pod virt-launcher-<vm>-xxxxx · restartPolicy Never terminationGracePeriodSeconds 60 = 30 + 15 + 15 initContainers guest-console-log (restartPolicy: Always) 네이티브 사이드카 · virt-tail · CPU 5m · 메모리 35M (container-disk-binary · volumeroot-init) 컨테이너 디스크를 미리 받아 두는 초기화 containers compute virt-launcher-monitor --name … --grace-period-seconds 45 요청: CPU 200m (= 2 vCPU ÷ 10) · 메모리 2328Mi = 게스트 2048Mi + 오버헤드 280Mi 장치: devices.kubevirt.io/kvm · tun · vhost-net (volumeroot — 디스크 이미지를 품은 컨테이너) volumes private public sockets libvirt-runtime ephemeral-disks container-disks hotplug-disks root ← PVC 오버헤드 280Mi 는 코드의 추정식과 정확히 같다: 고정 220Mi + 페이지 테이블 4Mi(2Gi ÷ 512) + vCPU 8Mi×2 + IO 스레드 8Mi + 비디오 32Mi (hypervisor/kvm GetMemoryOverhead) 8 vCPU · 8Gi 실습 VM 은 340Mi — 역시 식과 같다
図: template.go が描画した virt-launcher ポッドとメモリーオーバーヘッド

RenderLaunchManifest()(:355)はまずメモリーオーバーヘッドを計算し、実際の組み立ては renderLaunchManifest() (:395)に委譲します。400行近いこの関数の中で注目すべき点は5つあります。

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

Titi の一言。 コントローラーのコードを読むときは、「この関数が失敗したらどうなるのか?」を常に問いかけてください。大抵の答えは「名前をキューに 再度追加し、次回最初からやり直す」です。そのため、関数に多少の誤りがあっても最終的に正しい状態へと収束します!

参考

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

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

コメント

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

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