1

学习目的

了解sriov网卡的正确读取姿势,学习相关的golang开源库

配置文件

看这部分代码前我们先看看官方提供的一个配置文件范例:

apiVersion: v1
kind: ConfigMap
metadata:
  name: sriovdp-config
  namespace: kube-system
data:
  config.json: |
    {
        "resourceList": [{
                "resourceName": "intel_sriov_netdevice",
                "selectors": {
                    "vendors": ["8086"], 
                    "devices": ["154c", "10ed"],
                    "drivers": ["i40evf", "ixgbevf"]
                }
            },
            {
                "resourceName": "intel_sriov_dpdk",
                "selectors": {
                    "vendors": ["8086"],
                    "devices": ["154c", "10ed"],
                    "drivers": ["vfio-pci"],
                    "pfNames": ["enp0s0f0","enp2s2f1"]
                }
            },
            {
                "resourceName": "mlnx_sriov_rdma",
                "isRdma": true,
                "selectors": {
                    "vendors": ["15b3"],
                    "devices": ["1018"],
                    "drivers": ["mlx5_ib"]
                }
            },
            {
                "resourceName": "mlnx_sriov_rdma",
                "isRdma": true,
                "selectors": {
                    "pfNames": ["enp0s0f0#1,3,5-9,23","enp2s2f1"],
                    "vendors": ["15b3"],
                    "devices": ["1018"],
                    "drivers": ["mlx5_ib"]
                }
            }
        ]
    }

这里selectors是用来过滤机器上的pci网络设备的,只要当前check的网卡的属性包含于配置文件里对应属性的数组,就会被选入。

vendors表示厂商号,devices表示设备号,drivers表示驱动,pfNames表示vf所在pf的网卡名。

比如某个网卡的bdf是0000:00:11.4,我们可以通过lspci -n |grep 00:11.4得到他的信息:

lspci  -n |grep '00:11.4'     
00:11.4 0106: 8086:8d62 (rev 05)

8086就是厂商号,8d62就是设备号。

值得一提的是,这里selectors中的pfNames是一个规则表达式。可以简单地用来作为pf网卡名的过滤器,也可以通过这个字段指定pf上哪些vf要注册为k8s资源。

pfNames是一个字符串数组,他的每一个字符串元素都按照如下的规则:

"<PFName>#<SingleVF>,<FirstVF>-<LastVF>,<SingleVF>,<SingleVF>,<FirstVF>-<LastVF>"
`<PFName>`   - is the PF interface name
`<SingleVF>` - is a single VF index (0-based) that is included into the pool
`<FirstVF>`  - is the first VF index (0-based) that is included into the range
`<LastVF>`   - is the last VF index (0-based) that is included into the range

比如我们机器上sriov网卡有两块pf,分别名为eth0,eth1,每个pf有32个vf,其中前两个vf要预留作为其他网络的设备,那么可以写成:

"pfNames": ["eth0#2-31","eth1#2-31"]

入口和参数

intel/sriov-network-device-plugin的程序入口在cmd/sriovdp/main.go

提供了两个基本的参数:

  1. config-file。指定配置参数
  2. resource-prefix。指定要注册到apiserver中的硬件资源的前缀,比如intel.comnetease.com

main函数中调用了四个主要函数:

第一步:newResourceManager

resourceManager是deviceplugin的核心逻辑组件,初始化一个空的resourceManager,并依次调用readConfig,validConfigs, 注入配置。配置由ResourceConfig数组构成,包括:

type ResourceConfig struct {
    ResourceName string `json:"resourceName"` // the resource name will be added with resource prefix in K8s api
    IsRdma       bool   // the resource support rdma
    Selectors    struct {
        Vendors   []string `json:"vendors,omitempty"`
        Devices   []string `json:"devices,omitempty"`
        Drivers   []string `json:"drivers,omitempty"`
        PfNames   []string `json:"pfNames,omitempty"`
        LinkTypes []string `json:"linkTypes,omitempty"`
    } `json:"selectors,omitempty"` // Whether devices have SRIOV virtual function capabilities or not
}

第二步:discoverHostDevices

intel引用了github.com/jaypipes/ghw,将机器上的PCI设备全部list一遍,并判断和逐步过滤,最后归纳出所有vf设备,记录的数据类型是types.PciNetDevice

pci的设备必须是网络设备 -> 机器上必须没有该网卡对应的default路由 -> 是sriov的vf设备  -> 加入到resourceManager.netDeviceList

第三步:initServers

在上面我们提到的config-file中,我们可以定义多组配置,每组是一个resourceName,它会对应一个device-plugin的服务。对于每一组配置,做的事情如下:

  1. 生成资源池resourcePool。他们将之前在配置文件中写的多个Selectors应用到resourceManager.netDeviceList中(每个selector的filter逻辑见pkg/resources/deviceSelectors.go),得到我们想要的类型的vf设备,并还能根据配置文件过滤出rdma设备。我们可以看到在resourcePool的结构体中包含了一个devicePool,这里是真正存储设备的地方。
  2. 初始化一个resourceServer,按照我们的配置,生成一个resourceServer,它包括真正的资源池resourcePool,grpc Server,resourcePrefix等:

    return &resourceServer{
         resourcePool:       rp,
         pluginWatch:        pluginWatch,
         endPoint:           sockName,
         sockPath:           sockPath,
         resourceNamePrefix: prefix,
         grpcServer:         grpc.NewServer(),
         termSignal:         make(chan bool, 1),
         updateSignal:       make(chan bool),
         stopWatcher:        make(chan bool),
         checkIntervals:     20, // updates every 20 seconds
     }

