学习目的
了解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是一个字符串数组,他的每一个字符串元素都按照如下的规则:
"#,-,,,-"
`` - is the PF interface name
`` - is a single VF index (0-based) that is included into the pool
`` - is the first VF index (0-based) that is included into the range
`` - 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
提供了两个基本的参数:
- config-file。指定配置参数
- resource-prefix。指定要注册到apiserver中的硬件资源的前缀,比如
intel.com
,netease.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的服务。对于每一组配置,做的事情如下:
- 生成资源池
resourcePool
。他们将之前在配置文件中写的多个Selectors应用到resourceManager.netDeviceList
中(每个selector的filter逻辑见pkg/resources/deviceSelectors.go
),得到我们想要的类型的vf设备,并还能根据配置文件过滤出rdma设备。我们可以看到在resourcePool
的结构体中包含了一个devicePool
,这里是真正存储设备的地方。 - 初始化一个
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包括:
- Devices
- Envs
- Mounts
从上面的分析我们知道,resourcePool
中记录的资源,归根结底·是types.PciNetDevice
资源。所以我们还是要从github.com\intel\sriov-network-device-plugin\pkg\resources\pciNetDevice.go
去找“到底返回给kubelet的数据是怎样的”。
-
我们看到
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包括:
- vfio-pci。见
pkg/resources/vfioPool.go
- uio或igb_uio。见
pkg/resources/uioPool.go
- 其他。见
pkg/resources/netDevicePool.go
(可以由用户自己扩展,只要实现GetDeviceSpecs
,GetEnvVal
,GetMounts
就可以用)