KubeVirt virt-launcher 设备/网络/存储/外设层 超深度分析
分析范围:网络配置层、存储备份与CBT、GPU/vGPU/SR-IOV/DRA设备热插拔、virtiofs文件共享、EFI/SEV安全启动、Pre-migration Hook机制
目录
- 一、网络相关模块
- [1.1 converter/network/builder.go --- 域接口构建器](#1.1 converter/network/builder.go — 域接口构建器)
- [1.2 converter/network/configurator.go --- 域网络配置器](#1.2 converter/network/configurator.go — 域网络配置器)
- [1.3 converter/network/passt.go --- Passt端口转发](#1.3 converter/network/passt.go — Passt端口转发)
- [1.4 converter/network/virtio-queues.go --- VirtIO多队列](#1.4 converter/network/virtio-queues.go — VirtIO多队列)
- [1.5 network/manager.go --- 网络同步管理器](#1.5 network/manager.go — 网络同步管理器)
- [1.6 network/nichotplug.go --- 网卡热插拔](#1.6 network/nichotplug.go — 网卡热插拔)
- 二、存储相关模块
- [2.1 converter/storage/virtiofs.go --- virtiofs配置](#2.1 converter/storage/virtiofs.go — virtiofs配置)
- [2.2 disksource/disk_source.go --- 磁盘源解析](#2.2 disksource/disk_source.go — 磁盘源解析)
- [2.3 storage/manager.go --- 存储管理器](#2.3 storage/manager.go — 存储管理器)
- [2.4 storage/backup.go --- 虚拟机备份](#2.4 storage/backup.go — 虚拟机备份)
- [2.5 storage/backup_tunnel.go --- 备份隧道](#2.5 storage/backup_tunnel.go — 备份隧道)
- [2.6 storage/cbt.go --- 变更块追踪](#2.6 storage/cbt.go — 变更块追踪)
- [2.7 storage/fsfreeze.go --- 文件系统冻结](#2.7 storage/fsfreeze.go — 文件系统冻结)
- [2.8 storage/memoryDump.go --- 内存转储](#2.8 storage/memoryDump.go — 内存转储)
- 三、设备相关模块
- [3.1 hostdevice/hostdev.go --- 主机设备创建](#3.1 hostdevice/hostdev.go — 主机设备创建)
- [3.2 hostdevice/hotplug.go --- 设备热插拔](#3.2 hostdevice/hotplug.go — 设备热插拔)
- [3.3 hostdevice/addresspool.go --- 地址池](#3.3 hostdevice/addresspool.go — 地址池)
- [3.4 generic/ --- 通用主机设备](#3.4 generic/ — 通用主机设备)
- [3.5 gpu/ --- GPU设备](#3.5 gpu/ — GPU设备)
- [3.6 sriov/ --- SR-IOV设备](#3.6 sriov/ — SR-IOV设备)
- [3.7 dra/ --- DRA设备分配](#3.7 dra/ — DRA设备分配)
- [3.8 device/pciaddress.go --- PCI地址工具](#3.8 device/pciaddress.go — PCI地址工具)
- [3.9 device/usbaddress.go --- USB地址工具](#3.9 device/usbaddress.go — USB地址工具)
- 四、其他外设模块
- [4.1 efi/efi.go --- EFI固件](#4.1 efi/efi.go — EFI固件)
- [4.2 launchsecurity/sev.go --- SEV安全启动](#4.2 launchsecurity/sev.go — SEV安全启动)
- [4.3 cpudedicated/cpudedicated.go --- CPU绑核](#4.3 cpudedicated/cpudedicated.go — CPU绑核)
- [4.4 premigration-hook-server --- 迁移前Hook](#4.4 premigration-hook-server — 迁移前Hook)
- 五、核心交互流程图
一、网络相关模块
1.1 converter/network/builder.go --- 域接口构建器
模块定位
业务职责 :使用 Builder 模式构造 libvirt 域 XML 的 <interface> 元素。所有网卡(bridge/tap/passt/SR-IOV)在转换为 libvirt 域定义时,都通过此构建器组装属性。
系统位置 :位于 converter/network 包,是 configurator.go 的底层支撑------configurator 决定"填什么",builder 决定"怎么构造"。
核心结构
go
type builderOption func(p *api.Interface) // 函数选项模式
func newDomainInterface(name, modelType string, options ...builderOption) api.Interface
设计模式 :Functional Options(函数选项模式)。每个 withXxx 函数返回一个 builderOption 闭包,在 newDomainInterface 中逐一应用到 api.Interface 结构体。
逐行解析
go
// 核心构造函数:创建一个基础的 api.Interface,设置别名和型号
func newDomainInterface(name, modelType string, options ...builderOption) api.Interface {
iface := api.Interface{
Alias: api.NewUserDefinedAlias(name), // 用户定义的别名,格式 "ua-{name}"
Model: &api.Model{Type: modelType}, // 网卡型号,如 "virtio", "e1000"
}
// 逐一应用选项闭包,修改 iface 的各个字段
for _, f := range options {
f(&iface)
}
return iface
}
选项函数清单:
| 选项函数 | 作用 | 对应libvirt XML |
|---|---|---|
withDriver |
设置virtio驱动(vhost/queues/IOMMU) | <driver name="vhost" queues="N" iommu="on"/> |
withPCIAddress |
设置客户机PCI地址 | <address type="pci" .../> |
withACPIIndex |
设置ACPI索引 | <acpi index="N"/> |
withIfaceType |
设置接口类型(ethernet/vhostuser/bridge) | <interface type="ethernet"> |
withBootOrder |
设置PXE启动顺序 | <boot order="N"/> |
withROMDisabled |
禁用ROM(防止PXE ROM加载) | <rom enabled="no"/> |
withLinkStateDown |
链路状态设为down | <link state="down"/> |
withMACAddress |
设置MAC地址 | <mac address="xx:xx:xx:xx:xx:xx"/> |
withSource |
设置源设备(tap/passt socket) | <source dev="tap0"/> |
withBackend |
设置passt后端 | <backend type="passt" logfile="..."/> |
withPortForward |
设置passt端口转发规则 | <portForward proto="tcp">...</portForward> |
构建流程图
#mermaid-svg-dmFXCBwPnQYEYIZr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dmFXCBwPnQYEYIZr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dmFXCBwPnQYEYIZr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dmFXCBwPnQYEYIZr .error-icon{fill:#552222;}#mermaid-svg-dmFXCBwPnQYEYIZr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dmFXCBwPnQYEYIZr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dmFXCBwPnQYEYIZr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dmFXCBwPnQYEYIZr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dmFXCBwPnQYEYIZr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dmFXCBwPnQYEYIZr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dmFXCBwPnQYEYIZr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dmFXCBwPnQYEYIZr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dmFXCBwPnQYEYIZr .marker.cross{stroke:#333333;}#mermaid-svg-dmFXCBwPnQYEYIZr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dmFXCBwPnQYEYIZr p{margin:0;}#mermaid-svg-dmFXCBwPnQYEYIZr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dmFXCBwPnQYEYIZr .cluster-label text{fill:#333;}#mermaid-svg-dmFXCBwPnQYEYIZr .cluster-label span{color:#333;}#mermaid-svg-dmFXCBwPnQYEYIZr .cluster-label span p{background-color:transparent;}#mermaid-svg-dmFXCBwPnQYEYIZr .label text,#mermaid-svg-dmFXCBwPnQYEYIZr span{fill:#333;color:#333;}#mermaid-svg-dmFXCBwPnQYEYIZr .node rect,#mermaid-svg-dmFXCBwPnQYEYIZr .node circle,#mermaid-svg-dmFXCBwPnQYEYIZr .node ellipse,#mermaid-svg-dmFXCBwPnQYEYIZr .node polygon,#mermaid-svg-dmFXCBwPnQYEYIZr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dmFXCBwPnQYEYIZr .rough-node .label text,#mermaid-svg-dmFXCBwPnQYEYIZr .node .label text,#mermaid-svg-dmFXCBwPnQYEYIZr .image-shape .label,#mermaid-svg-dmFXCBwPnQYEYIZr .icon-shape .label{text-anchor:middle;}#mermaid-svg-dmFXCBwPnQYEYIZr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dmFXCBwPnQYEYIZr .rough-node .label,#mermaid-svg-dmFXCBwPnQYEYIZr .node .label,#mermaid-svg-dmFXCBwPnQYEYIZr .image-shape .label,#mermaid-svg-dmFXCBwPnQYEYIZr .icon-shape .label{text-align:center;}#mermaid-svg-dmFXCBwPnQYEYIZr .node.clickable{cursor:pointer;}#mermaid-svg-dmFXCBwPnQYEYIZr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dmFXCBwPnQYEYIZr .arrowheadPath{fill:#333333;}#mermaid-svg-dmFXCBwPnQYEYIZr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dmFXCBwPnQYEYIZr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dmFXCBwPnQYEYIZr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dmFXCBwPnQYEYIZr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dmFXCBwPnQYEYIZr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dmFXCBwPnQYEYIZr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dmFXCBwPnQYEYIZr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dmFXCBwPnQYEYIZr .cluster text{fill:#333;}#mermaid-svg-dmFXCBwPnQYEYIZr .cluster span{color:#333;}#mermaid-svg-dmFXCBwPnQYEYIZr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dmFXCBwPnQYEYIZr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dmFXCBwPnQYEYIZr rect.text{fill:none;stroke-width:0;}#mermaid-svg-dmFXCBwPnQYEYIZr .icon-shape,#mermaid-svg-dmFXCBwPnQYEYIZr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dmFXCBwPnQYEYIZr .icon-shape p,#mermaid-svg-dmFXCBwPnQYEYIZr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dmFXCBwPnQYEYIZr .icon-shape .label rect,#mermaid-svg-dmFXCBwPnQYEYIZr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dmFXCBwPnQYEYIZr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dmFXCBwPnQYEYIZr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dmFXCBwPnQYEYIZr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} newDomainInterface
创建基础 Interface
Alias=ua-name, Model=modelType
遍历 builderOption
withDriver → Driver
withPCIAddress → Address
withIfaceType → Type
withMACAddress → MAC
withSource → Source
withBackend → Backend
withPortForward → PortForward
withROMDisabled → Rom
withLinkStateDown → LinkState
withBootOrder → BootOrder
withACPIIndex → ACPI
D..N
返回完整 api.Interface
1.2 converter/network/configurator.go --- 域网络配置器
模块定位
业务职责 :将 VMI 网络规格(v1.Interface + v1.Network)转换为 libvirt 域 XML 的网络接口设备列表。核心决策点------根据绑定类型(Tap/Passt/SR-IOV/自定义Binding)选择不同的配置路径。
系统位置 :被 converter 主转换流程调用,在网络转换阶段为域 XML 填充 <devices><interface> 列表。
核心结构
go
type DomainConfigurator struct {
domainAttachmentByInterfaceName map[string]string // 接口名→域附加类型映射
useLaunchSecuritySEV bool // 是否启用SEV
useLaunchSecurityPV bool // 是否启用SEV-ES/PV
isROMTuningSupported bool // 是否支持ROM调优
virtioModel string // virtio型号(virtio/virtio-transitional)
}
构造方式:同样使用 Functional Options 模式:
WithDomainAttachmentByInterfaceName--- 注入接口到附加类型映射WithUseLaunchSecuritySEV/PV--- 标记安全启动状态WithROMTuningSupport--- 控制ROM禁用WithVirtioModel--- 注入virtio型号字符串
核心方法深度解析
Configure --- 主入口方法
go
func (d DomainConfigurator) Configure(vmi *v1.VirtualMachineInstance, domain *api.Domain) error {
var domainInterfaces []api.Interface
// 1. 过滤掉 State=Absent 的接口(已标记删除的接口不参与配置)
nonAbsentIfaces := netvmispec.FilterInterfacesSpec(vmi.Spec.Domain.Devices.Interfaces, func(iface v1.Interface) bool {
return iface.State != v1.InterfaceStateAbsent
})
// 2. 只保留匹配的非Absent网络
nonAbsentNets := netvmispec.FilterNetworksByInterfaces(vmi.Spec.Networks, nonAbsentIfaces)
networks := indexNetworksByName(nonAbsentNets) // 建立名字→网络的索引
for i, iface := range nonAbsentIfaces {
_, isExist := networks[iface.Name]
if !isExist {
return fmt.Errorf("failed to find network %s", iface.Name) // 接口必须有对应网络
}
// 关键跳过逻辑:
// - 自定义Binding(非Tap类型)→ 跳过,由外部控制器处理
// - SR-IOV接口 → 跳过,由SR-IOV hostdev机制处理
if (iface.Binding != nil && d.domainAttachmentByInterfaceName[iface.Name] != string(v1.Tap)) || iface.SRIOV != nil {
continue
}
// 3. 对每个需要配置的接口,生成域接口定义
domainIface, err := d.configureInterface(&nonAbsentIfaces[i], vmi)
if err != nil {
return err
}
domainInterfaces = append(domainInterfaces, domainIface)
}
// 4. 将生成的接口列表写入域定义
domain.Spec.Devices.Interfaces = domainInterfaces
return nil
}
跳过条件的关键理解:
iface.Binding != nil && attachment != Tap:自定义绑定(如 Macvtap、Slirp),这些不在 libvirt 域 XML 中配置iface.SRIOV != nil:SR-IOV 设备通过<hostdev>传递,不走<interface>通道
configureInterface --- 单接口配置
go
func (d DomainConfigurator) configureInterface(iface *v1.Interface, vmi *v1.VirtualMachineInstance) (api.Interface, error) {
var builderOptions []builderOption
useLaunchSecurity := d.useLaunchSecuritySEV || d.useLaunchSecurityPV
// 1. 确定接口类型和型号
ifaceType := getInterfaceType(iface) // 优先取 iface.Model,默认 VirtIO
modelType := ifaceType
if ifaceType == v1.VirtIO {
modelType = d.virtioModel // 可能是 "virtio-transitional"
builderOptions = append(builderOptions, withDriver( // VirtIO需要设置driver
newVirtioDriver(vmi, useLaunchSecurity))) // vhost + 多队列 + IOMMU
}
// 2. PCI地址(用户可指定客户机内PCI槽位)
if iface.PciAddress != "" {
addr, err := device.NewPciAddressField(iface.PciAddress)
if err != nil { return api.Interface{}, err }
builderOptions = append(builderOptions, withPCIAddress(addr))
}
// 3. ACPI索引
if iface.ACPIIndex > 0 {
builderOptions = append(builderOptions, withACPIIndex(uint(iface.ACPIIndex)))
}
// 4. MAC地址
if iface.MacAddress != "" {
builderOptions = append(builderOptions, withMACAddress(iface.MacAddress))
}
// 5. 根据绑定类型添加特定选项
switch {
case d.domainAttachmentByInterfaceName[iface.Name] == string(v1.Tap):
// Tap绑定 → ethernet类型 + ROM/LinkState/BootOrder
builderOptions = append(builderOptions, d.tapBindingOptions(iface, useLaunchSecurity)...)
case iface.PasstBinding != nil:
// Passt绑定 → vhostuser类型 + socket源 + 后端 + 端口转发
passtOpts, err := d.passtBindingOptions(iface, vmi)
if err != nil { return api.Interface{}, err }
builderOptions = append(builderOptions, passtOpts...)
}
return newDomainInterface(iface.Name, modelType, builderOptions...), nil
}
tapBindingOptions --- Tap绑定选项
go
func (d DomainConfigurator) tapBindingOptions(iface *v1.Interface, useLaunchSecurity bool) []builderOption {
// Tap使用 libvirt "ethernet" 类型 --- 直接使用预配置的tap设备
opts := []builderOption{withIfaceType("ethernet")}
// PXE启动顺序
if iface.BootOrder != nil {
opts = append(opts, withBootOrder(*iface.BootOrder))
}
// ROM禁用条件:支持ROM调优 且 (无BootOrder 或 使用SEV)
// SEV环境下必须禁用ROM,因为PXE ROM无法在加密内存中运行
if d.isROMTuningSupported && (iface.BootOrder == nil || useLaunchSecurity) {
opts = append(opts, withROMDisabled())
}
// 链路状态Down(接口存在但不联网)
if iface.State == v1.InterfaceStateLinkDown {
opts = append(opts, withLinkStateDown())
}
return opts
}
passtBindingOptions --- Passt绑定选项
go
func (d DomainConfigurator) passtBindingOptions(iface *v1.Interface, vmi *v1.VirtualMachineInstance) ([]builderOption, error) {
// 必须从VMI状态获取Pod接口名(passt需要知道宿主侧的接口)
ifaceStatus := netvmispec.LookupInterfaceStatusByName(vmi.Status.Interfaces, iface.Name)
if ifaceStatus == nil || ifaceStatus.PodInterfaceName == "" {
return nil, fmt.Errorf("pod interface name not found...")
}
return []builderOption{
withIfaceType("vhostuser"), // vhost-user 类型
withSource(api.InterfaceSource{Device: ifaceStatus.PodInterfaceName}), // socket设备路径
withBackend(api.InterfaceBackend{Type: "passt", LogFile: "/var/run/kubevirt/passt.log"}),
withPortForward(generatePasstPortForward(iface, vmi)), // 端口转发规则
}, nil
}
Passt vs Tap 的关键差异:
- Tap:libvirt
ethernet类型,直接使用宿主机 tap 设备 - Passt:libvirt
vhostuser类型,通过 Unix socket 与 passt 进程通信,支持端口级别转发
newVirtioDriver --- VirtIO驱动配置
go
func newVirtioDriver(vmi *v1.VirtualMachineInstance, requiresIOMMU bool) *api.InterfaceDriver {
var driver *api.InterfaceDriver
queueCount := uint(NetworkQueuesCapacity(vmi)) // 计算多队列数
if queueCount > 0 || requiresIOMMU {
driver = &api.InterfaceDriver{Name: "vhost"} // 使用vhost内核加速
if queueCount > 0 {
driver.Queues = &queueCount // 多队列配置
}
if requiresIOMMU {
driver.IOMMU = "on" // SEV环境下启用IOMMU
}
}
return driver
}
网络配置决策流程图
#mermaid-svg-VrYRuWwaZzQYjNKK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VrYRuWwaZzQYjNKK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VrYRuWwaZzQYjNKK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VrYRuWwaZzQYjNKK .error-icon{fill:#552222;}#mermaid-svg-VrYRuWwaZzQYjNKK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VrYRuWwaZzQYjNKK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VrYRuWwaZzQYjNKK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VrYRuWwaZzQYjNKK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VrYRuWwaZzQYjNKK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VrYRuWwaZzQYjNKK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VrYRuWwaZzQYjNKK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VrYRuWwaZzQYjNKK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VrYRuWwaZzQYjNKK .marker.cross{stroke:#333333;}#mermaid-svg-VrYRuWwaZzQYjNKK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VrYRuWwaZzQYjNKK p{margin:0;}#mermaid-svg-VrYRuWwaZzQYjNKK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VrYRuWwaZzQYjNKK .cluster-label text{fill:#333;}#mermaid-svg-VrYRuWwaZzQYjNKK .cluster-label span{color:#333;}#mermaid-svg-VrYRuWwaZzQYjNKK .cluster-label span p{background-color:transparent;}#mermaid-svg-VrYRuWwaZzQYjNKK .label text,#mermaid-svg-VrYRuWwaZzQYjNKK span{fill:#333;color:#333;}#mermaid-svg-VrYRuWwaZzQYjNKK .node rect,#mermaid-svg-VrYRuWwaZzQYjNKK .node circle,#mermaid-svg-VrYRuWwaZzQYjNKK .node ellipse,#mermaid-svg-VrYRuWwaZzQYjNKK .node polygon,#mermaid-svg-VrYRuWwaZzQYjNKK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VrYRuWwaZzQYjNKK .rough-node .label text,#mermaid-svg-VrYRuWwaZzQYjNKK .node .label text,#mermaid-svg-VrYRuWwaZzQYjNKK .image-shape .label,#mermaid-svg-VrYRuWwaZzQYjNKK .icon-shape .label{text-anchor:middle;}#mermaid-svg-VrYRuWwaZzQYjNKK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VrYRuWwaZzQYjNKK .rough-node .label,#mermaid-svg-VrYRuWwaZzQYjNKK .node .label,#mermaid-svg-VrYRuWwaZzQYjNKK .image-shape .label,#mermaid-svg-VrYRuWwaZzQYjNKK .icon-shape .label{text-align:center;}#mermaid-svg-VrYRuWwaZzQYjNKK .node.clickable{cursor:pointer;}#mermaid-svg-VrYRuWwaZzQYjNKK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VrYRuWwaZzQYjNKK .arrowheadPath{fill:#333333;}#mermaid-svg-VrYRuWwaZzQYjNKK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VrYRuWwaZzQYjNKK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VrYRuWwaZzQYjNKK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VrYRuWwaZzQYjNKK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VrYRuWwaZzQYjNKK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VrYRuWwaZzQYjNKK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VrYRuWwaZzQYjNKK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VrYRuWwaZzQYjNKK .cluster text{fill:#333;}#mermaid-svg-VrYRuWwaZzQYjNKK .cluster span{color:#333;}#mermaid-svg-VrYRuWwaZzQYjNKK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VrYRuWwaZzQYjNKK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VrYRuWwaZzQYjNKK rect.text{fill:none;stroke-width:0;}#mermaid-svg-VrYRuWwaZzQYjNKK .icon-shape,#mermaid-svg-VrYRuWwaZzQYjNKK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VrYRuWwaZzQYjNKK .icon-shape p,#mermaid-svg-VrYRuWwaZzQYjNKK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VrYRuWwaZzQYjNKK .icon-shape .label rect,#mermaid-svg-VrYRuWwaZzQYjNKK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VrYRuWwaZzQYjNKK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VrYRuWwaZzQYjNKK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VrYRuWwaZzQYjNKK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SR-IOV
自定义Binding非Tap
Tap绑定
Passt绑定
是
否
是
VMI网络配置
过滤Absent接口
遍历每个接口
绑定类型判断
跳过 → hostdev处理
跳过 → 外部控制器
Tap配置路径
Passt配置路径
type=ethernet
VirtIO? → vhost driver + queues
PCI地址? → withPCIAddress
SEV或无BootOrder?
ROM disabled
ROM enabled
LinkDown?
link state=down
type=vhostuser
source dev=podIfaceName
backend type=passt
portForward规则
builder构建 api.Interface
1.3 converter/network/passt.go --- Passt端口转发
模块定位
业务职责 :生成 passt 的端口转发规则(<portForward> XML),将指定端口的流量从宿主机转发到虚拟机。同时处理 Istio sidecar 代理的端口排除。
逐行解析
go
func generatePasstPortForward(iface *v1.Interface, vmi *v1.VirtualMachineInstance) []api.InterfacePortForward {
// 1. 检测Istio sidecar是否注入
istioProxyInjectionEnabled := false
if val, ok := vmi.GetAnnotations()["sidecar.istio.io/inject"]; ok {
istioProxyInjectionEnabled = strings.EqualFold(val, "true")
}
var tcpPortsRange, udpPortsRange []api.InterfacePortForwardRange
// 2. 如果Istio启用,排除Istio保留端口(15000-15006等)
if istioProxyInjectionEnabled {
for _, port := range istio.ReservedPorts() {
tcpPortsRange = append(tcpPortsRange, api.InterfacePortForwardRange{
Start: port, Exclude: "yes", // 标记为排除
})
}
}
// 3. 遍历用户定义的端口
for _, port := range iface.Ports {
portNumber := port.Port
if portNumber < 0 { continue } // 非法端口号跳过
if strings.EqualFold(port.Protocol, "tcp") || port.Protocol == "" {
// 默认协议为TCP
tcpPortsRange = append(tcpPortsRange, api.InterfacePortForwardRange{Start: uint(portNumber)})
} else if strings.EqualFold(port.Protocol, "udp") {
udpPortsRange = append(udpPortsRange, api.InterfacePortForwardRange{Start: uint(portNumber)})
}
// 其他协议passt不支持
}
// 4. 如果没有指定任何端口,则转发所有端口(全端口透传)
var portsFwd []api.InterfacePortForward
if len(udpPortsRange) == 0 && len(tcpPortsRange) == 0 {
portsFwd = append(portsFwd,
api.InterfacePortForward{Proto: "tcp"}, // 无Range表示全端口
api.InterfacePortForward{Proto: "udp"},
)
}
// 5. 有指定端口时,按协议分别添加
if len(tcpPortsRange) > 0 {
portsFwd = append(portsFwd, api.InterfacePortForward{Proto: "tcp", Ranges: tcpPortsRange})
}
if len(udpPortsRange) > 0 {
portsFwd = append(portsFwd, api.InterfacePortForward{Proto: "udp", Ranges: udpPortsRange})
}
return portsFwd
}
关键逻辑:
- 无端口指定 → 全端口透传(passt 默认行为)
- 有Istio → 排除Istio保留端口,避免端口冲突
- Exclude="yes" → passt不会转发这些端口的流量
Passt端口转发流程图
#mermaid-svg-iM0vBds70sNF70HY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iM0vBds70sNF70HY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iM0vBds70sNF70HY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iM0vBds70sNF70HY .error-icon{fill:#552222;}#mermaid-svg-iM0vBds70sNF70HY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iM0vBds70sNF70HY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iM0vBds70sNF70HY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iM0vBds70sNF70HY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iM0vBds70sNF70HY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iM0vBds70sNF70HY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iM0vBds70sNF70HY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iM0vBds70sNF70HY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iM0vBds70sNF70HY .marker.cross{stroke:#333333;}#mermaid-svg-iM0vBds70sNF70HY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iM0vBds70sNF70HY p{margin:0;}#mermaid-svg-iM0vBds70sNF70HY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iM0vBds70sNF70HY .cluster-label text{fill:#333;}#mermaid-svg-iM0vBds70sNF70HY .cluster-label span{color:#333;}#mermaid-svg-iM0vBds70sNF70HY .cluster-label span p{background-color:transparent;}#mermaid-svg-iM0vBds70sNF70HY .label text,#mermaid-svg-iM0vBds70sNF70HY span{fill:#333;color:#333;}#mermaid-svg-iM0vBds70sNF70HY .node rect,#mermaid-svg-iM0vBds70sNF70HY .node circle,#mermaid-svg-iM0vBds70sNF70HY .node ellipse,#mermaid-svg-iM0vBds70sNF70HY .node polygon,#mermaid-svg-iM0vBds70sNF70HY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iM0vBds70sNF70HY .rough-node .label text,#mermaid-svg-iM0vBds70sNF70HY .node .label text,#mermaid-svg-iM0vBds70sNF70HY .image-shape .label,#mermaid-svg-iM0vBds70sNF70HY .icon-shape .label{text-anchor:middle;}#mermaid-svg-iM0vBds70sNF70HY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iM0vBds70sNF70HY .rough-node .label,#mermaid-svg-iM0vBds70sNF70HY .node .label,#mermaid-svg-iM0vBds70sNF70HY .image-shape .label,#mermaid-svg-iM0vBds70sNF70HY .icon-shape .label{text-align:center;}#mermaid-svg-iM0vBds70sNF70HY .node.clickable{cursor:pointer;}#mermaid-svg-iM0vBds70sNF70HY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iM0vBds70sNF70HY .arrowheadPath{fill:#333333;}#mermaid-svg-iM0vBds70sNF70HY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iM0vBds70sNF70HY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iM0vBds70sNF70HY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iM0vBds70sNF70HY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iM0vBds70sNF70HY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iM0vBds70sNF70HY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iM0vBds70sNF70HY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iM0vBds70sNF70HY .cluster text{fill:#333;}#mermaid-svg-iM0vBds70sNF70HY .cluster span{color:#333;}#mermaid-svg-iM0vBds70sNF70HY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iM0vBds70sNF70HY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iM0vBds70sNF70HY rect.text{fill:none;stroke-width:0;}#mermaid-svg-iM0vBds70sNF70HY .icon-shape,#mermaid-svg-iM0vBds70sNF70HY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iM0vBds70sNF70HY .icon-shape p,#mermaid-svg-iM0vBds70sNF70HY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iM0vBds70sNF70HY .icon-shape .label rect,#mermaid-svg-iM0vBds70sNF70HY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iM0vBds70sNF70HY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iM0vBds70sNF70HY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iM0vBds70sNF70HY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
TCP或空
UDP
其他
无
有
VMI端口规格
Istio注入?
排除Istio保留端口
Exclude=yes
直接处理
遍历端口列表
协议类型
TCP端口范围
UDP端口范围
忽略/报错
有端口规则?
全端口透传
tcp+udp无Range
按协议生成
portForward条目
1.4 converter/network/virtio-queues.go --- VirtIO多队列
模块定位
业务职责:计算 virtio-net 网卡的多队列数量,与 vCPU 数量对齐以提升网络性能。
逐行解析
go
const MultiQueueMaxQueues = uint32(256) // tap设备最大队列数
func NetworkQueuesCapacity(vmi *v1.VirtualMachineInstance) uint32 {
// 1. 未启用MultiQueue → 返回0(不设置队列数)
if !isTrue(vmi.Spec.Domain.Devices.NetworkInterfaceMultiQueue) {
return 0
}
// 2. 根据CPU拓扑计算vCPU数
cpuTopology := vcpu.GetCPUTopology(vmi)
queueNumber := vcpu.CalculateRequestedVCPUs(cpuTopology)
// 3. 上限256(Linux tap设备限制)
if queueNumber > MultiQueueMaxQueues {
log.Log.Infof("Capped queues to %d", MultiQueueMaxQueues)
queueNumber = MultiQueueMaxQueues
}
return queueNumber
}
func isTrue(networkInterfaceMultiQueue *bool) bool {
return (networkInterfaceMultiQueue != nil) && (*networkInterfaceMultiQueue)
}
性能原理:virtio-net 多队列允许每个 vCPU 有独立的收发队列,避免单队列下的锁竞争。队列数 = vCPU数,上限 256。
1.5 network/manager.go --- 网络同步管理器
模块定位
业务职责 :在 VMI 生命周期中同步网络状态------热插新网卡、热拔Absent网卡、更新链路状态。是 virtwrap 层网络热插拔的编排器。
系统位置 :被 SyncVMI 流程调用,在域已运行后执行增量网络变更。
逐行解析
go
type domainClient interface {
AttachDeviceFlags(xml string, flags libvirt.DomainDeviceModifyFlags) error
UpdateDeviceFlags(xml string, flags libvirt.DomainDeviceModifyFlags) error
DetachDeviceFlags(xml string, flags libvirt.DomainDeviceModifyFlags) error
Free() error
}
func Sync(
domain *api.Domain, // 期望的域定义
oldSpec *api.DomainSpec, // 当前运行的域规格
dom domainClient, // libvirt域客户端
vmi *v1.VirtualMachineInstance, // VMI对象
domainAttachments map[string]string, // 接口→附加类型映射
) error {
if !vmi.IsRunning() {
return nil // VMI未运行,无需同步
}
// 创建网络配置器
networkConfigurator := netsetup.NewVMNetworkConfigurator(vmi, cache.CacheCreator{},
netsetup.WithDomainAttachments(domainAttachments))
// 创建接口管理器
networkInterfaceManager := newVirtIOInterfaceManager(dom, networkConfigurator)
// 三步同步:
// 1. 热插:新接口 → AttachDevice
if err := networkInterfaceManager.hotplugVirtioInterface(vmi, &api.Domain{Spec: *oldSpec}, domain); err != nil {
return err
}
// 2. 热拔:Absent接口 → DetachDevice
if err := networkInterfaceManager.hotUnplugVirtioInterface(vmi, &api.Domain{Spec: *oldSpec}); err != nil {
return err
}
// 3. 链路状态更新:LinkDown/LinkUp → UpdateDevice
if err := networkInterfaceManager.updateDomainLinkState(&api.Domain{Spec: *oldSpec}, domain); err != nil {
return err
}
return nil
}
网络同步总体流程图
Libvirt vmConfigurator virtIOInterfaceManager network/manager SyncVMI Libvirt vmConfigurator virtIOInterfaceManager network/manager SyncVMI #mermaid-svg-W7BTTSYslCHsODJ4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-W7BTTSYslCHsODJ4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-W7BTTSYslCHsODJ4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-W7BTTSYslCHsODJ4 .error-icon{fill:#552222;}#mermaid-svg-W7BTTSYslCHsODJ4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-W7BTTSYslCHsODJ4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-W7BTTSYslCHsODJ4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-W7BTTSYslCHsODJ4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-W7BTTSYslCHsODJ4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-W7BTTSYslCHsODJ4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-W7BTTSYslCHsODJ4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-W7BTTSYslCHsODJ4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-W7BTTSYslCHsODJ4 .marker.cross{stroke:#333333;}#mermaid-svg-W7BTTSYslCHsODJ4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-W7BTTSYslCHsODJ4 p{margin:0;}#mermaid-svg-W7BTTSYslCHsODJ4 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-W7BTTSYslCHsODJ4 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-W7BTTSYslCHsODJ4 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-W7BTTSYslCHsODJ4 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-W7BTTSYslCHsODJ4 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-W7BTTSYslCHsODJ4 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-W7BTTSYslCHsODJ4 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-W7BTTSYslCHsODJ4 .sequenceNumber{fill:white;}#mermaid-svg-W7BTTSYslCHsODJ4 #sequencenumber{fill:#333;}#mermaid-svg-W7BTTSYslCHsODJ4 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-W7BTTSYslCHsODJ4 .messageText{fill:#333;stroke:none;}#mermaid-svg-W7BTTSYslCHsODJ4 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-W7BTTSYslCHsODJ4 .labelText,#mermaid-svg-W7BTTSYslCHsODJ4 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-W7BTTSYslCHsODJ4 .loopText,#mermaid-svg-W7BTTSYslCHsODJ4 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-W7BTTSYslCHsODJ4 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-W7BTTSYslCHsODJ4 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-W7BTTSYslCHsODJ4 .noteText,#mermaid-svg-W7BTTSYslCHsODJ4 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-W7BTTSYslCHsODJ4 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-W7BTTSYslCHsODJ4 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-W7BTTSYslCHsODJ4 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-W7BTTSYslCHsODJ4 .actorPopupMenu{position:absolute;}#mermaid-svg-W7BTTSYslCHsODJ4 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-W7BTTSYslCHsODJ4 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-W7BTTSYslCHsODJ4 .actor-man circle,#mermaid-svg-W7BTTSYslCHsODJ4 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-W7BTTSYslCHsODJ4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Sync(domain, oldSpec, dom, vmi, attachments) hotplugVirtioInterface networksToHotplug (VMI状态有MultusStatus但域中不存在) SetupPodNetworkPhase2(新接口) 域接口定义 AttachDeviceFlags(LIVE|CONFIG) hotUnplugVirtioInterface interfacesToHotUnplug (State=Absent) DetachDeviceFlags(LIVE|CONFIG) updateDomainLinkState 对比当前/期望链路状态 UpdateDeviceFlags(LIVE|CONFIG)
1.6 network/nichotplug.go --- 网卡热插拔
模块定位
业务职责:实现 virtio 网卡的热插拔(Attach/Detach)、链路状态更新,以及接口占位符机制(为热插预留PCI控制器资源)。
核心结构
go
type virtIOInterfaceManager struct {
dom domainClient // libvirt域操作客户端
configurator vmConfigurator // 网络配置器接口
}
const affectDeviceLiveAndConfigLibvirtFlags = libvirt.DOMAIN_DEVICE_MODIFY_LIVE | libvirt.DOMAIN_DEVICE_MODIFY_CONFIG
// 同时修改运行时和持久化配置
hotplugVirtioInterface --- 热插网卡
go
func (vim *virtIOInterfaceManager) hotplugVirtioInterface(vmi *v1.VirtualMachineInstance, currentDomain, updatedDomain *api.Domain) error {
// 1. 找出需要热插的网络(VMI状态有MultusStatus + 域中不存在 + 非Absent + 非SR-IOV)
for _, network := range networksToHotplugWhoseInterfacesAreNotInTheDomain(vmi, indexedDomainInterfaces(currentDomain)) {
// 2. 调用配置器生成域接口定义
if err := vim.configurator.SetupPodNetworkPhase2(updatedDomain, []v1.Network{network}); err != nil {
return err
}
// 3. 从dummy域中取出刚生成的接口定义
relevantIface := lookupDomainInterfaceByName(updatedDomain.Spec.Devices.Interfaces, network.Name)
// 4. 序列化为XML
ifaceXML, err := xml.Marshal(relevantIface)
// 5. 通过libvirt热插设备(LIVE|CONFIG双标记)
if err := vim.dom.AttachDeviceFlags(strings.ToLower(string(ifaceXML)), affectDeviceLiveAndConfigLibvirtFlags); err != nil {
return err
}
}
return nil
}
热插判定条件 (networksToHotplugWhoseInterfacesAreNotInTheDomain):
- VMI接口状态有
InfoSourceMultusStatus标记(multus已分配网络) - 域XML中不存在该接口的alias
- VMI规格中接口State不是Absent
- 接口不是SR-IOV(SR-IOV走hostdev路径)
hotUnplugVirtioInterface --- 热拔网卡
go
func (vim *virtIOInterfaceManager) hotUnplugVirtioInterface(vmi *v1.VirtualMachineInstance, currentDomain *api.Domain) error {
// 找出需要热拔的接口:
// - VMI规格中标记State=Absent
// - 域中确实存在该接口
// - 目标设备名匹配hash命名(确认是同一个tap设备)
ifacesToHotUnplug := interfacesToHotUnplug(
vmi.Spec.Domain.Devices.Interfaces,
vmi.Spec.Networks,
currentDomain.Spec.Devices.Interfaces,
)
for _, domainIface := range ifacesToHotUnplug {
ifaceXML, _ := xml.Marshal(domainIface)
// DetachDeviceFlags 双标记
vim.dom.DetachDeviceFlags(strings.ToLower(string(ifaceXML)), affectDeviceLiveAndConfigLibvirtFlags)
}
return nil
}
hasDeviceWithHashedTapName:通过对比目标设备名和根据命名方案生成的tap名,确保拔出的是正确的设备。
updateDomainLinkState --- 链路状态更新
go
func (vim *virtIOInterfaceManager) updateDomainLinkState(currentDomain, desiredDomain *api.Domain) error {
currentDomainIfacesByAlias := indexedDomainInterfaces(currentDomain)
for _, desiredIface := range desiredDomain.Spec.Devices.Interfaces {
curIface, ok := currentDomainIfacesByAlias[desiredIface.Alias.GetName()]
if !ok { continue } // 当前域没有该接口,跳过
// 对比链路状态是否一致
if !isLinkStateEqual(curIface, desiredIface) {
curIface.LinkState = desiredIface.LinkState
// 通过UpdateDeviceFlags更新链路状态
vim.updateIfaceInDomain(&curIface)
}
}
return nil
}
WithNetworkIfacesResources --- 接口占位符机制
go
func WithNetworkIfacesResources(vmi *v1.VirtualMachineInstance, domainSpec *api.DomainSpec, count int,
f func(v *v1.VirtualMachineInstance, s *api.DomainSpec) (cli.VirDomain, error),
) (retDomainClient cli.VirDomain, err error) {
if count > 0 {
// 1. 添加count个占位接口到域定义
domainSpecWithIfacesResource := AppendPlaceholderInterfacesToTheDomain(vmi, domainSpec, count)
// 2. 用扩展的域定义创建libvirt域(触发libvirt分配PCI控制器等依赖资源)
dom, domErr := f(vmi, domainSpecWithIfacesResource)
// 3. 读取libvirt实际生成的域定义(包含PCI控制器等)
domainSpecWithoutIfacePlaceholders, _ := util.GetDomainSpecWithFlags(dom, libvirt.DOMAIN_XML_INACTIVE)
// 4. 用原始接口列表替换(去掉占位接口,但保留PCI控制器等依赖设备)
domainSpecWithoutIfacePlaceholders.Devices.Interfaces = domainSpec.Devices.Interfaces
domainSpecWithoutIfacePlaceholders.Devices.DeepCopyInto(&domainSpec.Devices)
}
return f(vmi, domainSpec)
}
设计意图:热插网卡需要PCI控制器等资源,但这些资源在域创建时就需要预留。占位接口确保域创建时libvirt会自动添加所需的PCI控制器,之后移除占位接口但保留控制器。
热插占位符机制流程图
Libvirt WithNetworkIfacesResources Caller Libvirt WithNetworkIfacesResources Caller #mermaid-svg-EyXYk7J3GRuviuFe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EyXYk7J3GRuviuFe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EyXYk7J3GRuviuFe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EyXYk7J3GRuviuFe .error-icon{fill:#552222;}#mermaid-svg-EyXYk7J3GRuviuFe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EyXYk7J3GRuviuFe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EyXYk7J3GRuviuFe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EyXYk7J3GRuviuFe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EyXYk7J3GRuviuFe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EyXYk7J3GRuviuFe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EyXYk7J3GRuviuFe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EyXYk7J3GRuviuFe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EyXYk7J3GRuviuFe .marker.cross{stroke:#333333;}#mermaid-svg-EyXYk7J3GRuviuFe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EyXYk7J3GRuviuFe p{margin:0;}#mermaid-svg-EyXYk7J3GRuviuFe .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EyXYk7J3GRuviuFe text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-EyXYk7J3GRuviuFe .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-EyXYk7J3GRuviuFe .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-EyXYk7J3GRuviuFe .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-EyXYk7J3GRuviuFe .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-EyXYk7J3GRuviuFe #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-EyXYk7J3GRuviuFe .sequenceNumber{fill:white;}#mermaid-svg-EyXYk7J3GRuviuFe #sequencenumber{fill:#333;}#mermaid-svg-EyXYk7J3GRuviuFe #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-EyXYk7J3GRuviuFe .messageText{fill:#333;stroke:none;}#mermaid-svg-EyXYk7J3GRuviuFe .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EyXYk7J3GRuviuFe .labelText,#mermaid-svg-EyXYk7J3GRuviuFe .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-EyXYk7J3GRuviuFe .loopText,#mermaid-svg-EyXYk7J3GRuviuFe .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-EyXYk7J3GRuviuFe .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-EyXYk7J3GRuviuFe .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-EyXYk7J3GRuviuFe .noteText,#mermaid-svg-EyXYk7J3GRuviuFe .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-EyXYk7J3GRuviuFe .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EyXYk7J3GRuviuFe .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EyXYk7J3GRuviuFe .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EyXYk7J3GRuviuFe .actorPopupMenu{position:absolute;}#mermaid-svg-EyXYk7J3GRuviuFe .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-EyXYk7J3GRuviuFe .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EyXYk7J3GRuviuFe .actor-man circle,#mermaid-svg-EyXYk7J3GRuviuFe line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-EyXYk7J3GRuviuFe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} libvirt自动分配 PCI控制器等资源 count=3 (预留3个热插口) AppendPlaceholderInterfaces (3个ethernet占位接口) 创建域(含占位接口) 域对象 GetDomainSpec(INACTIVE) 完整域定义(含PCI控制器) 替换Interfaces为原始列表 保留Devices中的PCI控制器 用修正后的域定义创建 最终域(无占位接口但有PCI控制器)
二、存储相关模块
2.1 converter/storage/virtiofs.go --- virtiofs配置
模块定位
业务职责 :将 VMI 规格中的 filesystem 设备(virtiofs类型)转换为 libvirt 域 XML 的 <filesystem> 元素。virtiofs 是高性能的共享文件系统方案,利用 virtio 协议在宿主机和虚拟机间共享目录。
逐行解析
go
type VirtiofsConfigurator struct{}
func (f VirtiofsConfigurator) Configure(vmi *v1.VirtualMachineInstance, domain *api.Domain) error {
for _, fs := range vmi.Spec.Domain.Devices.Filesystems {
if fs.Virtiofs == nil {
continue // 跳过非virtiofs文件系统
}
domain.Spec.Devices.Filesystems = append(domain.Spec.Devices.Filesystems,
api.FilesystemDevice{
Type: "mount", // mount类型(非block)
AccessMode: "passthrough", // 透传模式(独占访问)
Driver: &api.FilesystemDriver{
Type: "virtiofs", // 驱动类型
Queue: "1024", // 请求队列深度
},
Source: &api.FilesystemSource{
Socket: virtiofs.VirtioFSSocketPath(fs.Name), // Unix socket路径
// 路径格式: /var/run/kubevirt/virtiofs-containers/{name}.sock
},
Target: &api.FilesystemTarget{
Dir: fs.Name, // 虚拟机内挂载标签
},
},
)
}
return nil
}
virtiofs 工作原理:
- 宿主机侧运行 virtiofsd 守护进程,监听 Unix socket
- libvirt 域 XML 中
<filesystem>元素的 source socket 指向该 socket - 虚拟机内通过
mount -t virtiofs {name} /mnt/xxx挂载 - Queue=1024 设置了 virtqueue 的深度,影响并发IO性能
virtiofs配置流程图
#mermaid-svg-sMPPmk2RE3d9hpoW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sMPPmk2RE3d9hpoW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sMPPmk2RE3d9hpoW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sMPPmk2RE3d9hpoW .error-icon{fill:#552222;}#mermaid-svg-sMPPmk2RE3d9hpoW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sMPPmk2RE3d9hpoW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sMPPmk2RE3d9hpoW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sMPPmk2RE3d9hpoW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sMPPmk2RE3d9hpoW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sMPPmk2RE3d9hpoW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sMPPmk2RE3d9hpoW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sMPPmk2RE3d9hpoW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sMPPmk2RE3d9hpoW .marker.cross{stroke:#333333;}#mermaid-svg-sMPPmk2RE3d9hpoW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sMPPmk2RE3d9hpoW p{margin:0;}#mermaid-svg-sMPPmk2RE3d9hpoW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sMPPmk2RE3d9hpoW .cluster-label text{fill:#333;}#mermaid-svg-sMPPmk2RE3d9hpoW .cluster-label span{color:#333;}#mermaid-svg-sMPPmk2RE3d9hpoW .cluster-label span p{background-color:transparent;}#mermaid-svg-sMPPmk2RE3d9hpoW .label text,#mermaid-svg-sMPPmk2RE3d9hpoW span{fill:#333;color:#333;}#mermaid-svg-sMPPmk2RE3d9hpoW .node rect,#mermaid-svg-sMPPmk2RE3d9hpoW .node circle,#mermaid-svg-sMPPmk2RE3d9hpoW .node ellipse,#mermaid-svg-sMPPmk2RE3d9hpoW .node polygon,#mermaid-svg-sMPPmk2RE3d9hpoW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sMPPmk2RE3d9hpoW .rough-node .label text,#mermaid-svg-sMPPmk2RE3d9hpoW .node .label text,#mermaid-svg-sMPPmk2RE3d9hpoW .image-shape .label,#mermaid-svg-sMPPmk2RE3d9hpoW .icon-shape .label{text-anchor:middle;}#mermaid-svg-sMPPmk2RE3d9hpoW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sMPPmk2RE3d9hpoW .rough-node .label,#mermaid-svg-sMPPmk2RE3d9hpoW .node .label,#mermaid-svg-sMPPmk2RE3d9hpoW .image-shape .label,#mermaid-svg-sMPPmk2RE3d9hpoW .icon-shape .label{text-align:center;}#mermaid-svg-sMPPmk2RE3d9hpoW .node.clickable{cursor:pointer;}#mermaid-svg-sMPPmk2RE3d9hpoW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sMPPmk2RE3d9hpoW .arrowheadPath{fill:#333333;}#mermaid-svg-sMPPmk2RE3d9hpoW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sMPPmk2RE3d9hpoW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sMPPmk2RE3d9hpoW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sMPPmk2RE3d9hpoW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sMPPmk2RE3d9hpoW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sMPPmk2RE3d9hpoW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sMPPmk2RE3d9hpoW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sMPPmk2RE3d9hpoW .cluster text{fill:#333;}#mermaid-svg-sMPPmk2RE3d9hpoW .cluster span{color:#333;}#mermaid-svg-sMPPmk2RE3d9hpoW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sMPPmk2RE3d9hpoW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sMPPmk2RE3d9hpoW rect.text{fill:none;stroke-width:0;}#mermaid-svg-sMPPmk2RE3d9hpoW .icon-shape,#mermaid-svg-sMPPmk2RE3d9hpoW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sMPPmk2RE3d9hpoW .icon-shape p,#mermaid-svg-sMPPmk2RE3d9hpoW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sMPPmk2RE3d9hpoW .icon-shape .label rect,#mermaid-svg-sMPPmk2RE3d9hpoW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sMPPmk2RE3d9hpoW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sMPPmk2RE3d9hpoW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sMPPmk2RE3d9hpoW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
VMI Spec.Filesystems
fs.Virtiofs != nil?
跳过
构造 FilesystemDevice
Type=mount
AccessMode=passthrough
Driver=virtiofs
Queue=1024
Source.Socket=....sock
Target.Dir=name
追加到 domain.Devices.Filesystems
2.2 disksource/disk_source.go --- 磁盘源解析
模块定位
业务职责:解析 libvirt 域磁盘定义的源信息,区分直接块设备/文件、以及带overlay的DataStore模式,为备份和CBT提供磁盘后端路径判断。
逐行解析
go
type ResolvedDiskSource struct {
sourcePath string // 源路径(qcow2 overlay文件或块设备路径)
backendPath string // 后端路径(底层镜像路径)
backendIsBlock bool // 后端是否为块设备
hasOverlay bool // 是否有overlay(CBT模式)
}
func Resolve(d api.Disk) ResolvedDiskSource {
rds := ResolvedDiskSource{}
// 1. 块设备源(PVC raw block)
if d.Source.Dev != "" {
rds.sourcePath = d.Source.Dev
rds.backendPath = d.Source.Dev
rds.backendIsBlock = true
}
// 2. 文件源(qcow2/raw文件)
if d.Source.File != "" {
rds.sourcePath = d.Source.File
rds.backendPath = d.Source.File
rds.backendIsBlock = false
}
// 3. DataStore overlay(CBT模式:source=overlay, backend=底层镜像)
if d.Source.DataStore != nil && d.Source.DataStore.Source != nil {
rds.hasOverlay = true
if d.Source.DataStore.Source.Dev != "" {
rds.backendPath = d.Source.DataStore.Source.Dev
rds.backendIsBlock = true
}
if d.Source.DataStore.Source.File != "" {
rds.backendPath = d.Source.DataStore.Source.File
rds.backendIsBlock = false
}
}
return rds
}
// 判断是否为热插磁盘(路径以 HotplugDiskDir 开头)
func (rds ResolvedDiskSource) IsHotplugDisk() bool {
return strings.HasPrefix(rds.backendPath, v1.HotplugDiskDir)
}
// 判断是否为热插或空磁盘
func (rds ResolvedDiskSource) IsHotplugOrEmpty() bool {
return rds.IsHotplugDisk() || rds.backendPath == ""
}
CBT overlay 结构:启用 CBT 后,磁盘结构为:
sourcePath→ qcow2 overlay文件(记录变更数据)backendPath→ 底层PVC镜像(通过DataStore引用)- overlay的backing_file指向backend
磁盘源类型结构图
#mermaid-svg-ZN7xyKU7SWlMqBbo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZN7xyKU7SWlMqBbo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZN7xyKU7SWlMqBbo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZN7xyKU7SWlMqBbo .error-icon{fill:#552222;}#mermaid-svg-ZN7xyKU7SWlMqBbo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZN7xyKU7SWlMqBbo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZN7xyKU7SWlMqBbo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZN7xyKU7SWlMqBbo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZN7xyKU7SWlMqBbo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZN7xyKU7SWlMqBbo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZN7xyKU7SWlMqBbo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZN7xyKU7SWlMqBbo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZN7xyKU7SWlMqBbo .marker.cross{stroke:#333333;}#mermaid-svg-ZN7xyKU7SWlMqBbo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZN7xyKU7SWlMqBbo p{margin:0;}#mermaid-svg-ZN7xyKU7SWlMqBbo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZN7xyKU7SWlMqBbo .cluster-label text{fill:#333;}#mermaid-svg-ZN7xyKU7SWlMqBbo .cluster-label span{color:#333;}#mermaid-svg-ZN7xyKU7SWlMqBbo .cluster-label span p{background-color:transparent;}#mermaid-svg-ZN7xyKU7SWlMqBbo .label text,#mermaid-svg-ZN7xyKU7SWlMqBbo span{fill:#333;color:#333;}#mermaid-svg-ZN7xyKU7SWlMqBbo .node rect,#mermaid-svg-ZN7xyKU7SWlMqBbo .node circle,#mermaid-svg-ZN7xyKU7SWlMqBbo .node ellipse,#mermaid-svg-ZN7xyKU7SWlMqBbo .node polygon,#mermaid-svg-ZN7xyKU7SWlMqBbo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZN7xyKU7SWlMqBbo .rough-node .label text,#mermaid-svg-ZN7xyKU7SWlMqBbo .node .label text,#mermaid-svg-ZN7xyKU7SWlMqBbo .image-shape .label,#mermaid-svg-ZN7xyKU7SWlMqBbo .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZN7xyKU7SWlMqBbo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZN7xyKU7SWlMqBbo .rough-node .label,#mermaid-svg-ZN7xyKU7SWlMqBbo .node .label,#mermaid-svg-ZN7xyKU7SWlMqBbo .image-shape .label,#mermaid-svg-ZN7xyKU7SWlMqBbo .icon-shape .label{text-align:center;}#mermaid-svg-ZN7xyKU7SWlMqBbo .node.clickable{cursor:pointer;}#mermaid-svg-ZN7xyKU7SWlMqBbo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZN7xyKU7SWlMqBbo .arrowheadPath{fill:#333333;}#mermaid-svg-ZN7xyKU7SWlMqBbo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZN7xyKU7SWlMqBbo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZN7xyKU7SWlMqBbo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZN7xyKU7SWlMqBbo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZN7xyKU7SWlMqBbo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZN7xyKU7SWlMqBbo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZN7xyKU7SWlMqBbo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZN7xyKU7SWlMqBbo .cluster text{fill:#333;}#mermaid-svg-ZN7xyKU7SWlMqBbo .cluster span{color:#333;}#mermaid-svg-ZN7xyKU7SWlMqBbo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZN7xyKU7SWlMqBbo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZN7xyKU7SWlMqBbo rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZN7xyKU7SWlMqBbo .icon-shape,#mermaid-svg-ZN7xyKU7SWlMqBbo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZN7xyKU7SWlMqBbo .icon-shape p,#mermaid-svg-ZN7xyKU7SWlMqBbo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZN7xyKU7SWlMqBbo .icon-shape .label rect,#mermaid-svg-ZN7xyKU7SWlMqBbo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZN7xyKU7SWlMqBbo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZN7xyKU7SWlMqBbo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZN7xyKU7SWlMqBbo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CBT Overlay模式
Source.File=overlay.qcow2
sourcePath=overlay
DataStore.Source.File=pvc.img
backendPath=pvc.img
hasOverlay=true
普通模式
Source.Dev=/dev/sdb
backendPath=源路径
block=true
Source.File=/img.qcow2
backendPath=源路径
block=false
2.3 storage/manager.go --- 存储管理器
模块定位
业务职责:virt-launcher 存储操作的核心管理器,协调备份、CBT overlay、内存转储、文件系统冻结等存储相关功能。
核心结构
go
type StorageManager struct {
virConn cli.Connection // libvirt连接
metadataCache *metadata.Cache // 元数据缓存
memoryDumpInProgress chan struct{} // 内存转储并发控制(容量1)
cancelSafetyUnfreezeChan chan struct{} // 取消安全解冻的信号通道
registerNBD RegisterNBDFunc // NBD服务注册函数(注入避免libnbd编译依赖)
activeBackupTunnel *backupTunnelManager // 活跃的备份隧道
backupTunnelMu sync.Mutex // 备份隧道互斥锁
}
func NewStorageManager(connection cli.Connection, metadataCache *metadata.Cache, registerNBD RegisterNBDFunc) *StorageManager {
return &StorageManager{
virConn: connection,
metadataCache: metadataCache,
memoryDumpInProgress: make(chan struct{}, MaxConcurrentMemoryDumps), // 容量1=同时只允许1个
cancelSafetyUnfreezeChan: make(chan struct{}),
registerNBD: registerNBD,
}
}
func (m *StorageManager) MigrationInProgress() bool {
migrationMetadata, exists := m.metadataCache.Migration.Load()
return exists && migrationMetadata.StartTimestamp != nil && migrationMetadata.EndTimestamp == nil
// 有开始时间但没有结束时间 = 迁移进行中
}
2.4 storage/backup.go --- 虚拟机备份
模块定位
业务职责:实现虚拟机完整备份和增量备份(基于libvirt checkpoint/bitmap机制),支持 Push 模式(写入本地文件)和 Pull 模式(通过NBD隧道导出)。
核心流程深度解析
BackupVirtualMachine --- 备份主入口
go
func (m *StorageManager) BackupVirtualMachine(vmi *v1.VirtualMachineInstance, backupOptions *backupv1.BackupOptions) error {
// 1. 迁移中不允许备份
if m.MigrationInProgress() {
return fmt.Errorf("failed to do backup, VMI is currently during migration")
}
// 2. 初始化备份元数据(幂等性检查)
inProgress, err := m.initializeBackupMetadata(backupOptions)
if inProgress { return nil } // 已在执行,直接返回
if err != nil { return err }
// 3. 执行备份
err = m.backup(vmi, backupOptions)
if err != nil {
m.metadataCache.Backup.Store(api.BackupMetadata{}) // 失败时清空元数据
return err
}
return nil
}
initializeBackupMetadata --- 备份元数据初始化
go
func (m *StorageManager) initializeBackupMetadata(backupOptions *backupv1.BackupOptions) (bool, error) {
backupMetadata, exists := m.metadataCache.Backup.Load()
if exists && backupMetadata.Name != "" {
sameBackup := backupMetadata.Name == backupOptions.BackupName &&
backupMetadata.StartTimestamp.Equal(backupOptions.BackupStartTime)
if sameBackup {
if backupMetadata.EndTimestamp == nil {
return true, nil // 同一备份进行中 → 幂等返回
} else {
return false, fmt.Errorf("backup already executed") // 已完成 → 拒绝重复
}
} else {
if backupMetadata.EndTimestamp == nil {
return false, fmt.Errorf("another backup in progress") // 其他备份进行中
}
// 旧备份已完成 → 允许新备份覆盖
}
}
// 新备份 → 存储初始元数据
m.metadataCache.Backup.Store(api.BackupMetadata{
Name: backupOptions.BackupName,
StartTimestamp: backupOptions.BackupStartTime,
SkipQuiesce: backupOptions.SkipQuiesce,
Mode: string(backupOptions.Mode),
})
return false, nil
}
backup --- 核心备份执行
go
func (m *StorageManager) backup(vmi *v1.VirtualMachineInstance, backupOptions *backupv1.BackupOptions) (failed error) {
// 1. 获取libvirt域对象
domName := api.VMINamespaceKeyFunc(vmi)
dom, err := m.virConn.LookupDomainByName(domName)
defer dom.Free()
// 2. 解析当前域的所有磁盘
domainDisks, err := util.GetAllDomainDisks(dom)
// 3. Push模式需要准备目标路径
var backupPath string
if backupOptions.TargetPath != nil {
backupPath = getBackupPath(backupOptions, vmi.Name)
kutil.MkdirAllWithNosec(backupPath)
defer func(path string) {
if failed != nil { os.RemoveAll(path) } // 失败时清理
}(backupPath)
}
// 4. 生成备份和checkpoint XML
domainBackup, domainCheckpoint, backupVolumesInfo := generateDomainBackup(domainDisks, backupOptions, backupPath)
backupXML, _ := xml.Marshal(domainBackup)
checkpointXML, _ := xml.Marshal(domainCheckpoint)
// 5. 存储checkpoint名和卷信息到元数据
m.metadataCache.Backup.WithSafeBlock(func(backupMetadata *api.BackupMetadata, _ bool) {
backupMetadata.CheckpointName = domainCheckpoint.Name
backupMetadata.Volumes = string(volumesJSON)
})
// 6. 文件系统冻结(quiesce)
frozenFS := false
if !backupOptions.SkipQuiesce {
if err := dom.FSFreeze(nil, 0); err != nil {
// 冻结失败不终止备份,但记录消息
backupMetadata.BackupMsg = fmt.Sprintf(freezeFailedMsg, err)
} else {
frozenFS = true
}
}
defer func() {
if frozenFS { dom.FSThaw(nil, 0) } // 解冻
}()
// 7. 调用libvirt BackupBegin API
return dom.BackupBegin(strings.ToLower(string(backupXML)), strings.ToLower(string(checkpointXML)), 0)
}
generateDomainBackup --- 生成备份/检查点XML
go
func generateDomainBackup(disks []api.Disk, backupOptions *backupv1.BackupOptions, backupPath string) (*api.DomainBackup, *api.DomainCheckpoint, []backupv1.BackupVolumeInfo) {
domainBackup := &api.DomainBackup{Mode: string(backupOptions.Mode)}
// 增量备份:设置Incremental指向上一检查点
if isIncrementalBackup(backupOptions) {
domainBackup.Incremental = backupOptions.Incremental
}
// Pull模式:设置NBD server
if backupOptions.Mode == backupv1.PullMode {
domainBackup.Server = &api.DomainBackupServer{
Transport: api.BackupUnixTransport,
Socket: "/var/run/kubevirt/sockets/backup-nbd-sock",
}
}
// 检查点名格式:{backupName}-{timestamp}
checkpointName := fmt.Sprintf("%s-%s", backupOptions.BackupName, backupTimeFormatted(backupOptions.BackupStartTime))
// 遍历域磁盘,设置每块盘的备份策略
for _, disk := range disks {
if disk.Target.Device == "" { continue }
if DiskHasDataStore(&disk) {
// CBT磁盘 → 备份=yes, checkpoint=bitmap
backupDisk.Backup = "yes"
backupDisk.Type = "file"
if backupOptions.Mode == backupv1.PullMode {
backupDisk.ExportName = volumeName // NBD导出名
backupDisk.ExportBitmap = checkpointName // 导出的bitmap名
}
if backupOptions.TargetPath != nil {
setBackupDiskTargetPath(&backupDisk, backupOptions, volumeName, backupPath)
// Push: backupDisk.Target.File = {path}/{backup}-{volume}.qcow2
// Pull: backupDisk.Scratch.File = {path}/{backup}-{volume}.qcow2 (临时scratch文件)
}
checkpointDisk.Checkpoint = "bitmap" // 创建bitmap用于增量追踪
} else {
// 非CBT磁盘 → backup=no, 不追踪
backupDisk.Backup = "no"
checkpointDisk.Checkpoint = "no"
}
}
return domainBackup, domainCheckpoint, backupVolumesInfo
}
HandleBackupJobCompletedEvent --- 备份完成事件处理
go
func HandleBackupJobCompletedEvent(domain cli.VirDomain, event *libvirt.DomainEventJobCompleted, metadataCache *metadata.Cache) {
backupMetadata, exists := metadataCache.Backup.Load()
if !exists { return } // 无活跃备份元数据
// 获取最终作业统计
finalStats, _ := domain.GetJobStats(libvirt.DOMAIN_JOB_STATS_COMPLETED)
var failed bool
var message string
switch event.Info.Type {
case libvirt.DOMAIN_JOB_COMPLETED:
// 成功完成
case libvirt.DOMAIN_JOB_CANCELLED:
// 取消:Push模式视为失败,Pull模式视为正常(客户端主动断开)
failed = backupMetadata.Mode == string(backupv1.PushMode)
case libvirt.DOMAIN_JOB_FAILED:
failed = true
}
// 更新元数据:结束时间、失败状态、消息
metadataCache.Backup.WithSafeBlock(func(backupMetadata *api.BackupMetadata, exists bool) {
backupMetadata.Failed = failed
backupMetadata.Completed = true
backupMetadata.BackupMsg = message
backupMetadata.EndTimestamp = &now
})
}
RedefineCheckpoint --- 重新定义检查点
go
func (m *StorageManager) RedefineCheckpoint(vmi *v1.VirtualMachineInstance, checkpoint *backupv1.BackupCheckpoint) (checkpointInvalid bool, err error) {
// VM重启后libvirt丢失checkpoint元数据,需要重新定义
// 1. 查找具有指定bitmap的磁盘
checkpointDisks, disksWithoutBitmap, err := findDisksWithCheckpointBitmap(dom, checkpoint.Name)
if len(checkpointDisks.Disks) == 0 {
return true, fmt.Errorf("no disks found with checkpoint bitmap") // bitmap无效
}
// 2. 构造checkpoint XML
domainCheckpoint := &api.DomainCheckpoint{
Name: checkpoint.Name,
CheckpointDisks: checkpointDisks,
CreationTime: &ct, // 保留原始创建时间
}
// 3. 使用REDEFINE|REDEFINE_VALIDATE标记重新定义
redefineFlags := libvirt.DOMAIN_CHECKPOINT_CREATE_REDEFINE | libvirt.DOMAIN_CHECKPOINT_CREATE_REDEFINE_VALIDATE
dom.CreateCheckpointXML(string(checkpointXML), redefineFlags)
// 4. 如果bitmap损坏,返回checkpointInvalid=true
return isLibvirtCheckpointInvalidError(err), err
}
queryBitmaps :通过QMP(QEMU Monitor Protocol)查询bitmap信息,避免使用qemu-img info(后者在VM运行时看不到最新bitmap状态)。
备份完整流程图
#mermaid-svg-FHHWLCIr7DxYkbLv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FHHWLCIr7DxYkbLv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FHHWLCIr7DxYkbLv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FHHWLCIr7DxYkbLv .error-icon{fill:#552222;}#mermaid-svg-FHHWLCIr7DxYkbLv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FHHWLCIr7DxYkbLv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FHHWLCIr7DxYkbLv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FHHWLCIr7DxYkbLv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FHHWLCIr7DxYkbLv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FHHWLCIr7DxYkbLv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FHHWLCIr7DxYkbLv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FHHWLCIr7DxYkbLv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FHHWLCIr7DxYkbLv .marker.cross{stroke:#333333;}#mermaid-svg-FHHWLCIr7DxYkbLv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FHHWLCIr7DxYkbLv p{margin:0;}#mermaid-svg-FHHWLCIr7DxYkbLv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FHHWLCIr7DxYkbLv .cluster-label text{fill:#333;}#mermaid-svg-FHHWLCIr7DxYkbLv .cluster-label span{color:#333;}#mermaid-svg-FHHWLCIr7DxYkbLv .cluster-label span p{background-color:transparent;}#mermaid-svg-FHHWLCIr7DxYkbLv .label text,#mermaid-svg-FHHWLCIr7DxYkbLv span{fill:#333;color:#333;}#mermaid-svg-FHHWLCIr7DxYkbLv .node rect,#mermaid-svg-FHHWLCIr7DxYkbLv .node circle,#mermaid-svg-FHHWLCIr7DxYkbLv .node ellipse,#mermaid-svg-FHHWLCIr7DxYkbLv .node polygon,#mermaid-svg-FHHWLCIr7DxYkbLv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FHHWLCIr7DxYkbLv .rough-node .label text,#mermaid-svg-FHHWLCIr7DxYkbLv .node .label text,#mermaid-svg-FHHWLCIr7DxYkbLv .image-shape .label,#mermaid-svg-FHHWLCIr7DxYkbLv .icon-shape .label{text-anchor:middle;}#mermaid-svg-FHHWLCIr7DxYkbLv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FHHWLCIr7DxYkbLv .rough-node .label,#mermaid-svg-FHHWLCIr7DxYkbLv .node .label,#mermaid-svg-FHHWLCIr7DxYkbLv .image-shape .label,#mermaid-svg-FHHWLCIr7DxYkbLv .icon-shape .label{text-align:center;}#mermaid-svg-FHHWLCIr7DxYkbLv .node.clickable{cursor:pointer;}#mermaid-svg-FHHWLCIr7DxYkbLv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FHHWLCIr7DxYkbLv .arrowheadPath{fill:#333333;}#mermaid-svg-FHHWLCIr7DxYkbLv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FHHWLCIr7DxYkbLv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FHHWLCIr7DxYkbLv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FHHWLCIr7DxYkbLv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FHHWLCIr7DxYkbLv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FHHWLCIr7DxYkbLv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FHHWLCIr7DxYkbLv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FHHWLCIr7DxYkbLv .cluster text{fill:#333;}#mermaid-svg-FHHWLCIr7DxYkbLv .cluster span{color:#333;}#mermaid-svg-FHHWLCIr7DxYkbLv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FHHWLCIr7DxYkbLv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FHHWLCIr7DxYkbLv rect.text{fill:none;stroke-width:0;}#mermaid-svg-FHHWLCIr7DxYkbLv .icon-shape,#mermaid-svg-FHHWLCIr7DxYkbLv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FHHWLCIr7DxYkbLv .icon-shape p,#mermaid-svg-FHHWLCIr7DxYkbLv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FHHWLCIr7DxYkbLv .icon-shape .label rect,#mermaid-svg-FHHWLCIr7DxYkbLv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FHHWLCIr7DxYkbLv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FHHWLCIr7DxYkbLv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FHHWLCIr7DxYkbLv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
是
否
Push
Pull
BackupVirtualMachine
迁移中?
拒绝备份
initializeBackupMetadata
同一备份进行中?
幂等返回
其他备份进行中?
拒绝
存储元数据
backup执行
获取域磁盘列表
generateDomainBackup
增量备份?
设置Incremental=上一检查点
全量备份
Push还是Pull?
Target.File=目标路径
Server.Socket=NBD socket
ExportName/Bitmap
FSFreeze quiesce
dom.BackupBegin
事件: JOB_COMPLETED
更新元数据
2.5 storage/backup_tunnel.go --- 备份隧道
模块定位
业务职责:实现 Pull 模式备份的数据传输隧道。通过 HTTP/2 CONNECT 方法建立从 virt-launcher 到备份服务端的隧道,在隧道上承载 NBD(Network Block Device)协议,使远端备份服务可以直接拉取增量数据。
核心结构
go
type backupTunnelManager struct {
targetAddr string // 备份服务端地址
serverName string // TLS SNI服务器名
nbdSocket string // 本地NBD socket路径
caCert string // CA证书
backupCert string // 客户端证书
backupKey string // 客户端私钥
backupName string // 备份名
backupStartTime *metav1.Time // 备份开始时间
registerNBD RegisterNBDFunc // NBD gRPC服务注册函数
mu sync.Mutex
server *grpc.Server // gRPC服务器(承载NBD协议)
cancel context.CancelFunc
}
Start --- 启动隧道
go
func (m *backupTunnelManager) Start() error {
ctx, cancel := context.WithCancel(context.Background())
m.cancel = cancel
// 监控NBD socket文件(当libvirt BackupBegin创建了socket才启动)
nbdSocketCh, err := m.watchSocket(ctx)
go func() {
defer cancel()
m.run(ctx, nbdSocketCh) // 主运行循环
}()
return nil
}
run --- 主运行循环(带重连)
go
func (m *backupTunnelManager) run(ctx context.Context, nbdSocketCh <-chan struct{}) error {
// 指数退避重连:初始1s,最大5min,重置30s
delayFn := wait.Backoff{
Duration: 1 * time.Second,
Cap: 5 * time.Minute,
Factor: 2.0,
Jitter: 0.1,
}.DelayWithReset(&clock.RealClock{}, 30*time.Second)
// 循环尝试连接,断开后自动重连
delayFn.Until(ctx, true, true, func(ctx context.Context) (bool, error) {
select {
case <-nbdSocketCh:
return false, fmt.Errorf("NBD socket removed") // socket被删除 → 终止
default:
if _, err := os.Stat(m.nbdSocket); os.IsNotExist(err) {
return false, fmt.Errorf("NBD socket not found") // socket不存在 → 终止
}
m.establishAndServe(ctx, nbdSocketCh) // 建立隧道并服务
return false, nil // 连接断开 → 重试
}
})
}
establishAndServe --- 建立隧道并服务
go
func (m *backupTunnelManager) establishAndServe(ctx context.Context, nbdSocketCh <-chan struct{}) error {
// 1. 准备TLS配置(mTLS双向认证)
tlsConfig, err := m.prepareTLSConfig()
// 2. 通过HTTP/2 CONNECT建立隧道
conn, err := m.openConnectTunnel(ctx, url, tlsConfig)
defer conn.Close()
// 3. 创建gRPC服务器,注册NBD服务
srv := grpc.NewServer(...)
m.registerNBD(srv, m.nbdSocket) // NBD服务从本地socket转发数据
// 4. gRPC服务器在HTTP/2隧道上服务
srv.Serve(&oneConnListener{conn: wrapped, ...})
// oneConnListener: 只Accept一次(单连接模型)
}
openConnectTunnel --- HTTP/2 CONNECT隧道
go
func (m *backupTunnelManager) openConnectTunnel(ctx context.Context, targetURL string, tlsConfig *tls.Config) (net.Conn, error) {
transport := &http2.Transport{TLSClientConfig: tlsConfig}
// 创建pipe:gRPC写入→pw→pr→HTTP/2 request body
pr, pw := io.Pipe()
// 发送CONNECT请求
req, _ := http.NewRequestWithContext(ctx, http.MethodConnect, targetURL, pr)
resp, err := transport.RoundTrip(req)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("server rejected CONNECT")
}
// h2ClientConn: 读从resp.Body, 写通过pw
return &h2ClientConn{r: resp.Body, w: pw, t: transport}, nil
}
隧道数据流:
远端备份服务 ←→ HTTP/2 CONNECT ←→ h2ClientConn ←→ gRPC Server ←→ NBD Socket ←→ QEMU
watchSocket --- Socket文件监控
go
func (m *backupTunnelManager) watchSocket(ctx context.Context) (<-chan struct{}, error) {
watcher, _ := fsnotify.NewWatcher()
watcher.Add(filepath.Dir(m.nbdSocket)) // 监控socket所在目录
// 当socket被删除或重命名时通知
// 监控目录而非文件本身,因为文件可能在监控前就被创建/删除
}
备份隧道架构图
#mermaid-svg-R08HFZSMOyUkKyRX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-R08HFZSMOyUkKyRX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-R08HFZSMOyUkKyRX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-R08HFZSMOyUkKyRX .error-icon{fill:#552222;}#mermaid-svg-R08HFZSMOyUkKyRX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-R08HFZSMOyUkKyRX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-R08HFZSMOyUkKyRX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-R08HFZSMOyUkKyRX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-R08HFZSMOyUkKyRX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-R08HFZSMOyUkKyRX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-R08HFZSMOyUkKyRX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-R08HFZSMOyUkKyRX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-R08HFZSMOyUkKyRX .marker.cross{stroke:#333333;}#mermaid-svg-R08HFZSMOyUkKyRX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-R08HFZSMOyUkKyRX p{margin:0;}#mermaid-svg-R08HFZSMOyUkKyRX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-R08HFZSMOyUkKyRX .cluster-label text{fill:#333;}#mermaid-svg-R08HFZSMOyUkKyRX .cluster-label span{color:#333;}#mermaid-svg-R08HFZSMOyUkKyRX .cluster-label span p{background-color:transparent;}#mermaid-svg-R08HFZSMOyUkKyRX .label text,#mermaid-svg-R08HFZSMOyUkKyRX span{fill:#333;color:#333;}#mermaid-svg-R08HFZSMOyUkKyRX .node rect,#mermaid-svg-R08HFZSMOyUkKyRX .node circle,#mermaid-svg-R08HFZSMOyUkKyRX .node ellipse,#mermaid-svg-R08HFZSMOyUkKyRX .node polygon,#mermaid-svg-R08HFZSMOyUkKyRX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-R08HFZSMOyUkKyRX .rough-node .label text,#mermaid-svg-R08HFZSMOyUkKyRX .node .label text,#mermaid-svg-R08HFZSMOyUkKyRX .image-shape .label,#mermaid-svg-R08HFZSMOyUkKyRX .icon-shape .label{text-anchor:middle;}#mermaid-svg-R08HFZSMOyUkKyRX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-R08HFZSMOyUkKyRX .rough-node .label,#mermaid-svg-R08HFZSMOyUkKyRX .node .label,#mermaid-svg-R08HFZSMOyUkKyRX .image-shape .label,#mermaid-svg-R08HFZSMOyUkKyRX .icon-shape .label{text-align:center;}#mermaid-svg-R08HFZSMOyUkKyRX .node.clickable{cursor:pointer;}#mermaid-svg-R08HFZSMOyUkKyRX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-R08HFZSMOyUkKyRX .arrowheadPath{fill:#333333;}#mermaid-svg-R08HFZSMOyUkKyRX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-R08HFZSMOyUkKyRX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-R08HFZSMOyUkKyRX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R08HFZSMOyUkKyRX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-R08HFZSMOyUkKyRX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R08HFZSMOyUkKyRX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-R08HFZSMOyUkKyRX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-R08HFZSMOyUkKyRX .cluster text{fill:#333;}#mermaid-svg-R08HFZSMOyUkKyRX .cluster span{color:#333;}#mermaid-svg-R08HFZSMOyUkKyRX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-R08HFZSMOyUkKyRX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-R08HFZSMOyUkKyRX rect.text{fill:none;stroke-width:0;}#mermaid-svg-R08HFZSMOyUkKyRX .icon-shape,#mermaid-svg-R08HFZSMOyUkKyRX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R08HFZSMOyUkKyRX .icon-shape p,#mermaid-svg-R08HFZSMOyUkKyRX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-R08HFZSMOyUkKyRX .icon-shape .label rect,#mermaid-svg-R08HFZSMOyUkKyRX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R08HFZSMOyUkKyRX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-R08HFZSMOyUkKyRX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-R08HFZSMOyUkKyRX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 备份服务端 Pod
virt-launcher Pod
NBD socket
数据流
HTTP/2 CONNECT
HTTP/2
NBD数据
socket删除
QEMU/KVM
gRPC NBD Server
h2ClientConn
Read=resp.Body
Write=pipe.Writer
TLS连接
CONNECT代理
备份存储
fsnotify监控
停止隧道
2.6 storage/cbt.go --- 变更块追踪
模块定位
业务职责:实现 Changed Block Tracking(CBT),为增量备份提供底层支持。核心是创建 qcow2 overlay 文件覆盖在原始PVC镜像上,使所有写IO记录在overlay中,配合 libvirt checkpoint bitmap 实现增量追踪。
核心流程深度解析
ApplyChangedBlockTracking --- CBT应用主入口
go
func ApplyChangedBlockTracking(vmi *v1.VirtualMachineInstance, c *convertertypes.ConverterContext) error {
applyCBTMap := make(map[string]string) // volumeName → overlayPath
for _, volume := range vmi.Spec.Volumes {
volumeName := volume.Name
if !cbt.IsCBTEligibleVolume(&volume) {
continue // 非CBT候选卷(如cloud-init、containerDisk等)跳过
}
overlayPath := cbt.GetQCOW2OverlayPath(vmi, volumeName)
hotplugStatus, isHotplug := c.HotplugVolumes[volumeName]
// 判断是否需要创建overlay
if !shouldCreateQCOW2Overlay(vmi, isHotplug, hotplugStatus.Phase) {
// overlay已存在或不需要创建 → 直接记录路径
applyCBTMap[volumeName] = overlayPath
continue
}
// 需要创建overlay
isBlock := c.IsBlockPVC[volumeName] || c.IsBlockDV[volumeName]
imagePath := converter.GetVolumeImagePath(volumeName, isBlock, isHotplug)
err := CreateQCOW2Overlay(overlayPath, imagePath, isBlock)
applyCBTMap[volumeName] = overlayPath
}
// 将overlay映射写入ConverterContext,后续converter会修改域XML
c.ApplyCBT = applyCBTMap
return nil
}
shouldCreateQCOW2Overlay --- 判断是否需要创建overlay
go
func shouldCreateQCOW2Overlay(vmi *v1.VirtualMachineInstance, isHotplug bool, hotplugPhase v1.VolumePhase) bool {
// CBT初始化阶段 → 总是创建
if cbt.CBTStateInitializing(vmi.Status.ChangedBlockTracking) {
return true
}
// CBT未启用 → 不创建
if !IsChangedBlockTrackingEnabled(vmi) {
return false
}
// CBT已启用 + 热插卷 → 仅在"已挂载但未加入域"阶段创建
// VolumeReady时overlay已存在,不需要重复创建
return isHotplug && hotplugPhase == v1.HotplugVolumeMounted
}
createQCOW2OverlayFunc --- 创建QCOW2 overlay
go
func createQCOW2OverlayFunc(overlayPath, imagePath string, blockDev bool) error {
// 1. 检查overlay是否已存在
if _, err := os.Stat(overlayPath); err == nil {
return nil // 已存在,跳过
}
// 2. 创建空文件
os.Create(overlayPath)
defer func() {
if err != nil { os.Remove(overlayPath) } // 失败时清理
}(overlayPath)
// 3. 获取底层镜像的虚拟大小
info, err := osdisk.GetDiskInfo(imagePath)
overlaySize := info.VirtualSize
// 4. 使用 qemu-storage-daemon 创建 qcow2 overlay
// 命令行等价:
// qemu-storage-daemon \
// --chardev stdio,id=stdio --monitor stdio \
// --blockdev file,node-name=file,filename=overlay.qcow2 \
// --blockdev file,node-name=data-file,filename=backend.img # 或 host_device 用于块设备
args := append([]string{},
"--chardev", "stdio,id=stdio", "--monitor", "stdio",
"--blockdev", fmt.Sprintf("file,node-name=file,filename=%s", overlayPath),
)
if blockDev {
args = append(args, "--blockdev", fmt.Sprintf("host_device,node-name=data-file,filename=%s", imagePath))
} else {
args = append(args, "--blockdev", fmt.Sprintf("file,node-name=data-file,filename=%s", imagePath))
}
// 5. 启动qemu-storage-daemon,通过QMP命令创建overlay
cmd := exec.CommandContext(ctx, "qemu-storage-daemon", args...)
// 6. QMP会话:协商能力 → blockdev-create → 等待完成 → dismiss job → quit
// qmp_capabilities → blockdev-create(driver=qcow2, file=file, data-file=data-file, data-file-raw=true, size=N)
// 等待 JOB_STATUS_CHANGE(concluded) → query-jobs → job-dismiss → quit
runOverlayQMPSession(ctx, stdin, stdout, overlaySize, overlayPath)
}
overlay 关键属性:
data-file-raw=true:数据文件以原始格式包含在overlay中,overlay只负责bitmap追踪data-file:指向底层PVC镜像,写IO会直接写入data-filefile:overlay本身的存储位置
ApplyChangedBlockTrackingForMigration --- 迁移场景CBT
go
func ApplyChangedBlockTrackingForMigration(vmi *v1.VirtualMachineInstance, c *convertertypes.ConverterContext) error {
for _, volume := range vmi.Spec.Volumes {
if !cbt.IsCBTEligibleVolume(&volume) { continue }
overlayPath := cbt.GetQCOW2OverlayPath(vmi, volumeName)
if isMigrationNewBackendStorage(vmi) {
// 迁移到不同后端存储(sourcePVC ≠ targetPVC)→ 需要创建overlay
// 因为目标端的PVC是新的,需要新的overlay指向它
CreateQCOW2Overlay(overlayPath, imagePath, isBlock)
} else {
// 相同后端存储(RWX模式)→ 复用现有overlay
// overlay指向的PVC在源和目标是同一个
}
applyCBTMap[volumeName] = overlayPath
}
}
DeleteQCOW2Overlay --- 热拔时删除overlay
go
func DeleteQCOW2Overlay(vmi *v1.VirtualMachineInstance, volumeName string) error {
if !cbt.HasCBTStateEnabled(vmi.Status.ChangedBlockTracking) {
return nil // CBT未启用,无需处理
}
overlayPath := cbt.GetQCOW2OverlayPath(vmi, volumeName)
os.Remove(overlayPath) // 删除overlay文件
}
CBT Overlay与备份关系图
#mermaid-svg-sv9VtRPk4W3FVgsI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sv9VtRPk4W3FVgsI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sv9VtRPk4W3FVgsI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sv9VtRPk4W3FVgsI .error-icon{fill:#552222;}#mermaid-svg-sv9VtRPk4W3FVgsI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sv9VtRPk4W3FVgsI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sv9VtRPk4W3FVgsI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sv9VtRPk4W3FVgsI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sv9VtRPk4W3FVgsI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sv9VtRPk4W3FVgsI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sv9VtRPk4W3FVgsI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sv9VtRPk4W3FVgsI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sv9VtRPk4W3FVgsI .marker.cross{stroke:#333333;}#mermaid-svg-sv9VtRPk4W3FVgsI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sv9VtRPk4W3FVgsI p{margin:0;}#mermaid-svg-sv9VtRPk4W3FVgsI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sv9VtRPk4W3FVgsI .cluster-label text{fill:#333;}#mermaid-svg-sv9VtRPk4W3FVgsI .cluster-label span{color:#333;}#mermaid-svg-sv9VtRPk4W3FVgsI .cluster-label span p{background-color:transparent;}#mermaid-svg-sv9VtRPk4W3FVgsI .label text,#mermaid-svg-sv9VtRPk4W3FVgsI span{fill:#333;color:#333;}#mermaid-svg-sv9VtRPk4W3FVgsI .node rect,#mermaid-svg-sv9VtRPk4W3FVgsI .node circle,#mermaid-svg-sv9VtRPk4W3FVgsI .node ellipse,#mermaid-svg-sv9VtRPk4W3FVgsI .node polygon,#mermaid-svg-sv9VtRPk4W3FVgsI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sv9VtRPk4W3FVgsI .rough-node .label text,#mermaid-svg-sv9VtRPk4W3FVgsI .node .label text,#mermaid-svg-sv9VtRPk4W3FVgsI .image-shape .label,#mermaid-svg-sv9VtRPk4W3FVgsI .icon-shape .label{text-anchor:middle;}#mermaid-svg-sv9VtRPk4W3FVgsI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sv9VtRPk4W3FVgsI .rough-node .label,#mermaid-svg-sv9VtRPk4W3FVgsI .node .label,#mermaid-svg-sv9VtRPk4W3FVgsI .image-shape .label,#mermaid-svg-sv9VtRPk4W3FVgsI .icon-shape .label{text-align:center;}#mermaid-svg-sv9VtRPk4W3FVgsI .node.clickable{cursor:pointer;}#mermaid-svg-sv9VtRPk4W3FVgsI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sv9VtRPk4W3FVgsI .arrowheadPath{fill:#333333;}#mermaid-svg-sv9VtRPk4W3FVgsI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sv9VtRPk4W3FVgsI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sv9VtRPk4W3FVgsI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sv9VtRPk4W3FVgsI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sv9VtRPk4W3FVgsI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sv9VtRPk4W3FVgsI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sv9VtRPk4W3FVgsI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sv9VtRPk4W3FVgsI .cluster text{fill:#333;}#mermaid-svg-sv9VtRPk4W3FVgsI .cluster span{color:#333;}#mermaid-svg-sv9VtRPk4W3FVgsI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sv9VtRPk4W3FVgsI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sv9VtRPk4W3FVgsI rect.text{fill:none;stroke-width:0;}#mermaid-svg-sv9VtRPk4W3FVgsI .icon-shape,#mermaid-svg-sv9VtRPk4W3FVgsI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sv9VtRPk4W3FVgsI .icon-shape p,#mermaid-svg-sv9VtRPk4W3FVgsI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sv9VtRPk4W3FVgsI .icon-shape .label rect,#mermaid-svg-sv9VtRPk4W3FVgsI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sv9VtRPk4W3FVgsI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sv9VtRPk4W3FVgsI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sv9VtRPk4W3FVgsI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 全量备份流程
增量备份流程
启用CBT后的磁盘层次
data-file-raw=true
写IO直接到backend
bitmap追踪
记录写入位置
PVC镜像
backend.img
qcow2 overlay
overlay.qcow2
QEMU虚拟机
看到完整磁盘
BackupBegin
创建checkpoint bitmap
bitmap记录变更块位置
增量导出
只传输bitmap标记的块
BackupBegin
无Incremental
导出全部数据
2.7 storage/fsfreeze.go --- 文件系统冻结
模块定位
业务职责:在备份/快照前冻结虚拟机文件系统(通过QEMU Guest Agent),确保磁盘数据一致性。提供安全解冻机制(超时自动解冻),防止因忘记解冻导致虚拟机卡死。
逐行解析
FreezeVMI --- 冻结文件系统
go
func (m *StorageManager) FreezeVMI(vmi *v1.VirtualMachineInstance, unfreezeTimeoutSeconds int32) error {
if m.MigrationInProgress() {
return fmt.Errorf("failed to freeze VMI, VMI is currently during migration")
}
// 1. 查询当前文件系统状态(幂等性检查)
fsfreezeStatus, err := m.getParsedFSStatus(domainName)
if fsfreezeStatus == api.FSFrozen {
return nil // 已冻结,直接返回
}
// 2. TPM设备特殊处理:对swtpm状态目录执行sync
// fsfreeze不会冻结TPM进程的写入,因此主动sync确保数据落盘
if tpm.HasPersistentDevice(&vmi.Spec) {
exec.Command("/usr/bin/sync", util.PathForSwtpm(vmi)).CombinedOutput()
}
// 3. 执行冻结
domain.FSFreeze(nil, 0) // nil=冻结所有文件系统, 0=默认标志
// 4. 启动安全解冻定时器
m.cancelSafetyUnfreeze()
if safetyUnfreezeTimeout != 0 {
go m.scheduleSafetyVMIUnfreeze(vmi, safetyUnfreezeTimeout)
}
return nil
}
UnfreezeVMI --- 解冻文件系统
go
func (m *StorageManager) UnfreezeVMI(vmi *v1.VirtualMachineInstance) error {
m.cancelSafetyUnfreeze() // 取消安全解冻定时器
// 检查当前状态(防止重复解冻触发thaw hook)
fsfreezeStatus, err := m.getParsedFSStatus(domainName)
if err == nil && fsfreezeStatus == api.FSThawed {
return nil // 已解冻
}
domain.FSThaw(nil, 0)
return nil
}
scheduleSafetyVMIUnfreeze --- 安全解冻定时器
go
func (m *StorageManager) scheduleSafetyVMIUnfreeze(vmi *v1.VirtualMachineInstance, unfreezeTimeout time.Duration) {
select {
case <-time.After(unfreezeTimeout):
// 超时未收到UnfreezeVMI调用 → 自动解冻
// 防止备份失败后忘记解冻导致虚拟机I/O完全阻塞
m.UnfreezeVMI(vmi)
case <-m.cancelSafetyUnfreezeChan:
// 收到取消信号(正常的UnfreezeVMI调用触发)
// 正常解冻,取消定时器
}
}
getParsedFSStatus --- 查询文件系统状态
go
func (m *StorageManager) getParsedFSStatus(domainName string) (string, error) {
// 通过QEMU Guest Agent查询
cmdResult, err := m.virConn.QemuAgentCommand(
`{"execute":"guest-fsfreeze-status"}`, domainName)
// 解析返回的 {"return": {"status": "frozen"/"thawed"}}
fsfreezeStatus, err := agentpoller.ParseFSFreezeStatus(cmdResult)
return fsfreezeStatus.Status, nil
}
FSFreeze流程图
Safety Timer QEMU Guest Agent StorageManager Caller Safety Timer QEMU Guest Agent StorageManager Caller #mermaid-svg-wVoyvZSxAMOpIDg2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wVoyvZSxAMOpIDg2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wVoyvZSxAMOpIDg2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wVoyvZSxAMOpIDg2 .error-icon{fill:#552222;}#mermaid-svg-wVoyvZSxAMOpIDg2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wVoyvZSxAMOpIDg2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wVoyvZSxAMOpIDg2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wVoyvZSxAMOpIDg2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wVoyvZSxAMOpIDg2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wVoyvZSxAMOpIDg2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wVoyvZSxAMOpIDg2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wVoyvZSxAMOpIDg2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wVoyvZSxAMOpIDg2 .marker.cross{stroke:#333333;}#mermaid-svg-wVoyvZSxAMOpIDg2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wVoyvZSxAMOpIDg2 p{margin:0;}#mermaid-svg-wVoyvZSxAMOpIDg2 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wVoyvZSxAMOpIDg2 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-wVoyvZSxAMOpIDg2 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wVoyvZSxAMOpIDg2 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-wVoyvZSxAMOpIDg2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-wVoyvZSxAMOpIDg2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-wVoyvZSxAMOpIDg2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-wVoyvZSxAMOpIDg2 .sequenceNumber{fill:white;}#mermaid-svg-wVoyvZSxAMOpIDg2 #sequencenumber{fill:#333;}#mermaid-svg-wVoyvZSxAMOpIDg2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-wVoyvZSxAMOpIDg2 .messageText{fill:#333;stroke:none;}#mermaid-svg-wVoyvZSxAMOpIDg2 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wVoyvZSxAMOpIDg2 .labelText,#mermaid-svg-wVoyvZSxAMOpIDg2 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-wVoyvZSxAMOpIDg2 .loopText,#mermaid-svg-wVoyvZSxAMOpIDg2 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-wVoyvZSxAMOpIDg2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wVoyvZSxAMOpIDg2 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-wVoyvZSxAMOpIDg2 .noteText,#mermaid-svg-wVoyvZSxAMOpIDg2 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-wVoyvZSxAMOpIDg2 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wVoyvZSxAMOpIDg2 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wVoyvZSxAMOpIDg2 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wVoyvZSxAMOpIDg2 .actorPopupMenu{position:absolute;}#mermaid-svg-wVoyvZSxAMOpIDg2 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-wVoyvZSxAMOpIDg2 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wVoyvZSxAMOpIDg2 .actor-man circle,#mermaid-svg-wVoyvZSxAMOpIDg2 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-wVoyvZSxAMOpIDg2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 正常路径 异常路径 FreezeVMI(timeout=300s) guest-fsfreeze-status thawed sync TPM state guest-fsfreeze-freeze OK 启动300s定时器 UnfreezeVMI() cancelSafetyUnfreeze guest-fsfreeze-thaw 300s超时 UnfreezeVMI() 自动解冻
2.8 storage/memoryDump.go --- 内存转储
模块定位
业务职责:将虚拟机内存内容转储到指定文件,用于调试和诊断。使用 libvirt CoreDump API,支持并发控制(同时只允许一个转储)。
逐行解析
go
func (m *StorageManager) MemoryDump(vmi *v1.VirtualMachineInstance, dumpPath string) error {
// 并发控制:缓冲区容量1,非阻塞写入
select {
case m.memoryDumpInProgress <- struct{}{}:
// 获得许可,继续执行
default:
// 已有转储在进行中,直接返回(幂等)
return nil
}
go func() {
defer func() { <-m.memoryDumpInProgress }() // 释放许可
m.memoryDump(vmi, dumpPath)
}()
return nil
}
func (m *StorageManager) memoryDump(vmi *v1.VirtualMachineInstance, dumpPath string) error {
// 1. 检查是否需要跳过(同文件名=正在进行或已完成)
if m.shouldSkipMemoryDump(dumpPath) {
return nil
}
// 2. 初始化元数据
m.initializeMemoryDumpMetadata(dumpPath)
// 3. 清理旧的转储文件
removePreviousMemoryDump(filepath.Dir(dumpPath))
// 4. 执行内存转储
// DUMP_MEMORY_ONLY: 只转储内存,不转储CPU状态
// DOMAIN_CORE_DUMP_FORMAT_RAW: 原始格式(非elf/kdump)
err = dom.CoreDumpWithFormat(dumpPath, libvirt.DOMAIN_CORE_DUMP_FORMAT_RAW, libvirt.DUMP_MEMORY_ONLY)
// 5. 记录结果到元数据
m.setMemoryDumpResult(failed, reason)
}
三、设备相关模块
3.1 hostdevice/hostdev.go --- 主机设备创建
模块定位
业务职责:通用主机设备创建框架,支持三种设备类型的域XML生成:PCI直通、MDEV(mediated device,如vGPU)、USB直通。是GPU/Generic/SR-IOV设备创建的底层公共逻辑。
核心结构
go
type HostDeviceMetaData struct {
AliasPrefix string // 别名前缀(如"gpu-"/"hostdevice-"/"sriov-")
Name string // 设备名
ResourceName string // 资源名(对应device plugin资源)
VirtualGPUOptions *v1.VGPUOptions // vGPU显示选项
DecorateHook func(*api.HostDevice) error // 装饰钩子(如设置客户机PCI地址)
}
type AddressPooler interface {
Pop(key string) (value string, err error) // 从地址池弹出一个地址
}
CreatePCIHostDevices --- 创建PCI直通设备
go
func CreatePCIHostDevices(hostDevicesData []HostDeviceMetaData, pciAddrPool AddressPooler) ([]api.HostDevice, error) {
return createHostDevices(hostDevicesData, pciAddrPool, createPCIHostDevice)
}
createHostDevices --- 通用创建框架
go
func createHostDevices(hostDevicesData []HostDeviceMetaData, addrPool AddressPooler, createHostDev createHostDevice) ([]api.HostDevice, error) {
var hostDevices []api.HostDevice
for _, hostDeviceData := range hostDevicesData {
// 1. 从地址池获取一个地址
address, err := addrPool.Pop(hostDeviceData.ResourceName)
if address == "" { continue } // 地址为空 → 跳过(BestEffort模式)
// 2. 调用特定类型的创建函数
hostDevice, err := createHostDev(hostDeviceData, address)
// 3. 执行装饰钩子(如设置客户机PCI地址、BootOrder)
if hostDeviceData.DecorateHook != nil {
hostDeviceData.DecorateHook(hostDevice)
}
hostDevices = append(hostDevices, *hostDevice)
}
return hostDevices, nil
}
createPCIHostDevice --- 创建PCI设备XML
go
func createPCIHostDevice(hostDeviceData HostDeviceMetaData, hostPCIAddress string) (*api.HostDevice, error) {
hostAddr, err := device.NewPciAddressField(hostPCIAddress)
// 生成:<hostdev type="pci" managed="no">
// <source><address domain="0x..." bus="0x.." slot="0x.." function="0x.."/></source>
// <alias name="ua-{prefix}{name}"/>
return &api.HostDevice{
Alias: api.NewUserDefinedAlias(hostDeviceData.AliasPrefix + hostDeviceData.Name),
Source: api.HostDeviceSource{Address: hostAddr},
Type: api.HostDevicePCI,
Managed: "no", // KubeVirt不使用libvirt的managed模式,自行管理VFIO绑定
}, nil
}
createMDEVHostDevice --- 创建MDEV设备XML
go
func createMDEVHostDevice(hostDeviceData HostDeviceMetaData, mdevUUID string) (*api.HostDevice, error) {
// 生成:<hostdev type="mdev" model="vfio-pci" mode="subsystem">
// <source><address uuid="{mdevUUID}"/></source>
return &api.HostDevice{
Alias: api.NewUserDefinedAlias(hostDeviceData.AliasPrefix + hostDeviceData.Name),
Source: api.HostDeviceSource{
Address: &api.Address{UUID: mdevUUID},
},
Type: api.HostDeviceMDev,
Mode: "subsystem",
Model: "vfio-pci",
}, nil
}
CreateMDEVHostDevices --- MDEV设备创建(带显示选项)
go
func CreateMDEVHostDevices(hostDevicesData []HostDeviceMetaData, mdevAddrPool AddressPooler, enableDefaultDisplay bool) ([]api.HostDevice, error) {
if enableDefaultDisplay {
devices, _ := createHostDevices(hostDevicesData, mdevAddrPool, createMDEVHostDeviceWithDisplay)
// 如果没有显式设置vGPU显示选项,为第一个MDEV设备启用默认显示
if !isVgpuDisplaySet(hostDevicesData) && len(devices) > 0 {
devices[0].Display = "on" // 启用显示输出
devices[0].RamFB = "on" // 启用RAM frame buffer(用于BIOS/UEFI显示)
}
return devices, nil
}
return createHostDevices(hostDevicesData, mdevAddrPool, createMDEVHostDevice)
}
Display/RamFB 的含义:
Display="on":将MDEV设备的显示输出连接到QEMU的显示前端(VNC/SPICE)RamFB="on":启用RAM-based framebuffer,使BIOS/UEFI启动画面能通过MDEV显示
createUSBHostDevice --- 创建USB设备XML
go
func createUSBHostDevice(device HostDeviceMetaData, usbAddress string) (*api.HostDevice, error) {
strs := strings.Split(usbAddress, ":") // 格式 "bus:device"
return &api.HostDevice{
Type: api.HostDeviceUSB,
Mode: "subsystem",
Alias: api.NewUserDefinedAlias("usb-host-" + device.Name),
Source: api.HostDeviceSource{
Address: &api.Address{Bus: strs[0], Device: strs[1]},
},
}, nil
}
设备创建统一流程图
渲染错误: Mermaid 渲染失败: Parse error on line 11: ...F --> GaddrPool.Pop(resourceName) -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
3.2 hostdevice/hotplug.go --- 设备热插拔
模块定位
业务职责:封装主机设备的热插(Attach)和热拔(Detach)操作,以及等待热拔完成的机制。
核心方法
SafelyDetachHostDevices --- 安全热拔
go
func SafelyDetachHostDevices(hostDevices []api.HostDevice, eventDetach EventRegistrar, dom DeviceDetacher, timeout time.Duration) error {
// 1. 注册libvirt事件监听(DEVICE_REMOVED事件)
eventDetach.Register()
defer eventDetach.Deregister()
// 2. 发送DetachDeviceFlags命令
detachHostDevices(dom, hostDevices)
// 3. 等待所有设备从域中移除(通过事件确认)
return waitHostDevicesToDetach(eventDetach, hostDevices, timeout)
}
为什么需要等待事件确认 :DetachDeviceFlags 只是请求libvirt移除设备,实际移除是异步的。必须在收到 DEVICE_REMOVED 事件后才能确认设备已安全移除,避免资源泄漏。
waitHostDevicesToDetach --- 等待设备分离
go
func waitHostDevicesToDetach(eventDetach EventRegistrar, hostDevices []api.HostDevice, timeout time.Duration) error {
var detachedHostDevices []string
desiredDetachCount := len(hostDevices)
for {
select {
case deviceAlias := <-eventDetach.EventChannel():
// 收到设备移除事件
if dev := deviceLookup(hostDevices, deviceAlias.(string)); dev != nil {
detachedHostDevices = append(detachedHostDevices, dev.Alias.GetName())
}
if desiredDetachCount == len(detachedHostDevices) {
return nil // 所有设备都已移除
}
case <-time.After(timeout):
return fmt.Errorf("timeout: %v/%v detached", detachedHostDevices, hostDevicesNames(hostDevices))
}
}
}
AttachHostDevices --- 热插设备
go
func AttachHostDevices(dom deviceAttacher, hostDevices []api.HostDevice) error {
var errs []error
for _, hostDev := range hostDevices {
// 逐个热插,收集所有错误
devXML, _ := xml.Marshal(hostDev)
err = dom.AttachDeviceFlags(string(devXML), affectLiveAndConfigLibvirtFlags)
if err != nil {
errs = append(errs, err)
}
}
// 部分失败也返回错误(不回滚已插入的设备)
if len(errs) > 0 {
return buildAttachHostDevicesErrorMessage(errs)
}
return nil
}
DifferenceHostDevicesByAlias --- 设备差异计算
go
func DifferenceHostDevicesByAlias(desiredHostDevices, actualHostDevices []api.HostDevice) []api.HostDevice {
// 找出期望有但实际没有的设备 → 需要热插
actualHostDevicesByAlias := make(map[string]struct{})
for _, hostDev := range actualHostDevices {
actualHostDevicesByAlias[hostDev.Alias.GetName()] = struct{}{}
}
var filteredSlice []api.HostDevice
for _, desiredHostDevice := range desiredHostDevices {
if _, exists := actualHostDevicesByAlias[desiredHostDevice.Alias.GetName()]; !exists {
filteredSlice = append(filteredSlice, desiredHostDevice)
}
}
return filteredSlice
}
设备热插拔流程图
libvirt事件 Libvirt hostdevice 控制循环 libvirt事件 Libvirt hostdevice 控制循环 #mermaid-svg-Z8QvrfqDaJYAcFUA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Z8QvrfqDaJYAcFUA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Z8QvrfqDaJYAcFUA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Z8QvrfqDaJYAcFUA .error-icon{fill:#552222;}#mermaid-svg-Z8QvrfqDaJYAcFUA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Z8QvrfqDaJYAcFUA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Z8QvrfqDaJYAcFUA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Z8QvrfqDaJYAcFUA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Z8QvrfqDaJYAcFUA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Z8QvrfqDaJYAcFUA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Z8QvrfqDaJYAcFUA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Z8QvrfqDaJYAcFUA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Z8QvrfqDaJYAcFUA .marker.cross{stroke:#333333;}#mermaid-svg-Z8QvrfqDaJYAcFUA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Z8QvrfqDaJYAcFUA p{margin:0;}#mermaid-svg-Z8QvrfqDaJYAcFUA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Z8QvrfqDaJYAcFUA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Z8QvrfqDaJYAcFUA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Z8QvrfqDaJYAcFUA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Z8QvrfqDaJYAcFUA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Z8QvrfqDaJYAcFUA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Z8QvrfqDaJYAcFUA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Z8QvrfqDaJYAcFUA .sequenceNumber{fill:white;}#mermaid-svg-Z8QvrfqDaJYAcFUA #sequencenumber{fill:#333;}#mermaid-svg-Z8QvrfqDaJYAcFUA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Z8QvrfqDaJYAcFUA .messageText{fill:#333;stroke:none;}#mermaid-svg-Z8QvrfqDaJYAcFUA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Z8QvrfqDaJYAcFUA .labelText,#mermaid-svg-Z8QvrfqDaJYAcFUA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Z8QvrfqDaJYAcFUA .loopText,#mermaid-svg-Z8QvrfqDaJYAcFUA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Z8QvrfqDaJYAcFUA .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Z8QvrfqDaJYAcFUA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Z8QvrfqDaJYAcFUA .noteText,#mermaid-svg-Z8QvrfqDaJYAcFUA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Z8QvrfqDaJYAcFUA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Z8QvrfqDaJYAcFUA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Z8QvrfqDaJYAcFUA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Z8QvrfqDaJYAcFUA .actorPopupMenu{position:absolute;}#mermaid-svg-Z8QvrfqDaJYAcFUA .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Z8QvrfqDaJYAcFUA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Z8QvrfqDaJYAcFUA .actor-man circle,#mermaid-svg-Z8QvrfqDaJYAcFUA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Z8QvrfqDaJYAcFUA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 热插流程 热拔流程 AttachHostDevices(期望设备列表) DifferenceHostDevicesByAlias(期望-实际) AttachDeviceFlags(逐个) SafelyDetachHostDevices Register(DEVICE_REMOVED) DetachDeviceFlags(逐个) DEVICE_REMOVED事件 确认设备已移除 Deregister
3.3 hostdevice/addresspool.go --- 地址池
模块定位
业务职责:管理主机设备的地址分配。从环境变量读取可用地址列表,Pop操作弹出地址并从所有资源中去除(防止重复分配)。
核心实现
go
type AddressPool struct {
addressesByResource map[string][]string // 资源名 → 地址列表
}
func NewAddressPool(resourcePrefix string, resources []string) *AddressPool {
pool := &AddressPool{addressesByResource: make(map[string][]string)}
pool.load(resourcePrefix, resources)
return pool
}
func (p *AddressPool) load(resourcePrefix string, resources []string) {
for _, resource := range resources {
// 环境变量名格式:{PREFIX}_{RESOURCE_NAME}
// 例如:PCI_RESOURCE_NVIDIA_COM_GPU=0000:3b:00.0,0000:3b:02.0
addressEnvVarName := util.ResourceNameToEnvVar(resourcePrefix, resource)
addressString, isSet := os.LookupEnv(addressEnvVarName)
if isSet {
p.addressesByResource[resource] = strings.Split(strings.TrimSuffix(addressString, ","), ",")
}
}
}
func (p *AddressPool) Pop(resource string) (string, error) {
addresses, exists := p.addressesByResource[resource]
if !exists { return "", fmt.Errorf("resource %s does not exist", resource) }
if len(addresses) == 0 { return "", fmt.Errorf("no more addresses", resource) }
selectedAddress := addresses[0]
// 关键:从所有资源中移除该地址(防止跨资源重复分配)
for resourceName, resourceAddresses := range p.addressesByResource {
p.addressesByResource[resourceName] = filterOutAddress(resourceAddresses, selectedAddress)
}
return selectedAddress, nil
}
BestEffortAddressPool:
go
type BestEffortAddressPool struct {
pool AddressPooler
}
func (p *BestEffortAddressPool) Pop(resource string) (string, error) {
address, _ := p.pool.Pop(resource) // 忽略错误
return address, nil // 总是返回nil error
}
BestEffort模式的用途:一个设备可能同时出现在PCI和MDEV资源池中(如GPU既支持PCI直通又支持vGPU MDEV),BestEffort允许在一种池中找不到时静默跳过,由另一种池提供地址。
地址池分配流程图
#mermaid-svg-EH2Rs5C072s0TKrl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EH2Rs5C072s0TKrl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EH2Rs5C072s0TKrl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EH2Rs5C072s0TKrl .error-icon{fill:#552222;}#mermaid-svg-EH2Rs5C072s0TKrl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EH2Rs5C072s0TKrl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EH2Rs5C072s0TKrl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EH2Rs5C072s0TKrl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EH2Rs5C072s0TKrl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EH2Rs5C072s0TKrl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EH2Rs5C072s0TKrl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EH2Rs5C072s0TKrl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EH2Rs5C072s0TKrl .marker.cross{stroke:#333333;}#mermaid-svg-EH2Rs5C072s0TKrl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EH2Rs5C072s0TKrl p{margin:0;}#mermaid-svg-EH2Rs5C072s0TKrl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EH2Rs5C072s0TKrl .cluster-label text{fill:#333;}#mermaid-svg-EH2Rs5C072s0TKrl .cluster-label span{color:#333;}#mermaid-svg-EH2Rs5C072s0TKrl .cluster-label span p{background-color:transparent;}#mermaid-svg-EH2Rs5C072s0TKrl .label text,#mermaid-svg-EH2Rs5C072s0TKrl span{fill:#333;color:#333;}#mermaid-svg-EH2Rs5C072s0TKrl .node rect,#mermaid-svg-EH2Rs5C072s0TKrl .node circle,#mermaid-svg-EH2Rs5C072s0TKrl .node ellipse,#mermaid-svg-EH2Rs5C072s0TKrl .node polygon,#mermaid-svg-EH2Rs5C072s0TKrl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EH2Rs5C072s0TKrl .rough-node .label text,#mermaid-svg-EH2Rs5C072s0TKrl .node .label text,#mermaid-svg-EH2Rs5C072s0TKrl .image-shape .label,#mermaid-svg-EH2Rs5C072s0TKrl .icon-shape .label{text-anchor:middle;}#mermaid-svg-EH2Rs5C072s0TKrl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EH2Rs5C072s0TKrl .rough-node .label,#mermaid-svg-EH2Rs5C072s0TKrl .node .label,#mermaid-svg-EH2Rs5C072s0TKrl .image-shape .label,#mermaid-svg-EH2Rs5C072s0TKrl .icon-shape .label{text-align:center;}#mermaid-svg-EH2Rs5C072s0TKrl .node.clickable{cursor:pointer;}#mermaid-svg-EH2Rs5C072s0TKrl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EH2Rs5C072s0TKrl .arrowheadPath{fill:#333333;}#mermaid-svg-EH2Rs5C072s0TKrl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EH2Rs5C072s0TKrl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EH2Rs5C072s0TKrl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EH2Rs5C072s0TKrl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EH2Rs5C072s0TKrl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EH2Rs5C072s0TKrl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EH2Rs5C072s0TKrl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EH2Rs5C072s0TKrl .cluster text{fill:#333;}#mermaid-svg-EH2Rs5C072s0TKrl .cluster span{color:#333;}#mermaid-svg-EH2Rs5C072s0TKrl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EH2Rs5C072s0TKrl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EH2Rs5C072s0TKrl rect.text{fill:none;stroke-width:0;}#mermaid-svg-EH2Rs5C072s0TKrl .icon-shape,#mermaid-svg-EH2Rs5C072s0TKrl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EH2Rs5C072s0TKrl .icon-shape p,#mermaid-svg-EH2Rs5C072s0TKrl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EH2Rs5C072s0TKrl .icon-shape .label rect,#mermaid-svg-EH2Rs5C072s0TKrl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EH2Rs5C072s0TKrl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EH2Rs5C072s0TKrl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EH2Rs5C072s0TKrl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 设置环境变量
设置环境变量
BestEffort包装
BestEffortAddressPool
Pop: 忽略错误
空地址 → 跳过设备
Device Plugin
PCI_RESOURCE_X=addr1,addr2
MDEV_RESOURCE_X=uuid1,uuid2
AddressPool.load
addressesByResource
Pop: 取第一个地址
从所有资源列表中移除该地址
返回地址
3.4 generic/ --- 通用主机设备
模块定位
业务职责:处理非GPU的通用主机设备(HostDevice CRD字段),支持PCI、MDEV、USB三种类型的设备分配。
逐行解析
go
const AliasPrefix = "hostdevice-" // 别名前缀
func CreateHostDevices(vmiHostDevices []v1.HostDevice) ([]api.HostDevice, error) {
return CreateHostDevicesFromPools(vmiHostDevices,
NewPCIAddressPool(vmiHostDevices),
NewMDEVAddressPool(vmiHostDevices),
NewUSBAddressPool(vmiHostDevices))
}
func CreateHostDevicesFromPools(vmiHostDevices []v1.HostDevice, pciAddressPool, mdevAddressPool, usbAddressPool hostdevice.AddressPooler) ([]api.HostDevice, error) {
// 1. 用BestEffort包装各地址池(设备可能只属于某一种类型)
pciPool := hostdevice.NewBestEffortAddressPool(pciAddressPool)
mdevPool := hostdevice.NewBestEffortAddressPool(mdevAddressPool)
usbPool := hostdevice.NewBestEffortAddressPool(usbAddressPool)
// 2. 生成设备元数据
hostDevicesMetaData := createHostDevicesMetadata(vmiHostDevices)
// 3. 分别从三种池创建设备
pciHostDevices, _ := hostdevice.CreatePCIHostDevices(hostDevicesMetaData, pciPool)
mdevHostDevices, _ := hostdevice.CreateMDEVHostDevices(hostDevicesMetaData, mdevPool, false) // 通用设备不启用display
usbHostDevices, _ := hostdevice.CreateUSBHostDevices(hostDevicesMetaData, usbPool)
// 4. 合并
hostDevices := append(pciHostDevices, mdevHostDevices...)
hostDevices = append(hostDevices, usbHostDevices...)
// 5. 验证:非DRA设备数量必须与生成的hostDevice数量匹配
validateCreationOfDevicePluginsDevices(vmiHostDevices, hostDevices)
return hostDevices, nil
}
地址池工厂:
go
func NewPCIAddressPool(hostDevices []v1.HostDevice) *hostdevice.AddressPool {
return hostdevice.NewAddressPool(v1.PCIResourcePrefix, extractResources(hostDevices))
// PCI_RESOURCE_{DEVICENAME}=0000:3b:00.0,0000:5c:00.0
}
func NewMDEVAddressPool(hostDevices []v1.HostDevice) *hostdevice.AddressPool {
return hostdevice.NewAddressPool(v1.MDevResourcePrefix, extractResources(hostDevices))
// MDEV_RESOURCE_{DEVICENAME}=uuid1,uuid2
}
func NewUSBAddressPool(hostDevices []v1.HostDevice) *hostdevice.AddressPool {
return hostdevice.NewAddressPool(v1.USBResourcePrefix, extractResources(hostDevices))
// USB_RESOURCE_{DEVICENAME}=1:2,3:4
}
DRA验证排除 :validateCreationOfDevicePluginsDevices 排除了DRA(Dynamic Resource Allocation)设备,因为DRA设备不通过device plugin环境变量获取地址,而是通过Kubernetes ResourceClaim机制。
3.5 gpu/ --- GPU设备
模块定位
业务职责:处理GPU设备分配,与generic类似但有两个关键差异:
- 默认启用显示 (
DefaultDisplayOn=true)--- vGPU通常需要显示输出 - 支持vGPU显示选项(VirtualGPUOptions)--- 控制Display和RamFB
逐行解析
go
const (
AliasPrefix = "gpu-"
DefaultDisplayOn = true // GPU默认启用显示
)
func CreateHostDevicesFromPools(vmiGPUs []v1.GPU, pciAddressPool, mdevAddressPool hostdevice.AddressPooler) ([]api.HostDevice, error) {
hostDevicesMetaData := createHostDevicesMetadata(vmiGPUs)
// GPU的metadata包含VirtualGPUOptions
for _, dev := range vmiGPUs {
hostDevicesMetaData = append(hostDevicesMetaData, hostdevice.HostDeviceMetaData{
AliasPrefix: AliasPrefix,
Name: dev.Name,
ResourceName: dev.DeviceName,
VirtualGPUOptions: dev.VirtualGPUOptions, // ← 关键差异:携带显示选项
})
}
pciHostDevices, _ := hostdevice.CreatePCIHostDevices(hostDevicesMetaData, pciPool)
mdevHostDevices, _ := hostdevice.CreateMDEVHostDevices(hostDevicesMetaData, mdevPool, DefaultDisplayOn)
// ↑ GPU默认启用显示
hostDevices := append(pciHostDevices, mdevHostDevices...)
validateCreationOfDevicePluginsDevices(vmiGPUs, hostDevices) // DRA设备排除
}
GPU vs Generic 对比:
| 特性 | Generic | GPU |
|---|---|---|
| 别名前缀 | hostdevice- |
gpu- |
| 默认Display | 关 | 开 |
| VirtualGPUOptions | 无 | 支持 |
| USB支持 | 有 | 无 |
3.6 sriov/ --- SR-IOV设备
模块定位
业务职责:处理SR-IOV网络接口的设备直通。与generic/gpu不同,SR-IOV的PCI地址不来自device plugin环境变量,而是来自multus downward API的网络状态文件。
逐行解析
CreateHostDevices --- 创建SR-IOV设备
go
func CreateHostDevices(vmi *v1.VirtualMachineInstance) ([]api.HostDevice, error) {
// 1. 过滤出SR-IOV接口(有SRIOV字段 + MultusStatus存在)
SRIOVInterfaces := vmispec.FilterInterfacesSpec(vmi.Spec.Domain.Devices.Interfaces, func(iface v1.Interface) bool {
if iface.SRIOV == nil { return false }
ifaceStatus := vmispec.LookupInterfaceStatusByName(vmi.Status.Interfaces, iface.Name)
return ifaceStatus != nil && vmispec.ContainsInfoSource(ifaceStatus.InfoSource, vmispec.InfoSourceMultusStatus)
})
if len(SRIOVInterfaces) == 0 {
return []api.HostDevice{}, nil
}
// 2. 从downward API文件读取网络状态(包含PCI地址映射)
netStatusPath := path.Join(downwardapi.MountPath, downwardapi.NetworkInfoVolumePath)
pciAddressPool, err := newPCIAddressPoolWithNetworkStatusFromFile(netStatusPath)
// 3. 使用SR-IOV专用地址池创建PCI hostdev
return CreateHostDevicesFromIfacesAndPool(SRIOVInterfaces, pciAddressPoolWithNetworkStatus)
}
newDecorateHook --- SR-IOV装饰钩子
go
func newDecorateHook(iface v1.Interface) func(hostDevice *api.HostDevice) error {
return func(hostDevice *api.HostDevice) error {
// 设置客户机内PCI地址(用户可指定)
if guestPCIAddress := iface.PciAddress; guestPCIAddress != "" {
addr, _ := device.NewPciAddressField(guestPCIAddress)
hostDevice.Address = addr // 虚拟机内看到的PCI地址
}
// 设置启动顺序(PXE启动)
if iface.BootOrder != nil {
hostDevice.BootOrder = &api.BootOrder{Order: *iface.BootOrder}
}
return nil
}
}
PCIAddressWithNetworkStatusPool --- 网络状态PCI地址池
go
type PCIAddressWithNetworkStatusPool struct {
networkPCIMap map[string]string // 网络名 → PCI地址
}
func NewPCIAddressPoolWithNetworkStatus(networkInfoBytes []byte) (*PCIAddressWithNetworkStatusPool, error) {
// 从downward API的NetworkInfo解析
var networkInfo downwardapi.NetworkInfo
json.Unmarshal(networkInfoBytes, &networkInfo)
// 提取每个网络的PCI地址
for _, iface := range networkInfo.Interfaces {
if iface.DeviceInfo != nil && iface.DeviceInfo.Pci != nil && iface.DeviceInfo.Pci.PciAddress != "" {
networkPCIMap[iface.Network] = iface.DeviceInfo.Pci.PciAddress
}
}
}
func (p *PCIAddressWithNetworkStatusPool) Pop(networkName string) (string, error) {
pciAddress, exists := p.networkPCIMap[networkName]
delete(p.networkPCIMap, networkName) // 弹出后删除
return pciAddress, nil
}
GetHostDevicesToAttach --- 获取需要热插的SR-IOV设备
go
func GetHostDevicesToAttach(vmi *v1.VirtualMachineInstance, domainSpec *api.DomainSpec) ([]api.HostDevice, error) {
// 计算期望设备与当前设备的差异
sriovDevices, _ := CreateHostDevices(vmi) // 期望
currentAttachedSRIOVHostDevices := hostdevice.FilterHostDevicesByAlias(
domainSpec.Devices.HostDevices, deviceinfo.SRIOVAliasPrefix) // 当前
return hostdevice.DifferenceHostDevicesByAlias(sriovDevices, currentAttachedSRIOVHostDevices), nil
}
SR-IOV设备分配流程图
Multus downward API PCIAddressPool sriov包 VMI Multus downward API PCIAddressPool sriov包 VMI #mermaid-svg-jq9uYw9Cf8VtcIkN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jq9uYw9Cf8VtcIkN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jq9uYw9Cf8VtcIkN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jq9uYw9Cf8VtcIkN .error-icon{fill:#552222;}#mermaid-svg-jq9uYw9Cf8VtcIkN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jq9uYw9Cf8VtcIkN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jq9uYw9Cf8VtcIkN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jq9uYw9Cf8VtcIkN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jq9uYw9Cf8VtcIkN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jq9uYw9Cf8VtcIkN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jq9uYw9Cf8VtcIkN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jq9uYw9Cf8VtcIkN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jq9uYw9Cf8VtcIkN .marker.cross{stroke:#333333;}#mermaid-svg-jq9uYw9Cf8VtcIkN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jq9uYw9Cf8VtcIkN p{margin:0;}#mermaid-svg-jq9uYw9Cf8VtcIkN .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jq9uYw9Cf8VtcIkN text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-jq9uYw9Cf8VtcIkN .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jq9uYw9Cf8VtcIkN .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-jq9uYw9Cf8VtcIkN .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-jq9uYw9Cf8VtcIkN .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-jq9uYw9Cf8VtcIkN #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-jq9uYw9Cf8VtcIkN .sequenceNumber{fill:white;}#mermaid-svg-jq9uYw9Cf8VtcIkN #sequencenumber{fill:#333;}#mermaid-svg-jq9uYw9Cf8VtcIkN #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-jq9uYw9Cf8VtcIkN .messageText{fill:#333;stroke:none;}#mermaid-svg-jq9uYw9Cf8VtcIkN .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jq9uYw9Cf8VtcIkN .labelText,#mermaid-svg-jq9uYw9Cf8VtcIkN .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-jq9uYw9Cf8VtcIkN .loopText,#mermaid-svg-jq9uYw9Cf8VtcIkN .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-jq9uYw9Cf8VtcIkN .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jq9uYw9Cf8VtcIkN .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-jq9uYw9Cf8VtcIkN .noteText,#mermaid-svg-jq9uYw9Cf8VtcIkN .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-jq9uYw9Cf8VtcIkN .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jq9uYw9Cf8VtcIkN .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jq9uYw9Cf8VtcIkN .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jq9uYw9Cf8VtcIkN .actorPopupMenu{position:absolute;}#mermaid-svg-jq9uYw9Cf8VtcIkN .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-jq9uYw9Cf8VtcIkN .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jq9uYw9Cf8VtcIkN .actor-man circle,#mermaid-svg-jq9uYw9Cf8VtcIkN line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-jq9uYw9Cf8VtcIkN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 写入网络信息(含PCI地址) CreateHostDevices(vmi) 读取NetworkInfo文件 {Network: "net1", PCI: "0000:04:00.1"} NewPCIAddressPoolWithNetworkStatus createHostDevicesMetadata + DecorateHook Pop("net1") → "0000:04:00.1" PCI地址 createPCIHostDevice + DecorateHook(设置guest PCI/BootOrder)
3.7 dra/ --- DRA设备分配
模块定位
业务职责:处理通过 Kubernetes DRA(Dynamic Resource Allocation)机制分配的设备,替代传统的 device plugin 模式。DRA 通过 ResourceClaim 获取设备,设备信息来自 claim 元数据而非环境变量。
3.7.1 dra/generic_hostdev.go
go
const DRAHostDeviceAliasPrefix = "dra-hostdevice-"
func CreateDRAHostDevices(vmi *v1.VirtualMachineInstance, basePath string) ([]api.HostDevice, error) {
if !hasHostDevicesWithDRA(vmi) { return nil, nil }
for _, hd := range vmi.Spec.Domain.Devices.HostDevices {
if !drautil.IsHostDeviceDRA(hd) { continue }
hostDevice, err := createHostDeviceForHostDevice(hd, basePath, vmi.Spec)
// ...
}
validateCreationOfDRAHostDevices(...)
}
func createHostDeviceForHostDevice(hd v1.HostDevice, basePath string, vmiSpecs v1.VirtualMachineInstanceSpec) (*api.HostDevice, error) {
claimName := hd.ClaimRequest.ClaimName
requestName := hd.ClaimRequest.RequestName
// 优先检查mdevUUID(mediated device)
// 如果设备同时有mdevUUID和pciBusID,说明是vGPU而非PCI直通
mdevUUID, mdevErr := drautil.GetMDevUUIDForClaim(basePath, resourceClaims, claimName, requestName)
if mdevErr == nil {
// MDEV类型
model := "vfio-pci"
if vmiSpecs.Architecture == "s390x" {
model = "vfio-ap" // s390x架构使用vfio-ap(加密协处理器)
}
return &api.HostDevice{
Alias: api.NewUserDefinedAlias(DRAHostDeviceAliasPrefix + hd.Name),
Source: api.HostDeviceSource{Address: &api.Address{UUID: mdevUUID}},
Type: api.HostDeviceMDev,
Model: model,
}, nil
}
// 其次检查PCI地址
pciAddr, pciErr := drautil.GetPCIAddressForClaim(basePath, resourceClaims, claimName, requestName)
if pciErr == nil {
hostAddr, _ := device.NewPciAddressField(pciAddr)
return &api.HostDevice{
Alias: api.NewUserDefinedAlias(DRAHostDeviceAliasPrefix + hd.Name),
Source: api.HostDeviceSource{Address: hostAddr},
Type: api.HostDevicePCI,
Managed: "no",
}, nil
}
// 两种都不匹配 → 错误
return nil, fmt.Errorf("HostDevice %s has no mdevUUID or pciBusID", hd.Name)
}
3.7.2 dra/gpu_hostdev.go
go
const AliasPrefix = "dra-gpu-"
func CreateDRAGPUHostDevices(vmi *v1.VirtualMachineInstance, basePath string) ([]api.HostDevice, error) {
// 与generic类似,但:
// 1. 默认启用Display(与GPU行为一致)
// 2. 支持VirtualGPUOptions(Display/RamFB控制)
// 3. 为第一个没有显式display选项的vGPU自动启用Display+RamFB
for _, gpu := range vmi.Spec.Domain.Devices.GPUs {
if !drautil.IsGPUDRA(gpu) { continue }
hostDevice, err := createHostDeviceForGPU(gpu, basePath, vmi.Spec.ResourceClaims)
// ...
}
// 默认显示:第一个vGPU启用Display+RamFB
if DefaultDisplayOn && !isVgpuDisplaySet(vmi.Spec.Domain.Devices.GPUs) {
for i := range hostDevices {
if hostDevices[i].Type == api.HostDeviceMDev {
hostDevices[i].Display = "on"
hostDevices[i].RamFB = "on"
break
}
}
}
}
func createHostDeviceForGPU(gpu v1.GPU, basePath string, resourceClaims []k8sv1.PodResourceClaim) (*api.HostDevice, error) {
// 同generic的逻辑:先检查MDEV,再检查PCI
// 额外处理VirtualGPUOptions:
if gpu.VirtualGPUOptions != nil && gpu.VirtualGPUOptions.Display != nil {
displayEnabled := gpu.VirtualGPUOptions.Display.Enabled
if displayEnabled == nil || *displayEnabled {
hostDevice.Display = "on"
if gpu.VirtualGPUOptions.Display.RamFB == nil || *gpu.VirtualGPUOptions.Display.RamFB.Enabled {
hostDevice.RamFB = "on"
}
}
}
}
DRA vs Device Plugin 对比图
#mermaid-svg-ChpEFr8Tm6Tqcupi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ChpEFr8Tm6Tqcupi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ChpEFr8Tm6Tqcupi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ChpEFr8Tm6Tqcupi .error-icon{fill:#552222;}#mermaid-svg-ChpEFr8Tm6Tqcupi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ChpEFr8Tm6Tqcupi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ChpEFr8Tm6Tqcupi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ChpEFr8Tm6Tqcupi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ChpEFr8Tm6Tqcupi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ChpEFr8Tm6Tqcupi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ChpEFr8Tm6Tqcupi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ChpEFr8Tm6Tqcupi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ChpEFr8Tm6Tqcupi .marker.cross{stroke:#333333;}#mermaid-svg-ChpEFr8Tm6Tqcupi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ChpEFr8Tm6Tqcupi p{margin:0;}#mermaid-svg-ChpEFr8Tm6Tqcupi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ChpEFr8Tm6Tqcupi .cluster-label text{fill:#333;}#mermaid-svg-ChpEFr8Tm6Tqcupi .cluster-label span{color:#333;}#mermaid-svg-ChpEFr8Tm6Tqcupi .cluster-label span p{background-color:transparent;}#mermaid-svg-ChpEFr8Tm6Tqcupi .label text,#mermaid-svg-ChpEFr8Tm6Tqcupi span{fill:#333;color:#333;}#mermaid-svg-ChpEFr8Tm6Tqcupi .node rect,#mermaid-svg-ChpEFr8Tm6Tqcupi .node circle,#mermaid-svg-ChpEFr8Tm6Tqcupi .node ellipse,#mermaid-svg-ChpEFr8Tm6Tqcupi .node polygon,#mermaid-svg-ChpEFr8Tm6Tqcupi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ChpEFr8Tm6Tqcupi .rough-node .label text,#mermaid-svg-ChpEFr8Tm6Tqcupi .node .label text,#mermaid-svg-ChpEFr8Tm6Tqcupi .image-shape .label,#mermaid-svg-ChpEFr8Tm6Tqcupi .icon-shape .label{text-anchor:middle;}#mermaid-svg-ChpEFr8Tm6Tqcupi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ChpEFr8Tm6Tqcupi .rough-node .label,#mermaid-svg-ChpEFr8Tm6Tqcupi .node .label,#mermaid-svg-ChpEFr8Tm6Tqcupi .image-shape .label,#mermaid-svg-ChpEFr8Tm6Tqcupi .icon-shape .label{text-align:center;}#mermaid-svg-ChpEFr8Tm6Tqcupi .node.clickable{cursor:pointer;}#mermaid-svg-ChpEFr8Tm6Tqcupi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ChpEFr8Tm6Tqcupi .arrowheadPath{fill:#333333;}#mermaid-svg-ChpEFr8Tm6Tqcupi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ChpEFr8Tm6Tqcupi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ChpEFr8Tm6Tqcupi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ChpEFr8Tm6Tqcupi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ChpEFr8Tm6Tqcupi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ChpEFr8Tm6Tqcupi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ChpEFr8Tm6Tqcupi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ChpEFr8Tm6Tqcupi .cluster text{fill:#333;}#mermaid-svg-ChpEFr8Tm6Tqcupi .cluster span{color:#333;}#mermaid-svg-ChpEFr8Tm6Tqcupi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ChpEFr8Tm6Tqcupi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ChpEFr8Tm6Tqcupi rect.text{fill:none;stroke-width:0;}#mermaid-svg-ChpEFr8Tm6Tqcupi .icon-shape,#mermaid-svg-ChpEFr8Tm6Tqcupi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ChpEFr8Tm6Tqcupi .icon-shape p,#mermaid-svg-ChpEFr8Tm6Tqcupi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ChpEFr8Tm6Tqcupi .icon-shape .label rect,#mermaid-svg-ChpEFr8Tm6Tqcupi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ChpEFr8Tm6Tqcupi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ChpEFr8Tm6Tqcupi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ChpEFr8Tm6Tqcupi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} DRA模式
Device Plugin模式
环境变量
claim元数据
Device Plugin
AddressPool
PCI_RESOURCE_X=addr1,addr2
Pop分配
ResourceClaim
drautil.GetPCI/GetMDev
直接获取地址/UUID
创建HostDevice XML
3.8 device/pciaddress.go --- PCI地址工具
逐行解析
go
func NewPciAddressField(address string) (*api.Address, error) {
// 输入格式: "0000:3b:00.0" (domain:bus:slot.function)
dbsfFields, err := hwutil.ParsePciAddress(address)
// 解析为 [domain, bus, slot, function] 四个字段
return &api.Address{
Type: api.AddressPCI, // "pci"
Domain: "0x" + dbsfFields[0], // "0x0000"
Bus: "0x" + dbsfFields[1], // "0x3b"
Slot: "0x" + dbsfFields[2], // "0x00"
Function: "0x" + dbsfFields[3], // "0x0"
}, nil
}
3.9 device/usbaddress.go --- USB地址工具
go
func USBDevicesFound(vmiHostDevices []v1.HostDevice) bool {
// 检查USB设备是否存在(通过环境变量)
for _, device := range vmiHostDevices {
env := util.ResourceNameToEnvVar(v1.USBResourcePrefix, device.DeviceName)
if _, ok := os.LookupEnv(env); ok {
return true
}
}
return false
}
四、其他外设模块
4.1 efi/efi.go --- EFI固件
模块定位
业务职责:检测和选择EFI固件文件,支持多种安全VM类型(普通/SecureBoot/SEV/SNP/TDX)和架构(x86/arm64)。
核心结构
go
type EFIEnvironment struct {
code string // 普通EFI Code
vars string // 普通EFI Vars
codeSecureBoot string // SecureBoot Code
varsSecureBoot string // SecureBoot Vars
codeSEV string // AMD SEV Code
varsSEV string // AMD SEV Vars
codeSNP string // AMD SNP Code
codeTDX string // Intel TDX Code
codeTDXSecureBoot string // Intel TDX + SecureBoot Code
}
type SecureVMType int
const (
None SecureVMType = iota // 普通VM
SEV // AMD SEV/SEV-ES
SNP // AMD SNP
TDX // Intel TDX
)
DetectEFIEnvironment --- 自动检测
go
func DetectEFIEnvironment(arch, ovmfPath string) *EFIEnvironment {
if arch == "arm64" {
// ARM64只有普通EFI(无SecureBoot/SEV/TDX)
return &EFIEnvironment{
code: getEFIBinaryIfExists(ovmfPath, "AAVMF_CODE.fd"),
vars: getEFIBinaryIfExists(ovmfPath, "AAVMF_VARS.fd"),
}
}
// x86_64: 检测所有固件变体
// 注意:code优先使用SecureBoot版本(即使不启用SecureBoot)
// 因为SecureBoot固件包含完整的UEFI功能,禁用SecureBoot仍可使用
code := codeWithSB // 优先OVMF_CODE.secboot.fd
if code == "" {
code = getEFIBinaryIfExists(ovmfPath, "OVMF_CODE.fd") // 回退到普通版本
}
return &EFIEnvironment{
codeSecureBoot: getEFIBinaryIfExists(ovmfPath, "OVMF_CODE.secboot.fd"),
varsSecureBoot: getEFIBinaryIfExists(ovmfPath, "OVMF_VARS.secboot.fd"),
code: code,
vars: getEFIBinaryIfExists(ovmfPath, "OVMF_VARS.fd"),
codeSEV: getEFIBinaryIfExists(ovmfPath, "OVMF_CODE.cc.fd"),
varsSEV: getEFIBinaryIfExists(ovmfPath, "OVMF_VARS.fd"), // SEV共享普通VARS
codeSNP: getEFIBinaryIfExists(ovmfPath, "OVMF.amdsev.fd"),
codeTDX: getEFIBinaryIfExists(ovmfPath, "OVMF.inteltdx.fd"),
codeTDXSecureBoot: getEFIBinaryIfExists(ovmfPath, "OVMF.inteltdx.secboot.fd"),
}
}
EFICode / EFIVars / Bootable --- 选择逻辑
go
func (e *EFIEnvironment) EFICode(secureBoot bool, vmType SecureVMType) string {
switch vmType {
case SEV:
if secureBoot { return "" } // SEV不支持SecureBoot
return e.codeSEV // OVMF_CODE.cc.fd
case SNP:
if secureBoot { return "" } // SNP不支持SecureBoot
return e.codeSNP // OVMF.amdsev.fd
case TDX:
if secureBoot { return e.codeTDXSecureBoot } // TDX支持SecureBoot
return e.codeTDX // OVMF.inteltdx.fd
default:
if secureBoot { return e.codeSecureBoot }
return e.code // OVMF_CODE.secboot.fd 或 OVMF_CODE.fd
}
}
func (e *EFIEnvironment) EFIVars(secureBoot bool, vmType SecureVMType) string {
switch vmType {
case SEV:
return e.varsSEV // SEV使用普通VARS
case SNP, TDX:
return "" // SNP/TDX无状态固件,不需要VARS
default:
if secureBoot { return e.varsSecureBoot }
return e.vars
}
}
EFI固件选择矩阵图
渲染错误: Mermaid 渲染失败: Parse error on line 13: ...amdsev.fd
- 无VARS(无状态)] A -- -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
4.2 launchsecurity/sev.go --- SEV安全启动
模块定位
业务职责:将KubeVirt SEV策略规格转换为AMD SEV/SEV-SNP的guest policy位图。guest policy是SEV固件的访问控制机制。
逐行解析
go
const (
SEVPolicyNoDebug uint = 1 << 0 // Bit 0: 禁止调试(SEV/SEV-ES始终设置)
SEVPolicyEncryptedState uint = 1 << 2 // Bit 2: 加密状态(SEV-ES启用)
SNPPolicySmt uint = 1 << 16 // Bit 16: SMT允许
SNPPolicyReserved uint = 1 << 17 // Bit 17: 保留位(必须设置)
)
func SEVPolicyToBits(policy *v1.SEVPolicy) uint {
bits := uint(SEVPolicyNoDebug) // 始终禁止调试(安全要求)
if policy != nil {
if policy.EncryptedState != nil && *policy.EncryptedState {
bits = bits | SEVPolicyEncryptedState // 启用SEV-ES(加密寄存器状态)
}
}
return bits
}
func SEVSNPPolicyToBits(policy *v1.SEVSNP) uint {
if policy != nil {
return SNPPolicySmt | SNPPolicyReserved // SNP必须设置SMT和Reserved
}
return 0
}
Policy位含义:
NoDebug:禁止外部调试器附加到SEV虚拟机EncryptedState:SEV-ES模式,加密CPU寄存器状态(防止VMEXIT时的寄存器泄露)SNP SMT:允许同时多线程(SMT/SMT)SNP Reserved:AMD SNP规范要求的保留位
4.3 cpudedicated/cpudedicated.go --- CPU绑核
模块定位
业务职责:在迁移场景下重新计算目标节点的CPU亲和性,生成更新后的域XML。专用CPU(CPU Dedication/NUMA/Pinning)在迁移到不同拓扑的目标节点时,需要重新映射vCPU到pCPU。
逐行解析
GenerateDomainForTargetCPUSetAndTopology
go
func GenerateDomainForTargetCPUSetAndTopology(vmi *v1.VirtualMachineInstance, domSpec *api.DomainSpec) (*api.Domain, error) {
// 1. 解析目标节点的CPU拓扑
var targetTopology cmdv1.Topology
json.Unmarshal([]byte(vmi.Status.MigrationState.TargetNodeTopology), &targetTopology)
// 2. 检查是否使用专用IO线程
useIOThreads := false
for _, diskDevice := range vmi.Spec.Domain.Devices.Disks {
if diskDevice.DedicatedIOThread != nil && *diskDevice.DedicatedIOThread {
useIOThreads = true
break
}
}
// 3. 创建最小域定义
domain := api.NewMinimalDomain(vmi.Name)
domain.Spec = *domSpec
// 4. 计算vCPU拓扑(考虑MaxSockets热插上限)
cpuTopology := vcpu.GetCPUTopology(vmi)
if vmiCPU != nil && vmiCPU.MaxSockets != 0 {
cpuTopology.Sockets = vmiCPU.MaxSockets
cpuCount = vcpu.CalculateRequestedVCPUs(cpuTopology)
}
domain.Spec.CPU.Topology = cpuTopology
domain.Spec.VCPU = &api.VCPU{Placement: "static", CPUs: cpuCount}
// 5. 根据目标节点拓扑和CPUSet调整域
vcpu.AdjustDomainForTopologyAndCPUSet(domain, vmi, &targetTopology, targetNodeCPUSet, useIOThreads)
return domain, err
}
ConvertCPUDedicatedFields
go
func ConvertCPUDedicatedFields(domain *api.Domain, domcfg *libvirtxml.Domain) error {
// 将内部api.Domain类型转换为libvirt原生XML类型
if domcfg.CPU == nil { domcfg.CPU = &libvirtxml.DomainCPU{} }
domcfg.CPU.Topology = convxml.ConvertKubeVirtCPUTopologyToDomainCPUTopology(domain.Spec.CPU.Topology)
domcfg.VCPU = convxml.ConvertKubeVirtVCPUToDomainVCPU(domain.Spec.VCPU)
domcfg.CPUTune = convxml.ConvertKubeVirtCPUTuneToDomainCPUTune(domain.Spec.CPUTune)
domcfg.NUMATune = convxml.ConvertKubeVirtNUMATuneToDomainNUMATune(domain.Spec.NUMATune)
domcfg.Features = convxml.ConvertKubeVirtFeaturesToDomainFeatureList(domain.Spec.Features)
return nil
}
4.4 premigration-hook-server --- 迁移前Hook
模块定位
业务职责:实现 libvirt 迁移前 Hook 服务器,在 VM 实时迁移的目标端修改域 XML。通过 Unix socket 接收 libvirt 发送的域 XML,经过注册的 Hook 函数修改后返回。
设计背景:libvirt 在迁移目标端创建域之前,会调用外部 Hook 程序,允许修改域定义。KubeVirt 利用这个机制在目标端执行必要的域 XML 调整。
4.4.1 hook_server.go --- Hook服务器核心
核心结构
go
type HookFunc func(c *convertertypes.ConverterContext, vmi *v1.VirtualMachineInstance, domain *libvirtxml.Domain) error
type PreMigrationHookServer struct {
c *convertertypes.ConverterContext // 转换上下文
hooks []HookFunc // 注册的Hook函数列表
stopChan chan struct{} // 停止信号
done chan struct{} // 完成信号
startOnce sync.Once // 只启动一次
socket net.Listener // Unix socket监听器
}
Start --- 启动服务器
go
func (h *PreMigrationHookServer) Start(c *convertertypes.ConverterContext) error {
h.c = c // 保存转换上下文(包含VMI、网络配置、设备列表等)
h.startOnce.Do(func() {
const socketPath = "/var/run/kubevirt/migration-hook-socket"
socket, _ := net.Listen("unix", socketPath)
h.socket = socket
// goroutine 1: 监听停止信号或连接处理完成
go func() {
select {
case <-h.stopChan: // VMI停止
case <-connectionHandled: // Hook处理完成
}
h.socket.Close()
os.Remove(socketPath) // 清理socket文件
close(h.done)
}()
// goroutine 2: 等待libvirt连接
go func() {
conn, _ := h.socket.Accept() // 只接受一个连接
h.handleConnection(conn)
conn.Close()
h.c = nil // 释放ConverterContext引用
}()
})
}
processHook --- 处理Hook请求
go
func (h *PreMigrationHookServer) processHook(conn net.Conn) error {
// 1. 从连接中解码libvirt发送的域XML
var domain libvirtxml.Domain
xml.NewDecoder(conn).Decode(&domain)
// 2. 逐一执行注册的Hook函数
for _, hook := range h.hooks {
if err := hook(h.c, h.c.VirtualMachine, &domain); err != nil {
return err
}
}
// 3. 将修改后的域XML编码回连接
xml.NewEncoder(conn).Encode(&domain)
return nil
}
协议流程:
- libvirt 迁移到目标端时,通过 Unix socket 发送域 XML
- Hook服务器解码XML,执行所有注册的Hook函数
- 修改后的XML编码回连接
- libvirt 使用修改后的XML创建目标域
4.4.2 cpuhook/cpu_hook.go --- CPU绑核Hook
go
func CPUDedicatedHook(_ *convertertypes.ConverterContext, vmi *v1.VirtualMachineInstance, domain *libvirtxml.Domain) error {
if !vmi.IsCPUDedicated() {
return nil // 非专用CPU,无需处理
}
// 1. 将libvirt XML转为内部api.DomainSpec
xmlstr, _ := domain.Marshal()
var apiDomainSpec api.DomainSpec
xml.Unmarshal([]byte(xmlstr), &apiDomainSpec)
// 2. 根据目标节点CPU拓扑重新生成域定义
processedDomain, _ := cpudedicated.GenerateDomainForTargetCPUSetAndTopology(vmi, &apiDomainSpec)
// 3. 将专用CPU字段写回libvirt XML
cpudedicated.ConvertCPUDedicatedFields(processedDomain, domain)
}
为什么需要CPU Hook:源节点和目标节点的CPU拓扑可能不同(不同NUMA布局、不同pCPU集合)。迁移后需要在目标端重新绑定vCPU到正确的pCPU。
4.4.3 network/ordinal_naming.go --- 网络命名升级Hook
go
func UpgradeOrdinalNamingScheme(_ *convertertypes.ConverterContext, vmi *v1.VirtualMachineInstance, domain *libvirtxml.Domain) error {
ordinalPattern := regexp.MustCompile(`^tap\d+$`) // 匹配旧的序数命名: tap0, tap1, ...
hashedPodNamingScheme := namescheme.CreateHashedNetworkNameScheme(vmi.Spec.Networks)
for i := range domain.Devices.Interfaces {
iface := &domain.Devices.Interfaces[i]
if iface.Target == nil || iface.Target.Dev == "" || iface.Target.Dev == "tap0" {
continue // 无目标或tap0保留 → 跳过
}
if !ordinalPattern.MatchString(iface.Target.Dev) {
continue // 已经是hash命名 → 跳过
}
// 将序数命名(tap1)升级为hash命名(tap{kubevirt-...})
netName, _ := networkNameFromAlias(iface.Alias) // 从alias提取网络名
hashedPodIfaceName, _ := hashedPodNamingScheme[netName] // 查hash命名方案
iface.Target.Dev = tapNameFromPodIfaceName(hashedPodIfaceName) // 转换为tap+hash
}
}
背景:KubeVirt早期使用序数命名(tap0, tap1, ...),后来改为hash命名以避免网络接口顺序依赖。此Hook确保迁移时能正确处理从旧版本源节点迁移过来的域(仍使用序数命名)。
4.4.4 vgpuhook/vgpu_hook.go --- vGPU迁移Hook
go
func VGPULiveMigration(c *convertertypes.ConverterContext, vmi *v1.VirtualMachineInstance, domain *libvirtxml.Domain) error {
gpuDevs := c.GPUHostDevices
// 限制:只支持一个vGPU的迁移
if len(gpuDevs) == 0 || len(domain.Devices.Hostdevs) == 0 { return nil }
if len(domain.Devices.Hostdevs) > 1 || len(gpuDevs) > 1 {
return fmt.Errorf("the migrating vmi should only have one vGPU")
}
// 只支持MDEV类型
if gpuDevs[0].Type != api.HostDeviceMDev {
return fmt.Errorf("unsupported gpu type for migration: %s", gpuDevs[0].Type)
}
// 将目标端的mdev UUID替换到域XML中
// 源端的vGPU UUID与目标端不同(每台主机的mdev实例UUID不同)
domain.Devices.Hostdevs[0].SubsysMDev.Source.Address.UUID = gpuDevs[0].Source.Address.UUID
}
为什么需要vGPU Hook:vGPU是MDEV设备,每个主机上的mdev实例有唯一的UUID。迁移后,源端的mdev UUID在目标端不存在,必须替换为目标端分配的mdev UUID。
Pre-migration Hook流程图
VGPULiveMigration UpgradeOrdinalNaming CPUDedicatedHook HookServer Unix Socket libvirt (目标端) VGPULiveMigration UpgradeOrdinalNaming CPUDedicatedHook HookServer Unix Socket libvirt (目标端) #mermaid-svg-3MzN6uVZcbXow2CF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3MzN6uVZcbXow2CF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3MzN6uVZcbXow2CF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3MzN6uVZcbXow2CF .error-icon{fill:#552222;}#mermaid-svg-3MzN6uVZcbXow2CF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3MzN6uVZcbXow2CF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3MzN6uVZcbXow2CF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3MzN6uVZcbXow2CF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3MzN6uVZcbXow2CF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3MzN6uVZcbXow2CF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3MzN6uVZcbXow2CF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3MzN6uVZcbXow2CF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3MzN6uVZcbXow2CF .marker.cross{stroke:#333333;}#mermaid-svg-3MzN6uVZcbXow2CF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3MzN6uVZcbXow2CF p{margin:0;}#mermaid-svg-3MzN6uVZcbXow2CF .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3MzN6uVZcbXow2CF text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-3MzN6uVZcbXow2CF .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-3MzN6uVZcbXow2CF .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-3MzN6uVZcbXow2CF .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-3MzN6uVZcbXow2CF .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-3MzN6uVZcbXow2CF #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-3MzN6uVZcbXow2CF .sequenceNumber{fill:white;}#mermaid-svg-3MzN6uVZcbXow2CF #sequencenumber{fill:#333;}#mermaid-svg-3MzN6uVZcbXow2CF #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-3MzN6uVZcbXow2CF .messageText{fill:#333;stroke:none;}#mermaid-svg-3MzN6uVZcbXow2CF .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3MzN6uVZcbXow2CF .labelText,#mermaid-svg-3MzN6uVZcbXow2CF .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-3MzN6uVZcbXow2CF .loopText,#mermaid-svg-3MzN6uVZcbXow2CF .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-3MzN6uVZcbXow2CF .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-3MzN6uVZcbXow2CF .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-3MzN6uVZcbXow2CF .noteText,#mermaid-svg-3MzN6uVZcbXow2CF .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-3MzN6uVZcbXow2CF .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3MzN6uVZcbXow2CF .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3MzN6uVZcbXow2CF .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3MzN6uVZcbXow2CF .actorPopupMenu{position:absolute;}#mermaid-svg-3MzN6uVZcbXow2CF .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-3MzN6uVZcbXow2CF .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3MzN6uVZcbXow2CF .actor-man circle,#mermaid-svg-3MzN6uVZcbXow2CF line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-3MzN6uVZcbXow2CF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发送域XML (迁移前) Accept连接 解码XML → libvirtxml.Domain CPUDedicatedHook(ctx, vmi, domain) 重新计算CPU绑核 修改后domain UpgradeOrdinalNaming(ctx, vmi, domain) tap1 → tap{hash} 修改后domain VGPULiveMigration(ctx, vmi, domain) 替换mdev UUID 修改后domain 编码修改后XML 返回修改后域XML 使用修改后XML创建目标域
五、核心交互流程图
5.1 网络配置完整流程
#mermaid-svg-zzW5a7ET3jMTsQPb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zzW5a7ET3jMTsQPb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zzW5a7ET3jMTsQPb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zzW5a7ET3jMTsQPb .error-icon{fill:#552222;}#mermaid-svg-zzW5a7ET3jMTsQPb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zzW5a7ET3jMTsQPb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zzW5a7ET3jMTsQPb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zzW5a7ET3jMTsQPb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zzW5a7ET3jMTsQPb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zzW5a7ET3jMTsQPb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zzW5a7ET3jMTsQPb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zzW5a7ET3jMTsQPb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zzW5a7ET3jMTsQPb .marker.cross{stroke:#333333;}#mermaid-svg-zzW5a7ET3jMTsQPb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zzW5a7ET3jMTsQPb p{margin:0;}#mermaid-svg-zzW5a7ET3jMTsQPb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zzW5a7ET3jMTsQPb .cluster-label text{fill:#333;}#mermaid-svg-zzW5a7ET3jMTsQPb .cluster-label span{color:#333;}#mermaid-svg-zzW5a7ET3jMTsQPb .cluster-label span p{background-color:transparent;}#mermaid-svg-zzW5a7ET3jMTsQPb .label text,#mermaid-svg-zzW5a7ET3jMTsQPb span{fill:#333;color:#333;}#mermaid-svg-zzW5a7ET3jMTsQPb .node rect,#mermaid-svg-zzW5a7ET3jMTsQPb .node circle,#mermaid-svg-zzW5a7ET3jMTsQPb .node ellipse,#mermaid-svg-zzW5a7ET3jMTsQPb .node polygon,#mermaid-svg-zzW5a7ET3jMTsQPb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zzW5a7ET3jMTsQPb .rough-node .label text,#mermaid-svg-zzW5a7ET3jMTsQPb .node .label text,#mermaid-svg-zzW5a7ET3jMTsQPb .image-shape .label,#mermaid-svg-zzW5a7ET3jMTsQPb .icon-shape .label{text-anchor:middle;}#mermaid-svg-zzW5a7ET3jMTsQPb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zzW5a7ET3jMTsQPb .rough-node .label,#mermaid-svg-zzW5a7ET3jMTsQPb .node .label,#mermaid-svg-zzW5a7ET3jMTsQPb .image-shape .label,#mermaid-svg-zzW5a7ET3jMTsQPb .icon-shape .label{text-align:center;}#mermaid-svg-zzW5a7ET3jMTsQPb .node.clickable{cursor:pointer;}#mermaid-svg-zzW5a7ET3jMTsQPb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zzW5a7ET3jMTsQPb .arrowheadPath{fill:#333333;}#mermaid-svg-zzW5a7ET3jMTsQPb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zzW5a7ET3jMTsQPb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zzW5a7ET3jMTsQPb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zzW5a7ET3jMTsQPb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zzW5a7ET3jMTsQPb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zzW5a7ET3jMTsQPb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zzW5a7ET3jMTsQPb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zzW5a7ET3jMTsQPb .cluster text{fill:#333;}#mermaid-svg-zzW5a7ET3jMTsQPb .cluster span{color:#333;}#mermaid-svg-zzW5a7ET3jMTsQPb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zzW5a7ET3jMTsQPb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zzW5a7ET3jMTsQPb rect.text{fill:none;stroke-width:0;}#mermaid-svg-zzW5a7ET3jMTsQPb .icon-shape,#mermaid-svg-zzW5a7ET3jMTsQPb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zzW5a7ET3jMTsQPb .icon-shape p,#mermaid-svg-zzW5a7ET3jMTsQPb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zzW5a7ET3jMTsQPb .icon-shape .label rect,#mermaid-svg-zzW5a7ET3jMTsQPb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zzW5a7ET3jMTsQPb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zzW5a7ET3jMTsQPb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zzW5a7ET3jMTsQPb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
bridge/tap
passt
SR-IOV
自定义Binding
VMI启动/更新
首次创建?
converter/network/configurator
Configure: 生成所有接口
network/manager Sync
热插拔同步
绑定类型
type=ethernet
Tap设备直连
type=vhostuser
passt socket+端口转发
hostdev type=pci
SR-IOV VF直通
跳过
外部控制器处理
builder.go 构造XML
sriov/hostdev.go 构造XML
hotplugVirtioInterface
新接口热插
hotUnplugVirtioInterface
Absent接口热拔
updateDomainLinkState
链路状态更新
5.2 存储备份与CBT完整流程
#mermaid-svg-0pOfMN59mJkk68t6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0pOfMN59mJkk68t6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0pOfMN59mJkk68t6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0pOfMN59mJkk68t6 .error-icon{fill:#552222;}#mermaid-svg-0pOfMN59mJkk68t6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0pOfMN59mJkk68t6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0pOfMN59mJkk68t6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0pOfMN59mJkk68t6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0pOfMN59mJkk68t6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0pOfMN59mJkk68t6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0pOfMN59mJkk68t6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0pOfMN59mJkk68t6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0pOfMN59mJkk68t6 .marker.cross{stroke:#333333;}#mermaid-svg-0pOfMN59mJkk68t6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0pOfMN59mJkk68t6 p{margin:0;}#mermaid-svg-0pOfMN59mJkk68t6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0pOfMN59mJkk68t6 .cluster-label text{fill:#333;}#mermaid-svg-0pOfMN59mJkk68t6 .cluster-label span{color:#333;}#mermaid-svg-0pOfMN59mJkk68t6 .cluster-label span p{background-color:transparent;}#mermaid-svg-0pOfMN59mJkk68t6 .label text,#mermaid-svg-0pOfMN59mJkk68t6 span{fill:#333;color:#333;}#mermaid-svg-0pOfMN59mJkk68t6 .node rect,#mermaid-svg-0pOfMN59mJkk68t6 .node circle,#mermaid-svg-0pOfMN59mJkk68t6 .node ellipse,#mermaid-svg-0pOfMN59mJkk68t6 .node polygon,#mermaid-svg-0pOfMN59mJkk68t6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0pOfMN59mJkk68t6 .rough-node .label text,#mermaid-svg-0pOfMN59mJkk68t6 .node .label text,#mermaid-svg-0pOfMN59mJkk68t6 .image-shape .label,#mermaid-svg-0pOfMN59mJkk68t6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-0pOfMN59mJkk68t6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0pOfMN59mJkk68t6 .rough-node .label,#mermaid-svg-0pOfMN59mJkk68t6 .node .label,#mermaid-svg-0pOfMN59mJkk68t6 .image-shape .label,#mermaid-svg-0pOfMN59mJkk68t6 .icon-shape .label{text-align:center;}#mermaid-svg-0pOfMN59mJkk68t6 .node.clickable{cursor:pointer;}#mermaid-svg-0pOfMN59mJkk68t6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0pOfMN59mJkk68t6 .arrowheadPath{fill:#333333;}#mermaid-svg-0pOfMN59mJkk68t6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0pOfMN59mJkk68t6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0pOfMN59mJkk68t6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0pOfMN59mJkk68t6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0pOfMN59mJkk68t6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0pOfMN59mJkk68t6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0pOfMN59mJkk68t6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0pOfMN59mJkk68t6 .cluster text{fill:#333;}#mermaid-svg-0pOfMN59mJkk68t6 .cluster span{color:#333;}#mermaid-svg-0pOfMN59mJkk68t6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0pOfMN59mJkk68t6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0pOfMN59mJkk68t6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-0pOfMN59mJkk68t6 .icon-shape,#mermaid-svg-0pOfMN59mJkk68t6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0pOfMN59mJkk68t6 .icon-shape p,#mermaid-svg-0pOfMN59mJkk68t6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0pOfMN59mJkk68t6 .icon-shape .label rect,#mermaid-svg-0pOfMN59mJkk68t6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0pOfMN59mJkk68t6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0pOfMN59mJkk68t6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0pOfMN59mJkk68t6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Push
Pull
是
否
启用CBT
ApplyChangedBlockTracking
创建qcow2 overlay
overlay: data-file-raw=true
指向底层PVC
converter修改域XML
disk.Source.DataStore=overlay
触发备份
Push/Pull?
BackupBegin
写入本地.qcow2
BackupBegin
NBD socket导出
backup_tunnel
HTTP/2 CONNECT到远端
gRPC NBD Server
转发socket数据
远端备份服务拉取数据
增量备份
Incremental=上一检查点名
只导出bitmap标记的变更块
VM重启
RedefineCheckpoint
恢复libvirt checkpoint元数据
bitmap有效?
继续增量备份
checkpointInvalid
需要全量备份
5.3 设备热插拔完整流程
#mermaid-svg-QVoQkvegTaARrn2l{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QVoQkvegTaARrn2l .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QVoQkvegTaARrn2l .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QVoQkvegTaARrn2l .error-icon{fill:#552222;}#mermaid-svg-QVoQkvegTaARrn2l .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QVoQkvegTaARrn2l .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QVoQkvegTaARrn2l .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QVoQkvegTaARrn2l .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QVoQkvegTaARrn2l .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QVoQkvegTaARrn2l .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QVoQkvegTaARrn2l .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QVoQkvegTaARrn2l .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QVoQkvegTaARrn2l .marker.cross{stroke:#333333;}#mermaid-svg-QVoQkvegTaARrn2l svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QVoQkvegTaARrn2l p{margin:0;}#mermaid-svg-QVoQkvegTaARrn2l .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QVoQkvegTaARrn2l .cluster-label text{fill:#333;}#mermaid-svg-QVoQkvegTaARrn2l .cluster-label span{color:#333;}#mermaid-svg-QVoQkvegTaARrn2l .cluster-label span p{background-color:transparent;}#mermaid-svg-QVoQkvegTaARrn2l .label text,#mermaid-svg-QVoQkvegTaARrn2l span{fill:#333;color:#333;}#mermaid-svg-QVoQkvegTaARrn2l .node rect,#mermaid-svg-QVoQkvegTaARrn2l .node circle,#mermaid-svg-QVoQkvegTaARrn2l .node ellipse,#mermaid-svg-QVoQkvegTaARrn2l .node polygon,#mermaid-svg-QVoQkvegTaARrn2l .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QVoQkvegTaARrn2l .rough-node .label text,#mermaid-svg-QVoQkvegTaARrn2l .node .label text,#mermaid-svg-QVoQkvegTaARrn2l .image-shape .label,#mermaid-svg-QVoQkvegTaARrn2l .icon-shape .label{text-anchor:middle;}#mermaid-svg-QVoQkvegTaARrn2l .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QVoQkvegTaARrn2l .rough-node .label,#mermaid-svg-QVoQkvegTaARrn2l .node .label,#mermaid-svg-QVoQkvegTaARrn2l .image-shape .label,#mermaid-svg-QVoQkvegTaARrn2l .icon-shape .label{text-align:center;}#mermaid-svg-QVoQkvegTaARrn2l .node.clickable{cursor:pointer;}#mermaid-svg-QVoQkvegTaARrn2l .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QVoQkvegTaARrn2l .arrowheadPath{fill:#333333;}#mermaid-svg-QVoQkvegTaARrn2l .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QVoQkvegTaARrn2l .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QVoQkvegTaARrn2l .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QVoQkvegTaARrn2l .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QVoQkvegTaARrn2l .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QVoQkvegTaARrn2l .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QVoQkvegTaARrn2l .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QVoQkvegTaARrn2l .cluster text{fill:#333;}#mermaid-svg-QVoQkvegTaARrn2l .cluster span{color:#333;}#mermaid-svg-QVoQkvegTaARrn2l div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QVoQkvegTaARrn2l .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QVoQkvegTaARrn2l rect.text{fill:none;stroke-width:0;}#mermaid-svg-QVoQkvegTaARrn2l .icon-shape,#mermaid-svg-QVoQkvegTaARrn2l .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QVoQkvegTaARrn2l .icon-shape p,#mermaid-svg-QVoQkvegTaARrn2l .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QVoQkvegTaARrn2l .icon-shape .label rect,#mermaid-svg-QVoQkvegTaARrn2l .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QVoQkvegTaARrn2l .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QVoQkvegTaARrn2l .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QVoQkvegTaARrn2l :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Device Plugin
DRA
SR-IOV
冷启动
热插
是
否
设备分配请求
分配方式
环境变量→AddressPool
ResourceClaim→drautil查询
downward API→PCIAddressPool
Pop分配地址
GetPCI/GetMDev获取
Pop分配PCI地址
createHostDevices
构造HostDevice XML
运行时热插?
直接写入域XML
AttachHostDevices
AttachDeviceFlags
需要热拔?
SafelyDetachHostDevices
DetachDeviceFlags + 等待事件
完成
5.4 安全启动与EFI选择流程
渲染错误: Mermaid 渲染失败: Parse error on line 11: ...RS
policy=NoDebug|EncryptedState?] -----------------------^ Expecting 'SQE', 'TAGEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PIPE'
5.5 Pre-migration Hook集成流程
#mermaid-svg-lPa5u3RMP2VX4PG3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lPa5u3RMP2VX4PG3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lPa5u3RMP2VX4PG3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lPa5u3RMP2VX4PG3 .error-icon{fill:#552222;}#mermaid-svg-lPa5u3RMP2VX4PG3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lPa5u3RMP2VX4PG3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lPa5u3RMP2VX4PG3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lPa5u3RMP2VX4PG3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lPa5u3RMP2VX4PG3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lPa5u3RMP2VX4PG3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lPa5u3RMP2VX4PG3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lPa5u3RMP2VX4PG3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lPa5u3RMP2VX4PG3 .marker.cross{stroke:#333333;}#mermaid-svg-lPa5u3RMP2VX4PG3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lPa5u3RMP2VX4PG3 p{margin:0;}#mermaid-svg-lPa5u3RMP2VX4PG3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lPa5u3RMP2VX4PG3 .cluster-label text{fill:#333;}#mermaid-svg-lPa5u3RMP2VX4PG3 .cluster-label span{color:#333;}#mermaid-svg-lPa5u3RMP2VX4PG3 .cluster-label span p{background-color:transparent;}#mermaid-svg-lPa5u3RMP2VX4PG3 .label text,#mermaid-svg-lPa5u3RMP2VX4PG3 span{fill:#333;color:#333;}#mermaid-svg-lPa5u3RMP2VX4PG3 .node rect,#mermaid-svg-lPa5u3RMP2VX4PG3 .node circle,#mermaid-svg-lPa5u3RMP2VX4PG3 .node ellipse,#mermaid-svg-lPa5u3RMP2VX4PG3 .node polygon,#mermaid-svg-lPa5u3RMP2VX4PG3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lPa5u3RMP2VX4PG3 .rough-node .label text,#mermaid-svg-lPa5u3RMP2VX4PG3 .node .label text,#mermaid-svg-lPa5u3RMP2VX4PG3 .image-shape .label,#mermaid-svg-lPa5u3RMP2VX4PG3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-lPa5u3RMP2VX4PG3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lPa5u3RMP2VX4PG3 .rough-node .label,#mermaid-svg-lPa5u3RMP2VX4PG3 .node .label,#mermaid-svg-lPa5u3RMP2VX4PG3 .image-shape .label,#mermaid-svg-lPa5u3RMP2VX4PG3 .icon-shape .label{text-align:center;}#mermaid-svg-lPa5u3RMP2VX4PG3 .node.clickable{cursor:pointer;}#mermaid-svg-lPa5u3RMP2VX4PG3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lPa5u3RMP2VX4PG3 .arrowheadPath{fill:#333333;}#mermaid-svg-lPa5u3RMP2VX4PG3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lPa5u3RMP2VX4PG3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lPa5u3RMP2VX4PG3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lPa5u3RMP2VX4PG3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lPa5u3RMP2VX4PG3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lPa5u3RMP2VX4PG3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lPa5u3RMP2VX4PG3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lPa5u3RMP2VX4PG3 .cluster text{fill:#333;}#mermaid-svg-lPa5u3RMP2VX4PG3 .cluster span{color:#333;}#mermaid-svg-lPa5u3RMP2VX4PG3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lPa5u3RMP2VX4PG3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lPa5u3RMP2VX4PG3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-lPa5u3RMP2VX4PG3 .icon-shape,#mermaid-svg-lPa5u3RMP2VX4PG3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lPa5u3RMP2VX4PG3 .icon-shape p,#mermaid-svg-lPa5u3RMP2VX4PG3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lPa5u3RMP2VX4PG3 .icon-shape .label rect,#mermaid-svg-lPa5u3RMP2VX4PG3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lPa5u3RMP2VX4PG3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lPa5u3RMP2VX4PG3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lPa5u3RMP2VX4PG3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 迁移开始
目标端: Start HookServer
注册3个Hook
监听 /var/run/kubevirt/migration-hook-socket
libvirt迁移到目标端
调用Hook: 发送域XML
CPUDedicatedHook
重新绑核
UpgradeOrdinalNaming
网络命名升级
VGPULiveMigration
替换mdev UUID
返回修改后XML
libvirt用修改后XML创建目标域
迁移完成
HookServer Done
清理socket
总结
模块间依赖关系图
#mermaid-svg-nqVPLWHHshTRmKgd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nqVPLWHHshTRmKgd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nqVPLWHHshTRmKgd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nqVPLWHHshTRmKgd .error-icon{fill:#552222;}#mermaid-svg-nqVPLWHHshTRmKgd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nqVPLWHHshTRmKgd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nqVPLWHHshTRmKgd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nqVPLWHHshTRmKgd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nqVPLWHHshTRmKgd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nqVPLWHHshTRmKgd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nqVPLWHHshTRmKgd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nqVPLWHHshTRmKgd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nqVPLWHHshTRmKgd .marker.cross{stroke:#333333;}#mermaid-svg-nqVPLWHHshTRmKgd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nqVPLWHHshTRmKgd p{margin:0;}#mermaid-svg-nqVPLWHHshTRmKgd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nqVPLWHHshTRmKgd .cluster-label text{fill:#333;}#mermaid-svg-nqVPLWHHshTRmKgd .cluster-label span{color:#333;}#mermaid-svg-nqVPLWHHshTRmKgd .cluster-label span p{background-color:transparent;}#mermaid-svg-nqVPLWHHshTRmKgd .label text,#mermaid-svg-nqVPLWHHshTRmKgd span{fill:#333;color:#333;}#mermaid-svg-nqVPLWHHshTRmKgd .node rect,#mermaid-svg-nqVPLWHHshTRmKgd .node circle,#mermaid-svg-nqVPLWHHshTRmKgd .node ellipse,#mermaid-svg-nqVPLWHHshTRmKgd .node polygon,#mermaid-svg-nqVPLWHHshTRmKgd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nqVPLWHHshTRmKgd .rough-node .label text,#mermaid-svg-nqVPLWHHshTRmKgd .node .label text,#mermaid-svg-nqVPLWHHshTRmKgd .image-shape .label,#mermaid-svg-nqVPLWHHshTRmKgd .icon-shape .label{text-anchor:middle;}#mermaid-svg-nqVPLWHHshTRmKgd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nqVPLWHHshTRmKgd .rough-node .label,#mermaid-svg-nqVPLWHHshTRmKgd .node .label,#mermaid-svg-nqVPLWHHshTRmKgd .image-shape .label,#mermaid-svg-nqVPLWHHshTRmKgd .icon-shape .label{text-align:center;}#mermaid-svg-nqVPLWHHshTRmKgd .node.clickable{cursor:pointer;}#mermaid-svg-nqVPLWHHshTRmKgd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nqVPLWHHshTRmKgd .arrowheadPath{fill:#333333;}#mermaid-svg-nqVPLWHHshTRmKgd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nqVPLWHHshTRmKgd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nqVPLWHHshTRmKgd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nqVPLWHHshTRmKgd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nqVPLWHHshTRmKgd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nqVPLWHHshTRmKgd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nqVPLWHHshTRmKgd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nqVPLWHHshTRmKgd .cluster text{fill:#333;}#mermaid-svg-nqVPLWHHshTRmKgd .cluster span{color:#333;}#mermaid-svg-nqVPLWHHshTRmKgd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nqVPLWHHshTRmKgd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nqVPLWHHshTRmKgd rect.text{fill:none;stroke-width:0;}#mermaid-svg-nqVPLWHHshTRmKgd .icon-shape,#mermaid-svg-nqVPLWHHshTRmKgd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nqVPLWHHshTRmKgd .icon-shape p,#mermaid-svg-nqVPLWHHshTRmKgd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nqVPLWHHshTRmKgd .icon-shape .label rect,#mermaid-svg-nqVPLWHHshTRmKgd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nqVPLWHHshTRmKgd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nqVPLWHHshTRmKgd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nqVPLWHHshTRmKgd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 外设层
设备层
网络层
SEV影响ROM
存储层
overlay结构
Pull模式
manager
backup
fsfreeze
memoryDump
backup_tunnel
cbt
disk_source
virtiofs
configurator
builder
passt
virtio-queues
manager
nichotplug
hostdev
addresspool
generic
gpu
sriov
dra
device/pciaddress
pcipool_netstatus
hotplug
efi
sev
cpudedicated
hook_server
cpu_hook
ordinal_naming
vgpu_hook
关键设计模式总结
- Functional Options:builder.go、configurator.go 使用函数选项模式,灵活组合接口属性
- Strategy Pattern:createHostDev 函数类型 + AddressPooler 接口,支持PCI/MDEV/USB三种策略
- BestEffort包装:BestEffortAddressPool 包装 AddressPooler,允许设备在多个池中尝试
- Hook Chain:PreMigrationHookServer 支持多个Hook函数串联执行
- 安全解冻:fsfreeze 使用超时定时器 + cancel通道,防止忘记解冻
- 占位接口:WithNetworkIfacesResources 通过临时添加接口触发PCI控制器分配
- 幂等性:备份/内存转储/FSFreeze 都有幂等检查,重复调用不会出错
- DRA vs Device Plugin 双轨:同一设备类型支持两种分配方式,通过drautil判断走哪条路径