第四步:startAllServers

执行resourceServer的Start方法,运行device-plugin。device-plugin会先向kubelet发送一个Register。kubelet收到后会启动一个grpc客户端,与resourceServer中的grpcServer建立连接。

kubelet会远程调用ListAndWatch,并在需要申请资源时调用Allocate

配置文件自定义

intel官方提供的配置文件并不足以满足所有的sriov设备,比如当我们使用Mellanox 5系列的硬件时,用这套configmap是没法识别出vf的。按照上问的说明,我们可以在机器上使用lspci确定该sriov网卡的vf的vendorID、deviceID,driver, 通过添加或修改configmap内容,实现对该硬件的采集。如:

# lspci  -n |grep 04:05.1
04:05.1 0200: 15b3:1016
# ls -l /sys/class/pci_bus/0000:04/device/0000:04:05.1  |grep driver
lrwxrwxrwx 1 root root       0 Nov 20 11:09 driver -> ../../../../bus/pci/drivers/mlx5_core
-rw-r--r-- 1 root root    4096 Nov 20 11:09 driver_override

## change configmap yaml file:
...
            {
                "resourceName": "mlnx_sriov_rdma",
                "isRdma": true,
                "selectors": {
                    "vendors": ["15b3"],
                    "devices": ["1016"],
                    "drivers": ["mlx5_core"]
                }
            }
            ...

device-plugin的工作内容

ListAndWatch

resourcePool中的所有设备组成数组返回给kubelet。然后开始监听退出信号和更新信号。一旦发生设备更新,会刷新将设备数组返回给kubelet

kubelet会在内存中记录设备数组。

Allocate

kubelet会记录那些容器用了哪些设备,并找到一个可以用的设备,向device-plugin申请该设备。resourceServer收到请求后,将使用该设备需要设置的docker option封装返回。options包括:

  1. Devices
  2. Envs
  3. Mounts

从上面的分析我们知道,resourcePool中记录的资源,归根结底·是types.PciNetDevice资源。所以我们还是要从github.com\intel\sriov-network-device-plugin\pkg\resources\pciNetDevice.go去找“到底返回给kubelet的数据是怎样的”。

  1. 我们看到func (nd *pciNetDevice) GetDeviceSpecs()func (nd *pciNetDevice) GetMounts()返回的都是资源的结构体字段,在func NewPciNetDevice 中我们又看到这些字段的赋值是按照不同驱动区分的:

    func NewPciNetDevice(pciDevice *ghw.PCIDevice, rFactory types.ResourceFactory) (types.PciNetDevice, error) {
     pciAddr := pciDevice.Address
     driverName, err := utils.GetDriverName(pciAddr)
     ...
     // 3. Get Device file info (e.g., uio, vfio specific)
     // Get DeviceInfoProvider using device driver
     infoProvider := rFactory.GetInfoProvider(driverName)
     dSpecs := infoProvider.GetDeviceSpecs(pciAddr)
     mnt := infoProvider.GetMounts(pciAddr)
     env := infoProvider.GetEnvVal(pciAddr)
     ...
    }

factory.go中,我们看到:

func (rf *resourceFactory) GetInfoProvider(name string) types.DeviceInfoProvider {
    switch name {
    case "vfio-pci":
        return newVfioResourcePool()
    case "uio", "igb_uio":
        return newUioResourcePool()
    default:
        return newNetDevicePool()
    }
}

意思很清楚了,不同的pciDevice有不同的driver,通过读取其driver,我们可以确认这些vf的厂家和型号。从而组织成对应的docker options

这里intel提供的可兼容driver包括:

  1. vfio-pci。见pkg/resources/vfioPool.go
  2. uio或igb_uio。见pkg/resources/uioPool.go
  3. 其他。见pkg/resources/netDevicePool.go(可以由用户自己扩展,只要实现GetDeviceSpecs,GetEnvVal,GetMounts就可以用)

一个Allocate的response案例如下:

AllocateResponse send: &AllocateResponse{ContainerResponses:[]*ContainerAllocateResponse{&ContainerAllocateResponse{Envs:map[string]string{PCIDEVICE_NETWORK_NETEASE_COM_SRIOV_VF: 0000:06:0c.1,},Mounts:[]*Mount{},Devices:[]*DeviceSpec{&DeviceSpec{ContainerPath:/dev/infiniband/ucm97,HostPath:/dev/infiniband/ucm97,Permissions:rwm,},&DeviceSpec{ContainerPath:/dev/infiniband/issm97,HostPath:/dev/infiniband/issm97,Permissions:rwm,},&DeviceSpec{ContainerPath:/dev/infiniband/umad97,HostPath:/dev/infiniband/umad97,Permissions:rwm,},&DeviceSpec{ContainerPath:/dev/infiniband/uverbs97,HostPath:/dev/infiniband/uverbs97,Permissions:rwm,},&DeviceSpec{ContainerPath:/dev/infiniband/rdma_cm,HostPath:/dev/infiniband/rdma_cm,Permissions:rwm,},},Annotations:map[string]string{},},},}

fzu_huang
293 声望126 粉丝