【kubevirt】(virt-launcher Part 6)virt-launcher 设备/网络/存储/外设层

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):

  1. VMI接口状态有 InfoSourceMultusStatus 标记(multus已分配网络)
  2. 域XML中不存在该接口的alias
  3. VMI规格中接口State不是Absent
  4. 接口不是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 工作原理

  1. 宿主机侧运行 virtiofsd 守护进程,监听 Unix socket
  2. libvirt 域 XML 中 <filesystem> 元素的 source socket 指向该 socket
  3. 虚拟机内通过 mount -t virtiofs {name} /mnt/xxx 挂载
  4. 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-file
  • file: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类似但有两个关键差异:

  1. 默认启用显示DefaultDisplayOn=true)--- vGPU通常需要显示输出
  2. 支持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
}

协议流程

  1. libvirt 迁移到目标端时,通过 Unix socket 发送域 XML
  2. Hook服务器解码XML,执行所有注册的Hook函数
  3. 修改后的XML编码回连接
  4. 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

关键设计模式总结

  1. Functional Options:builder.go、configurator.go 使用函数选项模式,灵活组合接口属性
  2. Strategy Pattern:createHostDev 函数类型 + AddressPooler 接口,支持PCI/MDEV/USB三种策略
  3. BestEffort包装:BestEffortAddressPool 包装 AddressPooler,允许设备在多个池中尝试
  4. Hook Chain:PreMigrationHookServer 支持多个Hook函数串联执行
  5. 安全解冻:fsfreeze 使用超时定时器 + cancel通道,防止忘记解冻
  6. 占位接口:WithNetworkIfacesResources 通过临时添加接口触发PCI控制器分配
  7. 幂等性:备份/内存转储/FSFreeze 都有幂等检查,重复调用不会出错
  8. DRA vs Device Plugin 双轨:同一设备类型支持两种分配方式,通过drautil判断走哪条路径
相关推荐
szxinmai主板定制专家2 小时前
基于 ARM+FPGA精密多轴实时运动控制卡设计方案,适用于半导体设备等高精度领域(一)
arm开发·人工智能·嵌入式硬件·fpga开发·架构·语音识别
达达尼昂3 小时前
AI Native 工程实践 : agent 自动化测试
前端·后端·架构
qq_356408663 小时前
Kubernetes Loki 日志收集系统部署文档 (读写分离模式 + Ceph S3 + Nginx 日志分离)
ceph·nginx·kubernetes
誰能久伴不乏3 小时前
工业级 Modbus 上位机架构:基于滴答引擎与状态锁的高并发调度器
c++·qt·架构
小小王app小程序开发3 小时前
陪诊小程序开发功能深度分析:功能架构、业务逻辑与落地要点
大数据·架构
SuniaWang4 小时前
《AgentX 专栏》08-工作流引擎:AgentWorkflow怎么把工具记忆流程串成一条流水线
java·ai·架构·langchain·工作流引擎·langgraph·agent架构
金融RPA机器人丨实在智能4 小时前
选择Agent平台如何避免“厂商锁定”?深度解析企业级AI智能体架构解耦与落地实践
人工智能·ai·架构
Soari5 小时前
GitHub 开源项目解析:revfactory/harness —— Claude Code 的多智能体团队架构工厂
架构·开源·多智能体协作·claude code·软件工程自动化
jiayong236 小时前
harness 与 hermes-agent 扩展性、安全与运维
运维·人工智能·安全·ai·架构·智能体·harness