ステータス: ドラフト (2026-10-17)、ソース + 読み取り専用の実測。 基準はKubeVirt
v1.9.0(コミット79d34c17)です。実測は小さなクラスターのvirt-handler・virt-launcherのログとVMI statusを読み取ったものです。
KubeVirt ソースコードリーディング 4: virt-handler
目次
1. 直感: 理想と現実のギャップをノードで埋める
3パートのVMIコントローラーは、ポッドの準備ができるとVMIにノード名を書き込んで手を離します。その後、VMを実際に起動したり停止させたりする作業は、そのノードのvirt-handlerの役割です。virt-handlerもコントローラーですが、比較対象が特殊です。
- あるべき姿: クラスターのVMIオブジェクト(仕様)。
- 現実: virt-launcherポッド内のlibvirtが保持しているドメイン(状態)。
両者を比較して「停止・削除・同期」のいずれかを選択し、その結果を再度VMIのstatusに書き込みます。そして、ポッド 外でのみ実行できる作業(他のコンテナのディスクのアタッチ、ポッドのネットワークネームスペースでのデバイス作成)を担当します。virt-launcherの特権を小さく抑えようとする設計であり、レビューガイドにも「ノードレベルの特権操作はvirt-launcherではなくvirt-handlerで行う」という原則があります(パート8)。
2. 図
3. コードパス
3.1 2つのインフォーマー
cmd/virt-handler/virt-handler.go の Run()(:209)が組み立てます。
- VMIインフォーマー。
factory.VMISourceHost(app.HostOverride)(:268)はラベルセレクターkubevirt.io/nodeName in (<내 노드>)でVMIを監視します (virtinformers.go:492)。1,000台のノードを持つクラスターで1,000個のvirt-handlerがすべてのVMIを監視すると、APIサーバーが耐えられなくなります。パート3の最後でVMIコントローラーが付与したラベルがここで使用されます。 - ドメインインフォーマー。
virtcache.NewSharedInformer(...)(:274)はKubernetesオブジェクトではなく このノードのvirt-launcherから送られてくるドメインイベントをインフォーマーのようにラップします。イベントの抜け漏れに備えてworker()(domain-watcher.go:73)が一定周期ごとにhandleResync()(:142を通じて各ランチャーのソケットにGetDomainを問い合わせて再充填します。周期はフラグ--domain-resync-period-secondsのデフォルト値である300秒 (virt-handler.go:118)です。
2つのインフォーマーが同じキー(네임스페이스/이름)をキューに入れるため、VMIが変更されてもドメインが変更されても同じ execute() が実行されます。
3.2 イベントはどのようにポッドの外に出るのか
virt-launcher内のnotifyクライアントは、自身のファイルシステムの /var/run/kubevirt/domain-notify-pipe.sock に接続します。このソケットはランチャーが作成したものではなく、 virt-handlerがポッド内に配置したものです。 pipe.InjectNotify()
(notify-server/pipe/pipe.go:54)がポッドのマウント内にリスニングソケットを作成し、着信した接続をハンドラー自身のnotifyサーバーに転送します(Proxy(), :104)。notifyサービスには HandleDomainEvent と HandleK8sEvent の2つのRPCのみが存在します (notify.proto:6)。ランチャーはハンドラーのソケットパスを知る必要も、閲覧する権限もありません。
反対方向(ハンドラー → ランチャー)は、ランチャーが開くcmdサーバーです。ハンドラーはkubeletのポッドフォルダーを /pods としてマウントしておき、
/pods/<파드 UID>/volumes/kubernetes.io~empty-dir/sockets/launcher-sock
(cmd-client/client.go:191)にgRPCで接続します。
Cmd サービス(cmd.proto:6)には
SyncVirtualMachine·ShutdownVirtualMachine·KillVirtualMachine·GetDomain·Ping など36個のRPCがあります。ハンドラーとランチャーのバージョンが異なる可能性があるため(アップグレード中)、このprotoを修正するPRでは 古いランチャーとも通信できるかが必ず確認されます。
3.3 execute — 引き継ぐかどうかの判断
execute()(vm.go:321)は、大部分において「処理しない理由」を選択します。
- VMIがローカルインフォーマーにない場合、グローバルインフォーマーからもう一度検索します。ドメインインフォーマーの方がVMIインフォーマーよりも早い場合があるためです。
- VMIは同じであるにもかかわらずドメインのUIDが異なる場合(同じ名前で再作成されたVM)、古いドメインを先に削除し、5秒後に再度確認します。
- マイグレーション中であるか、このノードのものではない場合(
isVMIOwnedByNode, :1829)、またはマイグレーションの送信元である場合は操作しません。送信元と宛先はmigration-source.go·migration-target.goの別のコントローラーが担当します。
残りの場合のみ sync() に進みます。
3.4 sync — 3つの分岐
sync()(vm.go:1433)はまず、VMIとドメインの状態を1行で
記録します(:1459).
c.logger.Object(vmi).Infof("VMI is in phase: %v | Domain status: %v, reason: %v", vmi.Status.Phase, domain.Status.Status, domain.Status.Reason)
出所: kubevirt/kubevirt v1.9.0 pkg/virt-handler/vm.go 1459行目(Apache-2.0)。続いて shouldShutdown·shouldDelete·shouldUpdate を計算し、コメントに示された順序に従って1つを選択します。 を削除するため、停止の処理 → 既に停止したものの整理 → スペック変更と最初の起動です
(:1571 付近の switch)。最後にいつも updateVMIStatus()
(:1097)によってドメインから読み取った内容をVMI statusに書き込みます。
フェーズ(phase)、インターフェース一覧、ゲストOS情報、ボリュームの状態、 AgentConnected のような条件がここで満たされます。
3.5 調整 — ポッドの外側でしか行えない準備
shouldUpdate の分岐は processVmUpdate()(:2256) →
vmUpdateHelperDefault()(:1933)へ向かいます。まだRunning
ではないVMIの場合、 handleStartingVMI()(:2043)が起動前の準備を行います。1つでも準備が整わない場合はキーを1秒後ろにずらして戻ります。
- コンテナディスク。 ディスクイメージを内蔵するコンテナ(
volumeroot)の準備ができたかを確認し(ContainerDisksReady)、そのコンテナのファイルを launcher のcontainer-disksフォルダーに結合します(MountAndVerify, container-disk/mount.go:246)。 異なるコンテナのファイルシステムを結合する作業であるため、ポッド内では実行できません。 - ホットプラグボリュームをアタッチし、準備完了を待ちます。
- ネットワーク。
setupNetwork()(controller.go:363)が ポッドのネットワーク名前空間に入り、ブリッジとtapデバイスを作成します。希望する構成をnmstate形式で記述してから適用します(第5回の図)。 - デバイスの所有者。
setupDevicesOwnerships()(controller.go:320)が/dev/kvmなどの所有者を非rootのQEMU(107)が使用できるように調整します。
準備が完了すると、 syncVirtualMachine()(:2123)がSMBIOSやメモリ
バルーニング周期などのクラスター設定をオプションに含め、 client.SyncVirtualMachine(vmi, options)
(cmd-client/client.go:332)を呼び出します。VMIは
JSONとして丸ごと渡され、その後の処理は第5回のvirt-launcherの担当となります。
4. 実測: 引き渡されてからRunningになるまで
2026-10-13に再作成された1つのVMI(第3回のあのVM)の起動について、virt-handlerとvirt-launcherのログを突き合わせて確認しました。時刻はログのUTCであり 「+」はvirt-handlerが最初に受け取った行からの経過時間です。
| +秒 | 誰が | pos | 内容 |
|---|---|---|---|
| −0.337 | launcher | virt-launcher.go:85 |
Marked as ready(cmdサーバーが起動) |
| 0.000 | handler | vm.go:1461 |
VMI is in phase: Scheduled | Domain does not exist |
| +0.006 | handler | netpod.go:187·244 |
現在・希望するポッドネットワーク(ブリッジ k6t-eth0) |
| +0.863 | handler | pipe.go:116 |
Accepted new notify pipe connection |
| +0.864 | launcher | manager.go:1651 |
Domain defined. |
| +1.057 | launcher | manager.go:1612 |
Domain started. |
| +1.060 | launcher | server.go:269 |
Synced vmi |
| +1.103 | handler | vm.go:1459 |
VMI is in phase: Scheduled | Domain status: Running |
| +1.136 | handler | vm.go:1459 |
VMI is in phase: Running | Domain status: Running |
handlerが受け取ってから1.1秒でVMIがRunningになりました。ネットワークの準備からnotifyパイプの接続までの0.86秒が最も長くなっています。 launcher側のドメイン定義と起動を合わせると0.2秒です(第5回)。
5分のresync。 同じVMのhandlerログにおいて、その後の行は …:03:25, …:08:25, …:13:25 のように正確に300秒ごとに記録されていました
(2026-10-16の1日分を読み込み)。第3.1節の defaultDomainResyncPeriodSeconds = 300 というイベントがなくても、ドメインに再度問い合わせる仕組みになっています。
statusに残る情報。 そのVMIのstatusには interfaces[0].infoSource: domain, guest-agent, guestOSInfo(ゲストが報告したOS名と
カーネル)、条件 AgentConnected(起動8秒後)が含まれていました。前者の2つはhandlerがドメインとゲストエージェントから読み取って書き込んだものです。
5. 制限
- 単一のノード、1回のVM起動におけるタイムラインです。ノードやVMのサイズ、コンテナディスクの有無による分布については 未計測 です。このVMは 未計測です。このVMは ルートがすでにプロビジョニングされたPVCであるため、コンテナディスクの段階はありませんでした。
- 停止と削除の分岐(
processVmShutdown·processVmDelete)は、今回はログで確認していません。 - マイグレーションコントローラー(
migration-source.go·migration-target.go)とデバイスマネージャー(第7回)は同じプロセス内に存在しますが、本記事のスコープからは除外しています。
Tティの一言。 virt-handlerを読むときは、「この処理はなぜlauncherではなくhandlerが行うのか?」と自問してみてください。ほぼ常に「ポッドの外側を操作する必要があるから」という理由に行き当たります。特権は1つのノードdaemonに集約し、VMごとに起動するポッドは軽量に保つという設計です!
参考
すべて2026-10-17に閲覧しました。
pkg/virt-handler/vm.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/virt-handler/vm.gocmd.proto·notify.proto: https://github.com/kubevirt/kubevirt/tree/v1.9.0/pkg/handler-launcher-com- レビューガイド(ノードコンポーネントのインフォーマ、特権操作の配置場所): https://github.com/kubevirt/kubevirt/blob/v1.9.0/docs/reviewer-guide.md
- 姉妹シリーズ 「学生VMが起動するまで」第1篇: 4つのレイヤーから見た全体像