TT Lab
ブログ

한국어 English 日本語 中文

はじめる
ブログ

ブログ

KubeVirt ソースコードリーディング 4: virt-handler — ノードでのVMIの同期

ステータス: ドラフト (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のstatusに書き込みます。そして、ポッド 外でのみ実行できる作業(他のコンテナのディスクのアタッチ、ポッドのネットワークネームスペースでのデバイス作成)を担当します。virt-launcherの特権を小さく抑えようとする設計であり、レビューガイドにも「ノードレベルの特権操作はvirt-launcherではなくvirt-handlerで行う」という原則があります(パート8)。

2. 図

virt-handler — 노드 한 대의 VMI 를 맞추는 데몬 두 가지를 본다: 클러스터의 VMI(바라는 것)와 파드 안의 도메인(실제) 노드 · virt-handler (DaemonSet) VMI 인포머 라벨 kubevirt.io/nodeName = 이 노드만 도메인 인포머 notify 서버로 받은 이벤트 + 300초마다 resync 작업 큐 → execute(key) → sync() shutdown 지울 때 delete 정리할 때 update 시작 · 변경 handleStartingVMI() — 파드 밖에서만 할 수 있는 일 컨테이너 디스크를 launcher 쪽에 붙이고 확인 파드 네트워크 네임스페이스에 브리지 · tap 만들기 /dev/kvm 같은 장치 파일 소유자 맞추기 virt-launcher 파드 cmd 서버 (gRPC) …/sockets/launcher-sock SyncVirtualMachine notify 클라이언트 도메인 이벤트를 handler 로 보낸다 syncVirtualMachine() 결과는 updateVMIStatus() 가 VMI status 에 적는다 (phase · interfaces · volumeStatus · conditions) 실측 타임라인(같은 VM, 로그의 시각): +0.000s 「Scheduled · Domain does not exist」 — handler 가 넘겨받음 +0.006s 파드 네트워크 설계(nmstate) — k6t-eth0 브리지 +0.863s launcher 의 notify 파이프 연결 +1.103s 「Scheduled · Domain status: Running」
図: virt-handlerの2つのインフォーマー、syncの3つの分岐、virt-launcherとの通信

3. コードパス

3.1 2つのインフォーマー

cmd/virt-handler/virt-handler.go の Run()(:209)が組み立てます。

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)は、大部分において「処理しない理由」を選択します。

残りの場合のみ 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秒後ろにずらして戻ります。

  1. コンテナディスク。 ディスクイメージを内蔵するコンテナ(volumeroot)の準備ができたかを確認し(ContainerDisksReady)、そのコンテナのファイルを launcher の container-disks フォルダーに結合します(MountAndVerify, container-disk/mount.go:246)。 異なるコンテナのファイルシステムを結合する作業であるため、ポッド内では実行できません。
  2. ホットプラグボリュームをアタッチし、準備完了を待ちます。
  3. ネットワーク。 setupNetwork()(controller.go:363)が ポッドのネットワーク名前空間に入り、ブリッジとtapデバイスを作成します。希望する構成をnmstate形式で記述してから適用します(第5回の図)。
  4. デバイスの所有者。 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. 制限

Tティの一言。 virt-handlerを読むときは、「この処理はなぜlauncherではなくhandlerが行うのか?」と自問してみてください。ほぼ常に「ポッドの外側を操作する必要があるから」という理由に行き当たります。特権は1つのノードdaemonに集約し、VMごとに起動するポッドは軽量に保つという設計です!

参考

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

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

コメント

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

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