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 控制器在 Pod 准备就绪时将节点名称写入 VMI 并放手。此后,实际启动和关闭 VM 的工作就落在了该节点的 virt-handler 身上。virt-handler 也是一个控制器,但它的对比对象很独特。

通过对比两者,从中选择“关闭·删除·同步”之一,并将结果写回 VMI 的 status 中。并且负责只能在 Pod 外部进行的工作(挂载其他容器的磁盘、在 Pod 的网络命名空间中创建设备)。这是为了尽量降低 virt-launcher 特权的架构设计,审查指南中也有“节点级别的特权操作应该在 virt-handler 中,而不是在 virt-launcher 中”的原则(第 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 的两个 Informer、sync 的三个分支以及与 virt-launcher 的通信

3. 代码路径

3.1 两个 Informer

cmd/virt-handler/virt-handler.go 的 Run()(:209)进行组装。

由于两个 Informer 会将相同的键(네임스페이스/이름)放入队列,因此无论是 VMI 发生变化还是 Domain 发生变化,都会运行相同的 execute() 。

3.2 事件是如何离开 Pod 的

virt-launcher 内的 notify 客户端会连接到其文件系统中的 /var/run/kubevirt/domain-notify-pipe.sock 。这个 socket 不是由 launcher 创建的,而是 virt-handler 预先植入 Pod 中的。 pipe.InjectNotify() (notify-server/pipe/pipe.go:54)会在 Pod 的挂载目录中创建一个监听 socket,并将传入的连接转发给 handler 自己的 notify 服务(Proxy(), :104)。 notify 服务只有 HandleDomainEvent 和 HandleK8sEvent 这两个 RPC (notify.proto:6)。 launcher 不需要知道 handler 的 socket 路径,也没有权限查看。

反方向(handler → launcher)则是 launcher 打开的 cmd 服务。handler 将 kubelet 的 Pod 目录作为 /pods 挂载并使用 gRPC 连接到 /pods/<파드 UID>/volumes/kubernetes.io~empty-dir/sockets/launcher-sock (cmd-client/client.go:191)。 Cmd 服务(cmd.proto:6)包含 SyncVirtualMachine·ShutdownVirtualMachine·KillVirtualMachine·GetDomain·Ping 等 36 个 RPC。 由于 handler 和 launcher 的版本可能不一致(在升级过程中),修改此 proto 的 PR 必定会检查 是否与旧 launcher 兼容 是否与旧 launcher 兼容。

3.3 execute — 是否接管

execute()(vm.go:321)主要用来挑选“不予处理的理由”。

只有在剩余情况下才会进入 sync() 。

3.4 sync — 三个分支

sync()(vm.go:1433)首先会打印一行 VMI 和 Domain 的状态(: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 ,并按照注释中指定的顺序选择一个。顺序是 删除导致的关闭工作 → 清理已经关闭的虚拟机 → 规格变更与首次启动 删除导致的关闭工作 → 清理已经关闭的虚拟机 → 规格变更与首次启动(:1571 附近的 switch)。最后总是通过 updateVMIStatus() (:1097将从 Domain 读取的内容写入 VMI status。 阶段(phase)、接口列表、Guest OS 信息、卷状态、 AgentConnected 等条件都是在这里被填入的。

3.5 同步 — 只能在 Pod 外部进行的准备工作

shouldUpdate 分支将进入 processVmUpdate()(:2256) → vmUpdateHelperDefault()(:1933。如果 VMI 尚未处于 Running 状态,则 handleStartingVMI()(:2043会执行启动前的准备工作。只要有一项准备工作未完成,就会将键延迟 1 秒后重新处理。

  1. Container Disk。 检查包含磁盘镜像的容器(volumeroot)是否准备就绪(ContainerDisksReady),并将该容器的文件挂载到 launcher 的 container-disks 目录中(MountAndVerify, container-disk/mount.go:246)。因为这是连接不同容器文件系统的工作,所以在 Pod 内部是无法完成的。
  2. 热插拔卷并等待准备就绪。
  3. 网络。 setupNetwork()(controller.go:363)进入 Pod 的网络命名空间,创建网桥和 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 重新创建的一个 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 当前与期望的 Pod 网络(网桥 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 侧的 Domain 定义和启动合计耗时 0.2 秒(第 5 篇)。

5 分钟 resync。 在同一个 VM 的 handler 日志中,后续的行都像 …:03:25, …:08:25, …:13:25 那样精确地每 300 秒打印一次 (读取了 2026-10-16 全天的日志)。这就是 3.1 节提到的即使没有事件也会重新查询 Domain 的机制。 defaultDomainResyncPeriodSeconds = 300 。

status 中记录的内容。 该 VMI 的 status 中包含 interfaces[0].infoSource: domain, guest-agent, guestOSInfo(Guest 报告的操作系统名称和内核)、条件 AgentConnected(启动 8 秒后)。前两项是 handler 从 Domain 和 Guest Agent 中读取并写入的。

5. 局限性

Titi 的总结。 在阅读 virt-handler 时,请多问自己一句:“为什么这件事不由 launcher 做,而要由 handler 做?”答案几乎总是“因为需要触及 Pod 外部”。将特权集中到一个节点守护进程中,让每个 VM 运行的 Pod 保持轻量!

参考

均于 2026-10-17 访问。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论