TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

KubeVirt 源码阅读 3:virt-controller — VM 与 VMI 的 reconcile

状态:草稿 (2026-10-17),源码 + 只读实测。 基准为 KubeVirt v1.9.0(提交 79d34c17)。实测是通过读取安装了 v1.9.0 的小型集群的日志、对象和 Pod 规格完成的,并未创建或修改新的 VM。

KubeVirt 源码阅读 3:virt-controller

目录

1. 直觉:队列中只有名称,判断永远从头开始

Kubernetes 控制器的运作依据不是“发生了什么”,而是“现在的状态是什么”。事件处理器只会将变更对象的 名称(네임스페이스/이름)放入工作队列中,由工作线程取出该名称,从缓存中读取最新对象,然后从头重新计算“期望状态与当前状态的 差距”。即使相同的名称被传入十次也只会处理一次,如果失败则会延迟并将名称重新放回队列。

virt-controller 中的一个 VM 会经历这两个环节。

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. 代码路径:只有成为领导者才能工作

即使部署了两个 Pod,virt-controller 也只会让 单个领导者 运行控制器。在 application.go 的领导者选举回调 onStartedLeading()(:609)中 启动 Informer 并像 go vca.vmController.Run(...) 那样拉起控制器。工作线程数的默认值为:VMI 控制器 10 (:128),包括 VM 控制器在内的多数控制器为 3(:125)。

4. VM 控制器: pkg/virt-controller/watch/vm/vm.go

4.1 Execute — 从队列中取出 一个

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)会从缓存(Index)中读取 VM 并创建 副本,查找同名的 VMI 并验证所有权(ownerReference)(ClaimVirtualMachineInstanceByName),收集 DataVolume 列表后调用 sync() 和 updateStatus() 。如果直接修改缓存中的对象会污染其他工作线程看到的缓存,因此 KubeVirt 代码几乎总是 从 DeepCopy() 开始。这是代码审查中经常被指出的点。

sync()(:3215)的顺序如下。

  1. 检查期望值。 satisfiedExpectations() 如果 为假,则什么都不做直接返回。这是因为刚刚请求创建了 VMI,但该 VMI 尚未在 Informer 中可见。如果没有这个机制,当缓存延迟时会重复创建 VMI。
  2. 如果正在删除, 则停止 VMI 并移除终结器(finalizer)。否则附加终结器 kubevirt.io/virtualMachineControllerFinalize 。
  3. vm.RunStrategy()(第 2 篇)将运行策略折叠为单一策略,同步 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,而是先做五件事。

  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)记录“将创建一个”并创建 VMI。当 Informer 看到新的 VMI 时, addVirtualMachineInstance() 会将期望值减 CreationObserved 。

5. VMI 控制器:创建并移交 Pod

5.1 lifecycle.go 中的 sync

VMI 控制器的判断逻辑位于 pkg/virt-controller/watch/vmi/lifecycle.go 的 sync() (:66)中。如果 VMI 正在被删除,则删除 Pod;如果已结束,则清理热插拔专用的 Pod。如果尚无 Pod 且 VMI 正在经历首次处理,则在确认若干条件(DataVolume 准备就绪、无进行中的迁移、后端存储就绪)后,通过 RenderLaunchManifest() (:156绘制 Pod 并通过 createPod() (:1076进行创建。如果因 Pod 安全标准受阻而被拒绝,则会将其转换为“需要 privileged 命名空间”的友好错误并记录在事件中。

5.2 updateStatus — 移交的那一刻

updateStatus()(:256)负责迁移阶段(phase)。创建 Pod 后为 Pending → Scheduling,接着在 Scheduling 中,当 Pod 变为 Ready 时,填充卷与网络 status,将阶段更改为 Scheduled ,同时在 VMI 上写入标签 kubevirt.io/nodeName=<노드> 和 status.nodeName (:421–426)。这个标签正是第 4 篇中 virt-handler 用来选择自己工作内容的标准。正如第 2 篇中看到的 Scheduled 的注释一样,接力棒正是在这里完成交接的。

6. Pod 模板: 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 Pod 与内存开销

RenderLaunchManifest()(:355)首先计算内存开销,实际组装则交由 renderLaunchManifest() (:395)负责。在这个近乎 400 行的函数中,有五个重点需要关注。

7. 实测

在安装了 v1.9.0 的小型集群中通过读取进行了验证(2026-10-17)。

领导者。 租约(lease) kubevirt/virt-controller 的持有者是两个 Pod 中的一个(租约期限 15 秒),另一个 Pod 的日志停留在启动时的“attempting to acquire leader lease”这一行。处于等待状态的 Pod 只盯着租约,不运行任何控制器。

单次重启日志。 2026-10-13,某个 VM(运行策略 Always)的 VMI 以 Failed 结束(结束原因超出本文范围)。如果在领导者日志中只提取该 VM 的行,4.3 节的分支将一目了然。 pos 的行号与 v1.9.0 源码完全一致。

时间(UTC) 位置 消息
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。

Pod 规格与计算公式。 读取了类似 VMI 的 virt-launcher Pod(2 vCPU·Guest 2Gi,根目录使用 PVC)以及一个用于实操的 VM(8 vCPU·8Gi,容器磁盘)。

项目 2 vCPU · 2Gi 8 vCPU · 8Gi 代码
CPU 请求 200m 800m vCPU ÷ 10
内存请求 2328Mi 8532Mi Guest + 开销
开销(注释 kubevirt.io/memory-overhead-bytes) 293,601,280 B = 280Mi 356,515,840 B = 340Mi 下方计算
--grace-period-seconds / Pod 宽限期 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”正是来源于此。

此外,Pod 还包含就绪门槛(readiness gate kubevirt.io/virtual-machine-unpaused(暂停 VM 会使 Pod 退出 Ready 状态))、设备资源 devices.kubevirt.io/kvm·tun·vhost-net 请求,以及 guest-console-log 初始化容器(restartPolicy: Always, CPU 5m·内存 35M)。

8. 局限性

Titi 的小提示。 阅读控制器代码时,请不断自问:“如果这个函数失败了会怎么样?”通常答案总是:“将名称重新放回队列,下次从头开始处理。”因此,即使函数稍有瑕疵,最终也会收敛到正确状态!

参考资料

均于 2026-10-17 访问。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论