【kubevirt】(virt-handler Part 3)KubeVirt virt-handler Device Manager 超深度分析

KubeVirt virt-handler Device Manager 超深度分析

基于源码级深度剖析,覆盖 Device Manager 全部核心文件


一、模块定位

1.1 业务职责

KubeVirt Device Manager 是 virt-handler 中负责主机设备发现与 Kubernetes Device Plugin 暴露的核心子系统。其核心使命是:

将宿主机上的物理设备(GPU、PCI设备、vGPU中介设备、KVM、VFIO、USB等)转化为 Kubernetes 可调度、可分配的 Extended Resource,使虚拟机工作负载能够通过标准的 K8s 资源请求机制获取所需的硬件设备。

具体而言,Device Manager 解决了以下关键问题:

问题解决方案
K8s 不感知宿主机硬件设备实现 Kubernetes Device Plugin API,将设备注册为 Extended Resource
虚拟机需要 GPU/PCI 直通发现 VFIO 设备组,通过 Device Plugin 暴露 /dev/vfio/*
vGPU 场景需要中介设备自动发现 mdev 类型、创建/销毁 mdev 实例、注册 mdev 资源
设备动态变化(热插拔)fsnotify 监控设备路径,实时更新设备健康状态
配置驱动的设备策略监听 KubeVirt ConfigMap,动态启用/禁用设备 Plugin
安全与权限SELinux relabel、cgroup misc capacity、socket 权限管理

1.2 在 KubeVirt 架构中的位置

┌─────────────────────────────────────────────────────────────────────┐
│                         KubeVirt 控制面                              │
│  ┌───────────────┐  ┌────────────────┐  ┌──────────────────────┐   │
│  │ virt-api      │  │ virt-controller│  │ kubevirt-config CM   │   │
│  └───────────────┘  └────────────────┘  └──────────────────────┘   │
└───────────────────────────┬─────────────────────────────────────────┘
                            │ ConfigMap Watch / Node Informer
┌───────────────────────────▼─────────────────────────────────────────┐
│                      virt-handler Pod (DaemonSet)                    │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    Device Manager                             │   │
│  │  ┌──────────────┐  ┌──────────────────────────────────┐     │   │
│  │  │DeviceController│──│ Generic/Mediated/PCI/Socket/USB  │     │   │
│  │  │  (编排层)     │  │      (各设备类型 Plugin)          │     │   │
│  │  └──────────────┘  └──────────────────────────────────┘     │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                            │ gRPC (Unix Socket)                    │
│  ┌─────────────────────────▼───────────────────────────────────┐   │
│  │                     Kubelet Device Plugin Manager            │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘
                            │ Extended Resource
                    ┌───────▼───────┐
                    │  Pod 调度/分配  │
                    └───────────────┘

1.3 核心交互对象

  • Kubelet Device Plugin Manager:通过 gRPC over Unix Socket 注册设备、报告状态、响应分配请求
  • KubeVirt ClusterConfig:监听 kubevirt-config ConfigMap,获取允许的设备列表、Feature Gate 开关
  • Node Informer:获取当前 Node 对象,用于读取 mdev 资源注解
  • 宿主机 sysfs/设备文件系统/sys/bus/pci/sys/bus/mdev/dev/vfio/dev/kvm
  • virt-chroot:创建/删除 mdev 实例时通过 chroot 执行系统命令

二、模块整体结构

2.1 DeviceController 结构体全字段详解

type DeviceController struct {
    permanentPlugins         map[string]Device        // 永久性设备插件(KVM/tun/vhost-net等)
    startedPlugins           map[string]controlledDevice // 已启动的设备插件(包含永久+动态)
    startedPluginsMutex      sync.Mutex               // 保护 startedPlugins 的互斥锁
    host                     string                   // 当前节点主机名
    maxDevices               int                      // 每种设备的最大实例数
    permissions              string                   // 设备权限(如 "rw"、"mrw")
    backoff                  []time.Duration          // 重试退避时间序列
    virtConfig               *virtconfig.ClusterConfig // 集群配置(ConfigMap Watcher)
    mdevTypesManager         *MDEVTypesManager         // mdev 类型管理器
    nodeStore                cache.Store               // Node Informer Store
    mdevRefreshWG            *sync.WaitGroup           // mdev 刷新的并发控制 WaitGroup
    lastTDXAttestationConfig *tdxConfigState           // TDX 远程认证配置状态缓存
}

各字段深度解读:

  1. permanentPlugins:存储节点上必须始终存在的设备插件。由 PermanentHostDevicePlugins() 函数创建,默认包含:

    • hypervisorDevice(通常为 kvm)→ /dev/kvm
    • tun/dev/net/tun
    • vhost-net/dev/vhost-net
      这些设备是虚拟化运行的基础,不可动态禁用。
  2. startedPlugins:所有正在运行的设备插件实例,键为 resourceName,值为 controlledDevice。包含永久插件和动态发现/配置驱动的插件(PCI、mdev、USB、TDX 等)。由 startedPluginsMutex 保护。

  3. startedPluginsMutex:关键并发保护。refreshPermittedDevices() 可被多个 informer 回调或 ConfigMap 快速连续更新触发,此锁防止同一设备被重复 start/stop。

  4. host:用于从 nodeStore 中查找当前 Node 对象的关键字。

  5. maxDevices:传递给每个 Generic/Socket Device Plugin 的最大设备数,决定 K8s 资源容量。默认值为配置中传入的数值(通常是 1000 或类似值)。

  6. permissions:设备文件权限字符串,如 "rw""mrw"(m= mmap, r= read, w= write),传递给 DeviceSpec.Permissions。

  7. backoffcontrolledDevice.Start() 失败时的退避序列,默认 [1s, 2s, 5s, 10s]。最大退避为最后一个元素。

  8. virtConfig:ClusterConfig 实例,提供:

    • GetPermittedHostDevices() — 获取允许的 PCI/mdev/USB 设备列表
    • WorkloadEncryptionTDXEnabled() — TDX Feature Gate
    • WorkloadEncryptionSEVEnabled() — SEV Feature Gate
    • VSOCKEnabled() — VSOCK Feature Gate
    • GetDesiredMDEVTypes() — 从 Node 注解获取期望的 mdev 类型
    • SetConfigModifiedCallback() — 注册 ConfigMap 变更回调
  9. mdevTypesManager:管理 mdev 类型的生命周期——创建、删除、发现可配置的 mdev 类型。

  10. nodeStore:Kubernetes Node Informer 的本地缓存,用于读取 Node 对象的注解(如 mdev 期望类型列表)。

  11. mdevRefreshWG:确保 mdev 刷新操作在 DeviceController 退出前完成,避免资源泄露。

  12. lastTDXAttestationConfig:缓存上一次 TDX 配置(QGS socket 路径 + 是否需要 QGS),用于检测配置变更并重启 TDX Device Plugin。

2.2 controlledDevice 结构体

type controlledDevice struct {
    devicePlugin Device            // 被控制的设备插件实例
    started      bool              // 是否已启动
    stopChan     chan struct{}     // 停止信号通道
    backoff      []time.Duration   // 重试退避序列
}

controlledDevice 是 DeviceController 对每个 Device Plugin 的管理封装。核心行为:

  • Start():如果未启动,创建 stop channel,启动一个 goroutine 循环调用 dev.Start(stop)。失败时按 backoff 序列重试。成功后 retries 归零。
  • Stop():关闭 stop channel,触发 goroutine 退出。
  • GetName():返回设备资源名称。

重试逻辑的关键细节:

retries = int(math.Min(float64(retries+1), float64(len(backoff)-1)))

失败时 retries 递增,但不超过 len(backoff)-1(最后一个元素为最大退避)。成功则归零。

2.3 设备类型接口与实现

2.3.1 Device 接口
type Device interface {
    Start(stop <-chan struct{}) error
    ListAndWatch(*pluginapi.Empty, pluginapi.DevicePlugin_ListAndWatchServer) error
    PreStartContainer(context.Context, *pluginapi.PreStartContainerRequest) (*pluginapi.PreStartContainerResponse, error)
    Allocate(context.Context, *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error)
    GetDeviceName() string
    GetInitialized() bool
}

这是所有设备插件必须实现的核心接口,直接映射 Kubernetes Device Plugin v1beta1 API:

方法对应 gRPC作用
Start启动 gRPC Server、注册到 Kubelet
ListAndWatchListAndWatch流式推送设备列表与健康状态
AllocateAllocate响应 Kubelet 的设备分配请求
PreStartContainerPreStartContainer容器启动前准备(本模块全部返回空)
GetDeviceName返回资源名称标识
GetInitialized返回插件是否已完成初始化
2.3.2 五种设备类型实现
                    ┌─────────── Device (Interface) ───────────┐
                    │  Start / ListAndWatch / Allocate / ...   │
                    └─────────────────┬───────────────────────┘
                                      │
            ┌─────────────────────────┼──────────────────────────┐
            │                         │                          │
    ┌───────▼──────┐  ┌───────────────▼──────────┐  ┌───────────▼──────────┐
    │GenericDevice │  │  DevicePluginBase        │  │ USBDevicePlugin      │
    │   Plugin     │  │  (嵌入式公共基类)        │  │ (独立实现)           │
    └──────────────┘  └───────────┬──────────────┘  └──────────────────────┘
                                │
                    ┌───────────┼────────────────┐
                    │           │                │
           ┌────────▼───┐ ┌────▼──────────┐ ┌───▼──────────────┐
           │MediatedDevice│ │PCIDevicePlugin│ │SocketDevicePlugin│
           │  Plugin      │ │               │ │                  │
           └──────────────┘ └───────────────┘ └──────────────────┘

各实现特点对比:

特性GenericMediatedPCISocketUSB
基类独立实现嵌入DevicePluginBase嵌入DevicePluginBase嵌入DevicePluginBase嵌入DevicePluginBase
设备ID来源名字+序号IOMMU GroupIOMMU Group名字+序号资源名+随机串+序号
Allocate返回DeviceSpecDeviceSpec+EnvsDeviceSpec+EnvsMountDeviceSpec+Envs
健康检查fsnotify(设备文件)fsnotify(VFIO设备)fsnotify(VFIO设备)fsnotify(Socket文件)fsnotify(USB设备路径)
NUMA拓扑
权限管理SELinux+ChownChown
典型设备/dev/kvm, /dev/tunvGPU mdevGPU NIC SR-IOVQGS socket, PR helperUSB 直通
2.3.3 DevicePluginBase 公共基类
type DevicePluginBase struct {
    devs         []*pluginapi.Device     // 设备列表
    server       *grpc.Server            // gRPC 服务器
    socketPath   string                  // Device Plugin socket 路径
    stop         <-chan struct{}         // 停止信号
    health       chan deviceHealth       // 健康状态变更通道
    resourceName string                 // K8s 资源名(如 devices.kubevirt.io/kvm)
    done         chan struct{}           // 插件完成信号
    initialized  bool                   // 是否初始化完成
    lock         *sync.Mutex            // 保护 initialized
    deregistered chan struct{}           // 反注册完成信号
    devicePath   string                 // 宿主机设备路径
    deviceRoot   string                 // 设备根路径(通常为 /proc/1/root)
    deviceName   string                 // 设备名称
}

DevicePluginBase 提供了四个公共方法实现:

  1. ListAndWatch:监听 health channel,收到健康变更后更新对应设备的 Health 字段并推送。支持按 DevId 精确更新或全量更新(DevId == "")。退出时发送空列表触发 Kubelet 反注册。

  2. PreStartContainer / GetDevicePluginOptions:直接返回空/默认响应,KubeVirt 不需要容器预启动操作。

  3. stopDevicePlugin:等待 deregistered channel 最多1秒(给 Kubelet 时间处理空设备列表),然后停止 gRPC Server,清理 socket 文件。

  4. register:通过 gRPC 连接 Kubelet socket (/var/lib/kubelet/device-plugins/kubelet.sock),发送 RegisterRequest

  5. cleanup:删除 Device Plugin socket 文件。

2.4 核心方法清单

DeviceController 层
方法作用
NewDeviceController构造函数
Run主循环入口,启动永久插件、注册回调、阻塞等待退出
Initialized所有已启动插件是否完成初始化
NodeHasDevice检查宿主机设备路径是否存在
updatePermittedHostDevicePlugins根据配置发现并构建允许的设备插件列表
refreshPermittedDevices对比新旧设备列表,启动新增/停止移除的插件
splitPermittedDevices将设备列表拆分为待启动和待停止两个集合
startDevice / stopDevice管理单个设备的生命周期
RefreshMediatedDeviceTypes外部触发的 mdev 类型刷新
refreshMediatedDeviceTypes实际的 mdev 配置更新逻辑
refreshTDXConfig检测 TDX 配置变更
updateTdxDevice构建 TDX Device Plugin
getExternallyProvidedMdevs获取外部提供的 mdev 资源映射
MDEVTypesManager 层
方法作用
updateMDEVTypesConfiguration更新 mdev 类型配置(删除不需要的、发现可配置的、创建期望的)
discoverConfigurableMDEVTypes发现同时满足"期望"和"可配置"的 mdev 类型
configureDesiredMDEVTypes按轮询策略在可用父设备上创建 mdev 实例
getAlreadyConfiguredMdevParents获取已有 mdev 实例的父 PCI 地址
initMDEVTypesRing初始化环形缓冲区用于轮询分配
DeviceHandler 接口(硬件操作层)
方法作用
GetDeviceIOMMUGroup读取 PCI 设备的 IOMMU 组号
GetDeviceDriver读取 PCI 设备绑定的驱动
GetDeviceNumaNode读取 PCI 设备的 NUMA 节点
GetDevicePCIID读取 PCI vendor:device ID
GetMdevParentPCIAddr获取 mdev 的父 PCI 地址
CreateMDEVType创建 mdev 实例(通过 virt-chroot)
RemoveMDEVType删除 mdev 实例(通过 virt-chroot)
ReadMDEVAvailableInstances读取 mdev 类型的可用实例数
HasVGPUProfile检查 NVIDIA 设备是否有 vGPU 配置
IsPhysicalFunction检查 PCI 设备是否为 SR-IOV PF

三、核心业务逻辑深度解析

3.1 Device Manager 启动流程

virt-handler 启动
       │
       ▼
NewDeviceController(host, maxDevices, permissions, permanentPlugins, clusterConfig, nodeStore)
       │
       │ 构建永久插件: kvm, tun, vhost-net
       │ 初始化 MDEVTypesManager
       ▼
DeviceController.Run(stop)
       │
       ├─1. 启动永久插件(加锁)
       │    for name, dev := range c.permanentPlugins:
       │        c.startDevice(name, dev)
       │
       ├─2. 注册 ConfigMap 变更回调
       │    c.virtConfig.SetConfigModifiedCallback(refreshMediatedDeviceTypesFn)
       │    c.virtConfig.SetConfigModifiedCallback(c.refreshPermittedDevices)
       │
       ├─3. 首次刷新允许的设备
       │    c.refreshPermittedDevices()
       │
       └─4. 阻塞等待 stop 信号
            <-stop
            停止所有插件 + WaitGroup 等待

单个 Device Plugin 的启动细节:

controlledDevice.Start()
       │
       ▼
go func() {
    for {
        dev.Start(stop)  ←────────────────────────────┐
            │                                            │
            ├─1. cleanup() 删除旧 socket                │
            ├─2. preOpen: 打开设备文件触发 modprobe     │
            ├─3. net.Listen("unix", socketPath)         │
            ├─4. grpc.NewServer + Register              │
            ├─5. server.Serve(sock)  // goroutine       │
            ├─6. waitForGRPCServer(socket, 5s)          │
            ├─7. register() → Kubelet                   │
            ├─8. healthCheck()  // goroutine            │
            ├─9. setInitialized(true)                   │
            └─10. 等待错误 ── 失败?── 退避重试 ────────┘
                                │
                                │ 成功运行直到 stop/错误
                                ▼
                           返回 err
    }
}()

关键步骤解析:

Step 2 - preOpen:某些设备(如 tun、vhost-net)的内核模块按需加载。打开设备文件触发 modprobe,确保设备可用。GenericDevicePluginpreOpen 字段控制此行为——仅非 hypervisor 设备(tun、vhost-net)启用。

Step 7 - register:连接 Kubelet 的 Device Plugin Registry socket,发送 RegisterRequest

reqt := &pluginapi.RegisterRequest{
    Version:      "v1beta1",
    Endpoint:     "kubevirt-kvm.sock",     // socket 文件名
    ResourceName: "devices.kubevirt.io/kvm", // 资源名
}

Kubelet 收到后,开始通过该 socket 调用 ListAndWatch 获取设备列表,并将该资源注册到 Node 的 CapacityAllocatable 中。

3.2 Generic Device:通用设备发现与分配

3.2.1 结构体
type GenericDevicePlugin struct {
    devs         []*pluginapi.Device    // 设备列表(虚拟多实例)
    server       *grpc.Server
    socketPath   string                 // /var/lib/kubelet/device-plugins/kubevirt-<name>.sock
    stop         <-chan struct{}
    health       chan deviceHealth
    devicePath   string                 // 如 /dev/kvm
    deviceName   string                 // 如 "kvm"
    resourceName string                 // 如 "devices.kubevirt.io/kvm"
    done         chan struct{}
    deviceRoot   string                 // /proc/1/root (容器内宿主根)
    preOpen      bool                   // 是否在启动时打开设备文件
    initialized  bool
    lock         *sync.Mutex
    permissions  string                 // 如 "rw"
    deregistered chan struct{}
}
3.2.2 设备发现

Generic Device 的"发现"极为简单——它不扫描 sysfs,而是在构造时直接创建 maxDevices 个虚拟设备实例:

for i := 0; i < maxDevices; i++ {
    deviceId := dpi.deviceName + strconv.Itoa(i)  // "kvm0", "kvm1", ...
    dpi.devs = append(dpi.devs, &pluginapi.Device{
        ID:     deviceId,
        Health: pluginapi.Healthy,
    })
}

这反映了通用设备的本质:/dev/kvm 只有一个物理实例,但可以被多个虚拟机共享。通过创建多个虚拟 Device ID,K8s 将其视为多个独立资源,调度器可以据此限制并发 VM 数量。

3.2.3 分配逻辑
func (dpi *GenericDevicePlugin) Allocate(ctx context.Context, r *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
    response := pluginapi.AllocateResponse{}
    containerResponse := new(pluginapi.ContainerAllocateResponse)

    dev := new(pluginapi.DeviceSpec)
    dev.HostPath = dpi.devicePath       // /dev/kvm
    dev.ContainerPath = dpi.devicePath  // /dev/kvm
    dev.Permissions = dpi.permissions   // "rw"
    containerResponse.Devices = []*pluginapi.DeviceSpec{dev}

    response.ContainerResponses = []*pluginapi.ContainerAllocateResponse{containerResponse}
    return &response, nil
}

关键特点

  • 无论请求多少个 Device ID,Allocate 始终返回同一个设备路径
  • 不设置 Envs — 通用设备不需要环境变量传递
  • DeviceSpec 的 HostPath 和 ContainerPath 相同,意味着设备在容器内的路径与宿主机一致
3.2.4 健康检查

设备 Create

设备 Remove/Rename

Socket Remove

stop channel

healthCheck goroutine 启动

创建 fsnotify Watcher

监控设备目录 + Plugin Socket 目录

设备文件存在?

发送 Unhealthy

发送 Healthy

事件循环

事件类型

发送 Healthy

发送 Unhealthy

Kubelet 重启,返回 nil

退出

Generic Device 的健康检查基于 fsnotify 监控设备文件的父目录。当 /dev/kvm 被创建/删除时,触发健康状态变更,通过 health channel 传递给 ListAndWatch,由其推送给 Kubelet。

3.3 Mediated Device:GPU/vGPU 中介设备

3.3.1 MDEV 结构体
type MDEV struct {
    UUID             string    // mdev 实例 UUID
    typeName         string    // mdev 类型名(如 "nvidia-223")
    parentPciAddress string    // 父设备 PCI 地址
    iommuGroup       string    // IOMMU 组号
    numaNode         int       // NUMA 节点
}
3.3.2 MediatedDevicePlugin 结构体
type MediatedDevicePlugin struct {
    *DevicePluginBase
    iommuToMDEVMap map[string]string  // IOMMU Group → mdev UUID 映射
}

iommuToMDEVMap 是 mdev 分配的核心映射。K8s Device Plugin 使用 IOMMU Group 作为 Device ID(因为 VFIO 设备通过 IOMMU Group 访问),而 mdev UUID 是实际的中介设备标识。分配时通过此映射将 IOMMU Group 转换为 mdev UUID。

3.3.3 设备发现流程

discoverPermittedHostMediatedDevices

读取 /sys/bus/mdev/devices/

遍历每个 mdev 实体

是符号链接?

getMdevTypeName: 读取 mdev_type/name

类型在 supportedMdevsMap?

构建 MDEV 结构体

GetMdevParentPCIAddr: 解析父 PCI 地址

GetDeviceNumaNode: 读取 NUMA 信息

GetDeviceIOMMUGroup: 读取 IOMMU 组

添加到 mdevsMap

关键细节:

  • mdev 实体在 /sys/bus/mdev/devices/ 下以 UUID 命名的符号链接形式存在
  • getMdevTypeName 先尝试读 mdev_type/name 文件,若不存在则读 mdev_type 符号链接的 basename
  • 空格替换为下划线:GRID T4-1QGRID_T4-1Q,与 ConfigMap 中的 selector 格式对齐
  • NUMA 拓扑信息从父 PCI 设备的 numa_node 文件读取,附加到 Device Topology
3.3.4 分配逻辑
func (dpi *MediatedDevicePlugin) Allocate(_ context.Context, r *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
    for _, request := range r.ContainerRequests {
        for _, devID := range request.DevicesIDs {
            // IOMMU Group → mdev UUID
            mdevUUID, exist := dpi.iommuToMDEVMap[devID]
            
            // 验证设备仍然存在
            _, err := os.Stat(filepath.Join(dpi.deviceRoot, dpi.devicePath, devID))
            
            // 生成 VFIO DeviceSpec
            formattedVFIO := formatVFIODeviceSpecs(devID)
            deviceSpecs = append(deviceSpecs, formattedVFIO...)
        }
        // 设置环境变量: KUBEVIRT_MEDIALIZED_RESOURCE_<RESOURCE> = <UUID1>,<UUID2>,...
        envVar[resourceNameEnvVar] = strings.Join(allocatedDevices, ",")
    }
}

formatVFIODeviceSpecs 生成的设备规范:

// 1. /dev/vfio/vfio (VFIO 容器接口)
&pluginapi.DeviceSpec{
    HostPath:      "/dev/vfio/vfio",
    ContainerPath: "/dev/vfio/vfio",
    Permissions:   "mrw",
}
// 2. /dev/vfio/<iommuGroup> (具体设备)
&pluginapi.DeviceSpec{
    HostPath:      "/dev/vfio/<iommuGroup>",
    ContainerPath: "/dev/vfio/<iommuGroup>",
    Permissions:   "mrw",
}

环境变量格式:KUBEVIRT_MDEV_RESOURCE_DEVICES_KUBEVIRT_IO_NVIDIA_223=uuid1,uuid2

3.4 PCI Device:PCI 设备直通

3.4.1 PCIDevice 结构体
type PCIDevice struct {
    pciID      string    // vendor:device ID(如 "10de:1eb8")
    driver     string    // 绑定的驱动名(如 "vfio-pci")
    pciAddress string    // PCI 地址(如 "0000:65:00.0")
    iommuGroup string    // IOMMU 组号
    numaNode   int       // NUMA 节点
}
3.4.2 设备发现流程

discoverPermittedHostPCIDevices

filepath.Walk /sys/bus/pci/devices/

遍历每个 PCI 设备文件

GetDevicePCIID: 读取 uevent → PCI_ID

PCI ID 在 supportedPCIDeviceMap?

GetDeviceDriver: 读取 driver 链接

driver == vfio-pci?

构建 PCIDevice

shouldPermitNonVFIOPCI → shouldPermitNvidia

driver == nvidia?

跳过,不支持的驱动

IsPhysicalFunction?

跳过 PF

HasVGPUProfile?

允许,构建 PCIDevice

GetDeviceIOMMUGroup

GetDeviceNumaNode

添加到 pciDevicesMap

NVIDIA vGPU 特殊处理(nvidia.go):

PCI 设备发现有一个关键的特殊逻辑:对于非 vfio-pci 驱动的设备,原则上应跳过(因为 VFIO 直通需要 vfio-pci 驱动)。但 NVIDIA vGPU VF 是例外——它们使用 nvidia 驱动而非 vfio-pci,但仍然可以通过 mdev 机制进行 vGPU 分配。

func shouldPermitNvidia(driver, pciBasePath, pciAddress, pciID string) bool {
    if driver != "nvidia" {
        return false
    }
    if handler.IsPhysicalFunction(pciBasePath, pciAddress) {
        return false  // PF 不可分配
    }
    if !handler.HasVGPUProfile(pciBasePath, pciAddress) {
        return false  // 没有 vGPU 配置的 VF 不可分配
    }
    return true  // 有 vGPU 配置的 NVIDIA VF 允许分配
}

IsPhysicalFunction 通过检查 /sys/bus/pci/devices/<addr>/sriov_totalvfs 是否存在来判断。只有 PF 才有此文件。

HasVGPUProfile 通过读取 /sys/bus/pci/devices/<addr>/nvidia/current_vgpu_type 判断,非空且非 “0” 表示有 vGPU 配置。

3.4.3 分配逻辑

PCI 分配与 Mediated 类似,都使用 IOMMU Group 作为 Device ID,生成 VFIO DeviceSpec:

func (dpi *PCIDevicePlugin) Allocate(_ context.Context, r *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
    for _, request := range r.ContainerRequests {
        for _, devID := range request.DevicesIDs {
            devPCIAddress, exist := dpi.iommuToPCIMap[devID]  // IOMMU → PCI 地址
            allocatedDevices = append(allocatedDevices, devPCIAddress)
            deviceSpecs = append(deviceSpecs, formatVFIODeviceSpecs(devID)...)
        }
        envVar[resourceNameEnvVar] = strings.Join(allocatedDevices, ",")
    }
}

环境变量格式:KUBEVIRT_PCI_RESOURCE_DEVICES_KUBEVIRT_IO_NVIDIA_GPU=0000:65:00.0,0000:66:00.0

3.5 Socket Device:Unix Socket 设备

3.5.1 结构体
type SocketDevicePlugin struct {
    *DevicePluginBase
    socketRoot    string                // Socket 根路径(/ 或 /proc/1/root)
    socketDir     string                // Socket 目录
    socket        string                // Socket 文件名
    socketName    string                // 设备名称
    executor      selinux.Executor       // SELinux 执行器
    p             PermissionManager      // 权限管理器
    healthChecks  bool                  // 是否启用健康检查
    hostRootMount string                // 宿主根挂载路径
}
3.5.2 使用场景

Socket Device Plugin 用于需要通过 Unix Socket 通信的场景:

  1. TDX QGS (Quote Generation Service):Intel TDX 远程认证需要 VM 访问 QGS socket
  2. Persistent Reservation Helper:SCSI PR 命令需要通过 socket 代理

两种构造函数:

// 标准 Socket Device Plugin — 健康检查启用
NewSocketDevicePlugin(socketName, socketDir, socket, maxDevices, executor, p, useHostRootMount)

// 可选 Socket Device Plugin — 健康检查禁用(设备始终健康)
NewOptionalSocketDevicePlugin(socketName, socketDir, socket, maxDevices, executor, p, useHostRootMount)

TDX 场景中,RequireQGS 为 true 时用标准版(QGS 必须存在才健康),否则用可选版(QGS 不存在也健康)。

3.5.3 分配逻辑

Socket 设备的分配不使用 DeviceSpec,而是使用 Mount

func (dpi *SocketDevicePlugin) Allocate(ctx context.Context, r *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
    m := new(pluginapi.Mount)
    m.HostPath = dpi.socketDir      // 如 /var/run/kubevirt-private/tdx-qgs
    m.ContainerPath = dpi.socketDir
    m.ReadOnly = false
    containerResponse.Mounts = []*pluginapi.Mount{m}
    return &response, nil
}

为什么不直接挂载 socket 文件而挂载目录? 因为 Kubernetes Mount 不支持单个文件挂载的原子性保证。挂载整个目录确保 socket 文件在被创建/重建后仍然可访问。

3.5.4 权限管理

Socket Device 有独特的权限管理需求:

func (dpi *SocketDevicePlugin) setSocketPermissions() error {
    // 1. 解析安全路径
    prSock, err := safepath.JoinAndResolveWithRelativeRoot(dpi.socketRoot, dpi.socketDir, dpi.socket)
    // 2. Chown 为 NonRoot UID
    err = dpi.p.ChownAtNoFollow(prSock, util.NonRootUID, util.NonRootUID)
    // 3. SELinux relabel
    if se, exists, err := dpi.executor.NewSELinux(); err == nil && exists {
        selinux.RelabelFilesUnprivileged(se.IsPermissive(), prSock)
    }
}

这确保了以非 root 用户运行的 QEMU 进程可以访问 socket。ChownAtNoFollow 避免符号链接攻击。设备出现(fsnotify Create 事件)时也会重新设置权限。

3.6 USB Device:USB 设备直通

3.6.1 USBDevice 结构体
type USBDevice struct {
    Name         string    // 设备名
    Manufacturer string    // 厂商
    Vendor       int       // 厂商 ID(十六进制)
    Product      int       // 产品 ID(十六进制)
    BCD          int       // 设备版本
    Bus          int       // 总线号
    DeviceNumber int       // 设备号
    Serial       string    // 序列号
    DevicePath   string    // /dev/bus/usb/BBB/DDD
}

唯一标识:Vendor:Product-Bus:DeviceNumber(如 1d6b:0002-01:01

3.6.2 设备发现流程

discoverAllowedUSBDevices

discoverPluggedUSBDevices

filepath.Walk /sys/bus/usb/devices/

跳过 usb* 前缀(控制器)

有 idVendor 文件?

parseSysUeventFile: 解析 uevent

构建 USBDevice

添加到 LocalDevices.devices 按 Vendor 索引

遍历 USBHostDevice 配置

ExternalResourceProvider?

跳过

localDevices.fetch: 按 Vendor:Product 查找

找到所有 selector?

跳过此 resourceName

创建 PluginDevices

从 LocalDevices 移除已分配

继续查找下一组

关键设计:

  • LocalDevices 按 Vendor ID 建立索引,加速查找
  • fetch() 方法找到一组匹配的 USB 设备后从列表中移除,避免同一个物理 USB 设备被分配给多个 K8s 资源
  • 支持多 selector:一个 USBHostDevice 可以指定多个 Vendor:Product 对,需要全部找到才算匹配
  • PluginDevices 的 ID 格式:<resourceName>-<4位随机串>-<index>,确保唯一性
3.6.3 分配逻辑
func (plugin *USBDevicePlugin) Allocate(_ context.Context, allocRequest *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
    for _, request := range allocRequest.ContainerRequests {
        for _, id := range request.DevicesIDs {
            pluginDevices := plugin.FindDevice(id)
            for _, dev := range pluginDevices.Devices {
                // Chown 设备文件为 NonRootUID
                safepath.ChownAtNoFollow(spath, util.NonRootUID, util.NonRootUID)
                
                // DeviceSpec
                &pluginapi.DeviceSpec{
                    ContainerPath: dev.DevicePath,  // /dev/bus/usb/001/002
                    HostPath:      dev.DevicePath,
                    Permissions:   "mrw",
                }
                
                // 环境变量: KUBEVIRT_USB_RESOURCE_<NAME>=Bus:DeviceNum,Bus:DeviceNum
                env[key] = fmt.Sprintf("%d:%d", dev.Bus, dev.DeviceNumber)
            }
        }
    }
}

USB 分配的权限为 "mrw",支持 mmap(某些 USB 设备需要)。环境变量传递 Bus:DeviceNumber 格式,virt-launcher 可据此定位具体设备。

3.7 NVIDIA GPU 专用管理

nvidia.go 是一个精简但关键的策略文件,核心函数 shouldPermitNvidia 仅20行,但解决了 PCI 设备发现中的一个重要边缘场景:

NVIDIA GPU 在 KubeVirt 中的两种使用模式:
                                                    ┌──────────────────┐
                                                    │  PCI 直通模式     │
                                                    │  驱动: vfio-pci  │
                                                    │  通过 VFIO 访问   │
                                                    └──────────────────┘
┌────────────────────┐                    ▲
│  NVIDIA vGPU 模式   │                    │
│  驱动: nvidia       │──── 允许通过 ───────┘
│  通过 mdev 分配     │    shouldPermitNvidia
│  VF 有 vGPU profile │                    │
└────────────────────┘                    │
                                          │  跳过
                                    ┌─────┴──────────────┐
                                    │  普通 NVIDIA GPU     │
                                    │  驱动: nvidia        │
                                    │  无 vGPU profile     │
                                    │  (不能直接 VFIO 直通) │
                                    └─────────────────────┘

为何需要此逻辑?

在 NVIDIA vGPU 部署中,物理 GPU 被拆分为多个 Virtual Functions (VFs)。这些 VFs 使用 nvidia 驱动(而非 vfio-pci),但每个 VF 配置了 vGPU profile,可以通过 mdev 机制创建 vGPU 实例。

如果不做特殊处理,discoverPermittedHostPCIDevices 会因驱动不是 vfio-pci 而跳过这些设备,导致 vGPU 资源无法被发现。shouldPermitNvidia 正确识别了这一场景。

3.8 设备权限与 cgroup 管理

3.8.1 DeviceSpec 权限模型

Kubernetes Device Plugin 的 DeviceSpec.Permissions 使用三个标志位:

标志含义使用场景
r (read)读取权限所有设备
w (write)写入权限所有设备
m (mmap)内存映射VFIO 设备、USB 设备

各设备类型的权限配置:

设备类型权限原因
Generic (KVM/tun/vhost-net)可配置(默认 “rw”)用户传入 permissions 参数
PCI/Mediated“mrw”VFIO 需要 mmap 访问设备寄存器
USB“mrw”USB 设备可能需要 mmap
Socket无 DeviceSpec使用 Mount 而非 DeviceSpec
3.8.2 Socket 权限与 SELinux

Socket Device 的权限管理涉及三层:

  1. 文件所有权 (Chown)safepath.ChownAtNoFollow(path, NonRootUID, NonRootUID) — 将 socket 文件的所有权改为非 root 用户,使 QEMU 可以访问
  2. SELinux Labelselinux.RelabelFilesUnprivileged(isPermissive, path) — 重新标记 SELinux 上下文,确保 virt-launcher 域可以访问
  3. 目录权限:不仅设置 socket 文件本身,还设置其父目录的权限

Socket 设备出现

setSocketDirectoryPermissions

ChownAtNoFollow 目录 → NonRootUID

SELinux 启用?

RelabelFilesUnprivileged 目录

继续

setSocketPermissions

ChownAtNoFollow socket → NonRootUID

SELinux 启用?

RelabelFilesUnprivileged socket

完成

3.8.3 TDX cgroup 容量管理

TDX (Trust Domain Extensions) 设备的数量由 cgroup misc capacity 控制:

func (c *DeviceController) updateTdxDevice() (Device, error) {
    maxTDXVMs, err := cgroup.GetMiscCapacity("tdx")
    // 从 /sys/fs/cgroup/misc.capacity/ 读取 tdx 的容量
    if maxTDXVMs > 0 {
        // 创建 Socket Device Plugin,maxDevices = maxTDXVMs
        if c.virtConfig.RequireQGS() {
            NewSocketDevicePlugin(services.TdxDeviceName, socketDir, socketFile, maxTDXVMs, ...)
        } else {
            NewOptionalSocketDevicePlugin(services.TdxDeviceName, socketDir, socketFile, maxTDXVMs, ...)
        }
    }
}

这确保了 TDX VM 的数量不会超过硬件/内核配置的容量限制。

3.9 热插拔设备处理

3.9.1 设备热插拔检测机制

Device Manager 通过 fsnotify 实现热插拔检测,这是所有设备类型的统一健康检查基础设施:

Create

Remove

Rename

fsnotify Watcher

文件系统事件

设备出现

发送 Healthy 到 health channel

设备消失

发送 Unhealthy 到 health channel

设备重命名/移除

ListAndWatch 收到健康变更

推送更新给 Kubelet

Kubelet 更新 Node Status

调度器感知设备变化

各设备类型的监控对象:

设备类型监控路径特殊行为
Generic设备文件父目录 + Plugin Socket 目录设备 Create→Healthy, Remove/Rename→Unhealthy
Mediated/dev/vfio/<iommuGroup> 每个 VFIO 设备每个 mdev 单独监控
PCI/dev/vfio/<iommuGroup> 每个 VFIO 设备每个 PCI 设备单独监控
Socket<socketRoot>/<socketDir>/<socket>Create 时重新设置权限
USB<hostRootMount>/<DevicePath> 每个设备支持 per-device 健康状态
3.9.2 USB 热插拔的精细化处理

USB 设备的热插拔最为复杂,因为它支持 per-device 健康状态:

func (plugin *USBDevicePlugin) setDeviceHealth(usbID string, isHealthy bool) {
    pd := plugin.FindDeviceByUSBID(usbID)
    isDifferent := pd.isHealthy != isHealthy
    pd.isHealthy = isHealthy
    if isDifferent {
        plugin.update <- struct{}{}  // 触发 ListAndWatch 推送
    }
}

与 Generic/Mediated/PCI 不同,USB 的 ListAndWatch 不监听 health channel,而是监听 update channel。当任何单个 USB 设备的健康状态变化时,整个设备列表被重新推送。

3.9.3 配置驱动的动态设备管理

除了物理设备的热插拔,Device Manager 还支持配置驱动的动态设备变更——当 KubeVirt ConfigMap 更新时,设备列表可能改变:

ConfigMap 更新

virtConfig 回调触发

refreshMediatedDeviceTypes

refreshPermittedDevices

getNode: 获取 Node 对象

GetDesiredMDEVTypes: 读取 Node 注解

updateMDEVTypesConfiguration

需要更新?

返回

加锁 startedPluginsMutex

refreshTDXConfig: 检查 TDX 配置变更

QGS 配置变更?

stopDevice TDX

继续

updatePermittedHostDevicePlugins: 构建新设备列表

splitPermittedDevices: 拆分新增/移除

启动新增设备插件

停止移除设备插件

释放锁

splitPermittedDevices 的集合运算逻辑:

func (c *DeviceController) splitPermittedDevices(devices []Device) (map[string]Device, map[string]struct{}) {
    devicePluginsToRun := make(map[string]Device)
    devicePluginsToStop := make(map[string]struct{})

    // 1. 将所有非永久的已启动插件标记为待停止
    for resourceName := range c.startedPlugins {
        _, isPermanent := c.permanentPlugins[resourceName]
        if !isPermanent {
            devicePluginsToStop[resourceName] = struct{}{}
        }
    }

    // 2. 遍历新设备列表
    for _, device := range devices {
        if _, isRunning := c.startedPlugins[device.GetDeviceName()]; !isRunning {
            // 新设备 → 待启动
            devicePluginsToRun[device.GetDeviceName()] = device
        } else {
            // 已在运行 → 从待停止中移除
            delete(devicePluginsToStop, device.GetDeviceName())
        }
    }

    return devicePluginsToRun, devicePluginsToStop
}

这本质上是一个集合差运算

  • ToRun = 新集合 - 旧集合
  • ToStop = 旧集合 - 新集合 - 永久集合
3.9.4 MDEV 类型的动态创建与删除

MDEVTypesManager 管理中介设备类型的生命周期:

删除流程:

func removeUndesiredMDEVs(desiredTypesMap map[string]struct{}) {
    files, err := os.ReadDir(mdevBasePath)  // /sys/bus/mdev/devices/
    for _, file := range files {
        if shouldRemoveMDEV(file.Name(), desiredTypesMap) {
            handler.RemoveMDEVType(file.Name())
            // 写入 /sys/bus/mdev/devices/<UUID>/remove
        }
    }
}

创建流程(轮询分配策略):

configureDesiredMDEVTypes

initMDEVTypesRing: 创建环形缓冲区

遍历 Ring

mdevType 有可用 parent?

从 availableMdevTypesMap 删除

getNextAvailableParentToConfigure

找到未配置的 parent?

移动到下一个 mdevType

createMdevTypes: 创建所有可用实例

创建成功?

标记 parent 已配置

记录错误继续

Map 为空或所有 parent 已配置?

结束

关键设计决策:每个 GPU 卡只配置一个 mdev 类型。 这是 NVIDIA vGPU 的硬件限制——一块物理 GPU 同时只能运行一种 vGPU profile。getNextAvailableParentToConfigure 找到第一个未配置的 parent,创建该类型的所有可用实例,然后标记 parent 为已配置。

createMdevTypes 为某个 parent 上的某个 mdev 类型创建所有可用实例:

func createMdevTypes(mdevType string, parentID string) error {
    instances, err := handler.ReadMDEVAvailableInstances(mdevType, parentID)
    // 读取 /sys/class/mdev_bus/<parent>/mdev_supported_types/<type>/available_instances
    for i := 0; i < instances; i++ {
        handler.CreateMDEVType(mdevType, parentID)
        // 通过 virt-chroot 写入 /sys/class/mdev_bus/<parent>/mdev_supported_types/<type>/create
    }
}

四、Mermaid 架构图集

4.1 DeviceManager 整体架构图

宿主机

Kubernetes

virt-handler Device Manager

KubeVirt 控制面

Device Plugin 实例

ConfigMap Watch

Node 缓存

管理

管理

管理

管理

管理

调用

调用

gRPC

gRPC

gRPC

gRPC

gRPC

更新

调度决策

读取

读取

TDX 容量

kubevirt-config ConfigMap

Node Informer

DeviceController
编排层

MDEVTypesManager
mdev 生命周期

DeviceHandler
硬件操作抽象

GenericDevicePlugin
/dev/kvm, /dev/tun, /dev/vhost-net
/dev/sev, /dev/vhost-vsock

MediatedDevicePlugin
vGPU mdev UUIDs

PCIDevicePlugin
GPU, NIC, SR-IOV VF

SocketDevicePlugin
TDX QGS, PR Helper

USBDevicePlugin
USB 直通设备

Kubelet Device Plugin Manager

Scheduler

Node Status

/sys/bus/pci
/sys/bus/mdev
/sys/class/mdev_bus

/dev/kvm
/dev/vfio/
/dev/bus/usb/

cgroup misc.capacity

4.2 DeviceController 类图

管理

缓存

实现

DeviceController

-permanentPlugins map<string>Device

-startedPlugins map<string>controlledDevice

-startedPluginsMutex sync.Mutex

-host string

-maxDevices int

-permissions string

-backoff []time.Duration

-virtConfig *ClusterConfig

-mdevTypesManager *MDEVTypesManager

-nodeStore cache.Store

-mdevRefreshWG *sync.WaitGroup

-lastTDXAttestationConfig *tdxConfigState

+NewDeviceController(host, maxDevices, permissions, permanentPlugins, clusterConfig, nodeStore) : *DeviceController

+Run(stop chan struct)

+Initialized() : bool

+NodeHasDevice(devicePath string) : bool

+RefreshMediatedDeviceTypes()

-updatePermittedHostDevicePlugins() : []Device

-refreshPermittedDevices()

-splitPermittedDevices(devices)(map<string>Device, map<string>struct)

-startDevice(resourceName, dev)

-stopDevice(resourceName)

-refreshMediatedDeviceTypes() : bool

-refreshTDXConfig() : bool

-updateTdxDevice()(Device, error)

controlledDevice

+devicePlugin Device

+started bool

+stopChan chan struct

+backoff []time.Duration

+Start()

+Stop()

+GetName() : string

tdxConfigState

+socketPath string

+requireQGS bool

«interface»

DeviceControllerInterface

+Initialized() : bool

+RefreshMediatedDeviceTypes()

4.3 设备类型层次图

实现

实现

实现

实现

实现

嵌入

嵌入

嵌入

嵌入

«interface»

Device

+Start(stop) : error

+ListAndWatch(Empty, Server) : error

+PreStartContainer(Context, Request)(Response, error)

+Allocate(Context, AllocateRequest)(AllocateResponse, error)

+GetDeviceName() : string

+GetInitialized() : bool

DevicePluginBase

+devs []*Device

+server *grpc.Server

+socketPath string

+stop chan struct

+health chan deviceHealth

+resourceName string

+done chan struct

+initialized bool

+lock *sync.Mutex

+deregistered chan struct

+devicePath string

+deviceRoot string

+deviceName string

+GetDeviceName() : string

+ListAndWatch() : error

+PreStartContainer()(Response, error)

+GetDevicePluginOptions()(Options, error)

+Allocate()(AllocateResponse, error)

+stopDevicePlugin() : error

+cleanup() : error

+GetInitialized() : bool

+setInitialized(bool)

+register() : error

GenericDevicePlugin

+preOpen bool

+permissions string

+Start(stop) : error

+ListAndWatch() : error

+Allocate()(AllocateResponse, error)

+healthCheck() : error

+register() : error

+cleanup() : error

MediatedDevicePlugin

+iommuToMDEVMap map<string>string

+Start(stop) : error

+Allocate()(AllocateResponse, error)

+healthCheck() : error

PCIDevicePlugin

+iommuToPCIMap map<string>string

+Start(stop) : error

+Allocate()(AllocateResponse, error)

+healthCheck() : error

SocketDevicePlugin

+socketRoot string

+socketDir string

+socket string

+executor SELinuxExecutor

+p PermissionManager

+healthChecks bool

+Start(stop) : error

+Allocate()(AllocateResponse, error)

+healthCheck() : error

+register() : error

+setSocketPermissions() : error

+setSocketDirectoryPermissions() : error

USBDevicePlugin

+update chan struct

+devices []*PluginDevices

+logger *FilteredLogger

+Start(stop) : error

+ListAndWatch() : error

+Allocate()(AllocateResponse, error)

+healthCheck() : error

+register() : error

+stopDevicePlugin() : error

+FindDevice(id) : *PluginDevices

+FindDeviceByUSBID(usbID) : *PluginDevices

+setDeviceHealth(usbID, isHealthy)

4.4 Generic Device 流程图

分配

运行时

启动

初始化

NewGenericDevicePlugin

创建 maxDevices 个虚拟 Device ID

socketPath = /var/lib/kubelet/device-plugins/kubevirt-kvm.sock

Start

cleanup: 删除旧 socket

preOpen?

os.Open 设备文件 → 触发 modprobe

跳过

net.Listen Unix socket

grpc.NewServer + Register

server.Serve goroutine

waitForGRPCServer 5s 超时

register → Kubelet

healthCheck goroutine

setInitialized true

ListAndWatch

发送初始设备列表

监听 health channel

健康变更

更新所有设备 Health

推送新列表给 Kubelet

Allocate

构建 DeviceSpec

HostPath = /dev/kvm

ContainerPath = /dev/kvm

Permissions = rw

4.5 Mediated Device 流程图

分配

插件构建

设备发现

discoverPermittedHostMediatedDevices

遍历 /sys/bus/mdev/devices/

读取 mdev_type/name

匹配 supportedMdevsMap?

跳过

构建 MDEV: UUID, typeName, parentPCIAddr, iommuGroup, numaNode

按 typeName 分组返回

NewMediatedDevicePlugin

constructDPIdevicesFromMdev

Device ID = IOMMU Group

iommuToMDEVMap: IOMMU→UUID

numaNode ≥ 0?

附加 NUMA Topology

无拓扑信息

返回 devs 列表

Allocate

遍历 ContainerRequests

遍历 DevicesIDs

IOMMU Group → mdev UUID

设备文件存在?

返回错误

formatVFIODeviceSpecs

/dev/vfio/vfio + /dev/vfio/IOMMU

设置 ENV: KUBEVIRT_MDEV_RESOURCE_...=UUID1,UUID2

4.6 PCI Device 流程图

分配

插件构建

设备发现

discoverPermittedHostPCIDevices

filepath.Walk /sys/bus/pci/devices/

GetDevicePCIID: 读取 uevent→PCI_ID

PCI ID 匹配 supportedPCIDeviceMap?

跳过

GetDeviceDriver

driver == vfio-pci?

允许

shouldPermitNvidia

driver == nvidia 且 有 vGPU profile 且 非 PF?

跳过

构建 PCIDevice: pciID, driver, pciAddress, iommuGroup, numaNode

按 resourceName 分组

NewPCIDevicePlugin

constructDPIdevices

Device ID = IOMMU Group

iommuToPCIMap: IOMMU→PCI地址

numaNode ≥ 0?

附加 NUMA Topology

无拓扑

Allocate

IOMMU → PCI 地址

formatVFIODeviceSpecs

/dev/vfio/vfio + /dev/vfio/IOMMU

ENV: KUBEVIRT_PCI_RESOURCE_...=PCI_ADDR1,PCI_ADDR2

4.7 Socket Device 流程图

健康检查

分配

构建

NewSocketDevicePlugin

设置 socketRoot: / 或 /proc/1/root

创建 maxDevices 个虚拟 Device ID

setSocketDirectoryPermissions

Chown 目录 → NonRootUID

SELinux relabel 目录

setSocketPermissions

Chown socket → NonRootUID

SELinux relabel socket

Allocate

构建 Mount

HostPath = socketDir

ContainerPath = socketDir

ReadOnly = false

healthCheck

healthChecks 启用?

设备始终 Healthy

fsnotify 监控 socket

socket Create

sendHealthUpdate true

重新设置权限

setSocketDirectoryPermissions

setSocketPermissions

socket Remove/Rename

sendHealthUpdate false

4.8 Nvidia GPU 流程图

PCI 设备发现

driver != vfio-pci?

标准 VFIO 直通

shouldPermitNonVFIOPCI

shouldPermitNvidia

driver == nvidia?

❌ 拒绝: 非支持的驱动

IsPhysicalFunction

有 sriov_totalvfs?

❌ 拒绝: 是 PF,不能直接分配

HasVGPUProfile

读取 nvidia/current_vgpu_type

vgpu_type 非空且非 0?

❌ 拒绝: 无 vGPU profile

✅ 允许: vGPU VF,通过 mdev 分配

4.9 设备注册时序图

KubeletDevicePlugincontrolledDeviceDeviceControllerKubeletDevicePlugincontrolledDeviceDeviceController注册完成,Kubelet 开始调用loop[重试循环(指数退避)]健康状态变更时Run() → startDevice(name, dev)controlledDevice.Start()创建 stop channelStart(stop)cleanup() 删除旧 socketpreOpen (仅 Generic)net.Listen(unix, socketPath)grpc.NewServer()RegisterDevicePluginServer()server.Serve(sock) [goroutine]waitForGRPCServer(5s)gRPC Connect(KubeletSocket)连接建立Register(Version, Endpoint, ResourceName)注册确认healthCheck() [goroutine]setInitialized(true)ListAndWatch()初始设备列表更新 Node Allocatablehealth ← deviceHealth{Healthy/Unhealthy}推送更新设备列表更新 Node Status

4.10 设备分配时序图

virt-launcherDevicePluginKubeletSchedulervirt-launcherDevicePluginKubeletScheduleralt[Generic Device][Mediated Device][PCI Device][Socket Device][USB Device]检查 Node Allocatable >= Pod requests绑定 Pod 到 NodeAllocate(ContainerRequests)构建 DeviceSpec(HostPath, ContainerPath, Permissions)ContainerAllocateResponse{Devices}IOMMU→mdev UUID 映射formatVFIODeviceSpecs(/dev/vfio/vfio + /dev/vfio/<IOMMU>)ENV: KUBEVIRT_MDEV_RESOURCE_...=UUID1,UUID2ContainerAllocateResponse{Devices, Envs}IOMMU→PCI 地址映射formatVFIODeviceSpecsENV: KUBEVIRT_PCI_RESOURCE_...=PCI_ADDR1,PCI_ADDR2ContainerAllocateResponse{Devices, Envs}构建 Mount(HostPath=socketDir, ContainerPath=socketDir)ContainerAllocateResponse{Mounts}FindDevice(pluginDeviceID)ChownAtNoFollow → NonRootUIDDeviceSpec(/dev/bus/usb/BBB/DDD, "mrw")ENV: KUBEVIRT_USB_RESOURCE_...=Bus:DeviceNumContainerAllocateResponse{Devices, Envs}容器创建,挂载设备/目录virt-launcher 读取 ENV,配置 QEMU 设备参数

4.11 cgroup/权限管理图

VFIO 设备权限

formatVFIODeviceSpecs

/dev/vfio/vfio: Permissions=mrw

/dev/vfio/IOMMU: Permissions=mrw

USB 权限管理

Allocate 时

safepath.JoinAndResolveWithRelativeRoot: /dev/bus/usb/BBB/DDD

ChownAtNoFollow → NonRootUID:NonRootUID

Socket 权限管理

setSocketPermissions

safepath.JoinAndResolveWithRelativeRoot

ChownAtNoFollow: socket → NonRootUID:NonRootUID

SELinux 启用?

RelabelFilesUnprivileged: socket

完成

setSocketDirectoryPermissions

safepath.JoinAndResolveWithRelativeRoot: dir

ChownAtNoFollow: dir → NonRootUID:NonRootUID

SELinux 启用?

RelabelFilesUnprivileged: dir

TDX 容量管理

cgroup.GetMiscCapacity tdx

读取 /sys/fs/cgroup/misc.capacity/tdx

maxTDXVMs = 容量值

Socket Device Plugin maxDevices = maxTDXVMs

4.12 热插拔处理图

mdev 动态配置

配置驱动的动态变更

物理热插拔 (fsnotify)

设备文件 Create

设备文件 Remove

设备文件 Rename

Plugin Socket Remove

fsnotify.Watcher

文件系统事件

发送 Healthy

发送 Unhealthy

Kubelet 重启,退出 healthCheck

ListAndWatch 更新

Kubelet 收到更新

更新 Node Status

调度器感知变化

ConfigMap 更新

virtConfig 回调

refreshPermittedDevices

加锁 startedPluginsMutex

refreshTDXConfig: 检测 TDX 变更

updatePermittedHostDevicePlugins: 重新发现设备

splitPermittedDevices: 计算新增/移除

启动新增 Device Plugin

停止移除 Device Plugin

释放锁

refreshMediatedDeviceTypes

获取 Node 注解中的期望 mdev 类型

removeUndesiredMDEVs: 删除不需要的 mdev

discoverConfigurableMDEVTypes: 发现可配置类型

configureDesiredMDEVTypes: 轮询创建 mdev

需要更新 Device Plugin?

返回

4.13 USB 设备发现与分配详细流程

USB 分配

Allocate

FindDevice: pluginDeviceID → PluginDevices

遍历 PluginDevices.Devices

ChownAtNoFollow: /dev/bus/usb/BBB/DDD → NonRootUID

DeviceSpec: ContainerPath=/dev/bus/usb/BBB/DDD, HostPath, mrw

ENV: KUBEVIRT_USB_RESOURCE_...=Bus:DeviceNum

USB 设备发现

discoverAllowedUSBDevices

discoverPluggedUSBDevices

Walk /sys/bus/usb/devices/

跳过 usb* 控制器

有 idVendor?

跳过

parseSysUeventFile

解析 BUSNUM, DEVNUM, PRODUCT, DEVNAME

构建 USBDevice{Vendor, Product, Bus, DeviceNumber, DevicePath}

按 Vendor 索引到 LocalDevices

遍历 USBHostDevice 配置

ExternalResourceProvider?

跳过

localDevices.fetch: 匹配 selectors

所有 selector 都找到?

跳过此资源

创建 PluginDevices

从 LocalDevices 移除已分配设备

继续查找下一组


五、关键设计模式与深度分析

5.1 Device Plugin 标准接口与 KubeVirt 扩展

KubeVirt Device Manager 严格遵循 Kubernetes Device Plugin v1beta1 API,同时做了以下扩展:

  1. 环境变量传递设备信息:PCI 和 Mediated Device 在 Allocate 响应中设置环境变量(KUBEVIRT_PCI_RESOURCE_*KUBEVIRT_MDEV_RESOURCE_*),virt-launcher 容器启动后读取这些变量获取具体的 PCI 地址或 mdev UUID,用于配置 QEMU 命令行参数。

  2. Socket Mount 而非 Device Spec:Socket Device 使用 Mount 而非 DeviceSpec,因为 Unix Socket 不是字符设备,不能用 DeviceSpec 传递。

  3. Feature Gate 驱动的设备发现:TDX、SEV、VSOCK 等设备只在对应 Feature Gate 启用时才暴露,避免不必要的资源注册。

  4. ExternalResourceProvider 排除:对于由外部 Device Plugin(如 NVIDIA GPU Operator)管理的设备,KubeVirt 跳过注册,避免重复暴露。

5.2 并发安全设计

Device Manager 的并发模型:

  1. startedPluginsMutex:保护 startedPlugins 的读写,防止 refreshPermittedDevices 并发执行导致的重复 start/stop。

  2. mdevRefreshWG:确保 mdev 刷新操作在 DeviceController 退出前完成。

  3. DevicePluginBase.lock:保护 initialized 标志的原子读写。

  4. controlledDevice.stopChan:通过 channel 关闭实现优雅停止,避免 data race。

  5. MDEVTypesManager.mdevsConfigurationMutex:保护 mdev 配置操作的串行化。

5.3 错误恢复与重试机制

  1. controlledDevice 退避重试:Device Plugin 启动失败时自动重试,退避序列 [1s, 2s, 5s, 10s]

  2. Kubelet 重启检测:当 Device Plugin 的 socket 被删除(Kubelet 重启时会清理),healthCheck goroutine 退出,触发 controlledDevice 的重试循环重新注册。

  3. 反注册优雅退出stopDevicePlugin 先发送空设备列表,等待最多 1 秒让 Kubelet 处理,然后才停止 Server。

5.4 资源命名规范

Generic:   devices.kubevirt.io/<deviceName>     (如 devices.kubevirt.io/kvm)
PCI:       <配置中指定的 resourceName>           (如 nvidia.com/GPU)
Mediated:  <配置中指定的 resourceName>           (如 nvidia.com/A100-PCIE-40GB)
Socket:    devices.kubevirt.io/<socketName>     (如 devices.kubevirt.io/tdx)
USB:       <配置中指定的 resourceName>           (如 kubevirt.io/storage)

Socket 路径命名:

/var/lib/kubelet/device-plugins/kubevirt-kvm.sock
/var/lib/kubelet/device-plugins/kubevirt-tun.sock
/var/lib/kubelet/device-plugins/kubevirt-nvidia---GPU.sock  (PCI, / → -)
/var/lib/kubelet/device-plugins/kubevirt-usb-GPU.sock       (USB)

5.5 NUMA 拓扑感知

PCI 和 Mediated Device 支持通过 TopologyInfo 报告 NUMA 亲和性:

if pciDevice.numaNode >= 0 {
    dpiDev.Topology = &pluginapi.TopologyInfo{
        Nodes: []*pluginapi.NUMANode{
            {ID: int64(pciDevice.numaNode)},
        },
    }
}

Kubernetes 1.26+ 的 TopologyManager 可以利用此信息,将 VM Pod 调度到正确的 NUMA 节点,避免跨 NUMA 访问的性能损失。

5.6 virt-chroot 与安全边界

mdev 的创建和删除通过 virt-chroot 执行,而非直接系统调用:

func (h *DeviceUtilsHandler) CreateMDEVType(mdevType string, parentID string) error {
    _, err := virt_chroot.CreateMDEVType(mdevType, parentID, string(uid)).Output()
}

这是因为 virt-handler 容器可能以非 root 运行,对 /sys/class/mdev_bus/ 的写操作需要提升权限。virt-chroot 通过 chroot 到宿主根文件系统并以适当权限执行命令来实现。


六、总结

KubeVirt Device Manager 是一个设计精巧的子系统,它:

  1. 完全兼容 Kubernetes Device Plugin API,无需修改 Kubelet
  2. 支持5种设备类型,覆盖虚拟化的所有硬件直通场景
  3. 动态配置,通过 ConfigMap Watch 实现设备策略的实时变更
  4. 热插拔感知,通过 fsnotify 实时跟踪设备状态变化
  5. 安全设计,通过 SELinux relabel、ChownAtNoFollow、virt-chroot 确保权限边界
  6. NUMA 拓扑,支持 TopologyManager 的 NUMA 亲和调度
  7. NVIDIA vGPU 特殊处理,正确识别 nvidia 驱动下的 vGPU VF
  8. 容错恢复,指数退避重试 + Kubelet 重启自动重新注册

其架构体现了关注点分离的原则:DeviceController 负责编排(发现、启停、配置变更),各 DevicePlugin 负责与 Kubelet 的 gRPC 交互,DeviceHandler 抽象硬件操作,MDEVTypesManager 管理 mdev 生命周期。每一层都可以独立测试和替换。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值