在传统的虚拟化中,虚拟机的网卡通常是通过桥接(Bridge或OVS)的方式,因为这种方式最方便,也最简单,但是这样做最大的问题在于性能。本文讲的SR-IOV在2010年左右由Intel提出,但是随着容器技术的推广,intel官方也给出了SR-IOV技术在容器中使用的开源组件,例如:sriov-cni和sriov-device-plugin等,所以SR-IOV也开始在容器领域得到的大量使用。
文|周宇
编辑|zouyee
技术深度|适中
SR-IOV简介
谈及智能制造,首先就需要从企业需求角度出发。制造业企业最关心的是质量能不能更好?成本能不能再低一些?怎么让交付更快?说到底是制造业本身对规模效应的追求,特别是在当今市场需求愈发多样化、个性化,企业需要具备更强的柔性制造能力和产品设计创新能力。智能制造正是要回应企业对规模效应和柔性化制造这两方面的诉求。
SR-IOV由来
在传统的虚拟化中,虚拟机的网卡通常是通过桥接(Bridge或OVS)的方式,因为这种方式最方便,也最简单,但是这样做最大的问题在于性能。本文讲的SR-IOV在2010年左右由Intel提出,SR-IOV全称Single-Root I/O Virtualization,是一种基于硬件的虚拟化解决方案,它允许多个云主机高效共享PCIe设备,且同时获得与物理设备性能媲美的I/O性能,能有效提高性能和可伸缩性。可适用于网络NFV、云游戏、视频流(UDP)等对网络性能、传输速度要求极高的应用场景,比如在游戏场景中,当宿主机的CPU压力比较大时,虚拟机内部网卡的发包率(PPS)性能会下降,极端情况会出现丢包。这点对于游戏业务来说,最直观的感受是游戏卡顿、玩家掉线,这在游戏业务中是绝对不允许的。虽然sriov最初提出时是面向虚拟机应用,但是随着容器技术的推广,intel官方也给出了SR-IOV技术在容器中使用的开源组件,例如:sriov-cni和sriov-device-plugin等,所以SR-IOV也开始在容器领域得到的大量使用。
SR-IOV技数主要是虚拟出来通道给用户使用的,通道分为两种:
- PF(Physical Function,物理功能):管理 PCIe 设备在物理层面的通道功能,可以看作是一个完整的 PCIe 设备,包含了 SR-IOV 的功能结构,具有管理、配置 VF 的功能。
- VF(Virtual Function,虚拟功能):是 PCIe 设备在虚拟层面的通道功能,即仅仅包含了 I/O 功能,VF 之间共享物理资源。VF 是一种裁剪版的 PCIe 设备,仅允许配置其自身的资源,虚拟机无法通过 VF 对 SR-IOV 网卡进行管理。所有的 VF 都是通过 PF 衍生而来,有些型号的 SR-IOV 网卡最多可以生成 256 个 VF。在性能方面,硬件化虚拟网卡VF,一般能达到原生网卡的一半性能。
关于PF和VF的关系描述:每个VF就像是物理网卡硬件资源的一个切片,而PF则是对所有物理网卡硬件资源的统筹者,包括管理众多 VF 可以协同工作。
对于SR-IOV支持的网卡(NIC),每个VF的MAC和IP地址可独立配置,VF之间的数据包切换在设备硬件中进行,pod中使用vf设备作为网卡设备使用。在 Kubernetes pod 中使用 SR-IOV 网络设备的优势包括:
- 与网卡设备的直接通信有助于向“裸机”性能靠近,也就是说容器可以直接访问物理网卡设备
- 支持用户空间中的多个快速网络数据包处理同步工作负载(例如,基于DPDK)。由于优越的性能优势,DPDK变得流行起来,越来越多的应用开始基于DPDK开发。传统的容器网络使用VETH作为网络设备,目前无法直接使用DPDK的PMD驱动,所以无法直接在容器内部使用基于DPDK的应用。而在多网卡的方案中,容器使用网络设备,网络设备为常见的E1000 或者 virtio_net设备,这两种设备都有PMD驱动,容器使用这个设备就能够直接运行基于DPDK的应用,能够在一定程度上提升容器内应用的网络性能。
适用场景
a. 游戏场景
网络游戏场景中对实时性要求比较高,使用纯虚拟化出来的网卡或者容器方案的话,首先包中转的设备较多且都是纯软件虚拟化出来的网络设备,无法保证低丢包率和低时延。而SR-IOV硬件化虚拟网卡VF,一般能达到原生网卡的一半性能,所以游戏场景会使用到SR-IOV。
b. 流量隔离场景
比如在金融行业中,需要对数据库每天进行备份,而容器内部只有一张网卡,因此,可能出现在备份过程中,出现一个很大的网络流量,直接把网卡带宽占满,造成数据库业务的中断。同时,在k8s集群中,有很多功能,需要通过容器原生的Overlay网络完成,如监控自动纳管、服务自动发现等功能。所以,在这类场景中,会使用容器多网卡方案,将流量隔离,监控流量和备份用一张网卡,业务流量用一张网卡。当然对性能要求高的,就得使用SR-IOV的那张网卡。
SR-IOV局限性
使用SR-IOV直通方案的优点是最优的网络性能,但是由于SR_IOV技术的硬件虚拟化特性,一张万兆网卡上的虚拟网卡(VF)数量是有限的,一般为63个,一些高端网卡是126个,也就是说一台机器上使用SR-IOV网卡的pod数量有限,所以得省着点用!解决方案:
- 一般业务,不使用SR-IOV网卡;
- 对于网络性能有一定要求,但要求不是很高的,例如低配版数据库实例,可以使用macvlan或者ipvlan的网卡(之前的文章有例子);
- 对网络性能要求较高的业务实(例如高配版数据库实例)例才可使用SR-IOV网卡。
SR-IOV设备数据包分发机制
其实,从逻辑上可以认为启用了 SR-IOV 技术后的物理网卡内置了一个特别的 Switch,将所有的 PF 和 VF 端口连接起来,通过 VF 和 PF 的 MAC 地址以及 VLAN ID 来进行数据包分发。
- 在 Ingress 上(从外部进入网卡):如果数据包的目的MAC地址和VLANID都匹配某一个VF,那么数据包会分发到该VF,否则数据包会进入PF;如果数据包的目的MAC地址是广播地址,那么数据包会在同一个 VLAN 内广播,所有 VLAN ID 一致的 VF 都会收到该数据包。
- 在 Egress 上(从 PF 或者 VF发出):如果数据包的MAC地址不匹配同一VLAN内的任何端口(VF或PF),那么数据包会向网卡外部转发,否则会直接在内部转发给对应的端口;如果数据包的 MAC 地址为广播地址,那么数据包会在同一个 VLAN 内以及向网卡外部广播。
注意:所有未设置 VLAN ID 的 VF 和 PF,可以认为是在同一个 LAN 中,不带 VLAN 的数据包在该 LAN 中按照上述规则进行处理。此外,设置了 VLAN 的 VF,发出数据包时,会自动给数据包加上 VLAN,在接收到数据包时,可以设置是否由硬件剥离 VLAN 头部。
SR-IOV设备与容器网络
英特尔推出了 SR-IOV CNI 插件,支持 Kubernetes pod 在两种模式任意之一的条件下直接连接 SR-IOV 虚拟功能 (VF)。第一个模式在容器主机核心中使用标准 SR-IOV VF 驱动程序。第二个模式支持在用户空间执行 VF 驱动程序和网络协议的 DPDK VNF。本文介绍的是第一个模式,直接连接SR-IOV虚拟功能(vf设备),如下图所示:
上图中包含了一个node节点上使用的组件:kubelet、sriov-device-plugin、sriov-cni和multus-cni。节点上的vf设备需要提前生成,然后由sriov-device-plugin将vf设备发布到k8s集群中。在pod创建的时候,由kubelet调用multus-cni,multus-cni分别调用默认cni和sriov-cni插件为pod构建网络环境。sriov-cni就是将主机上的vf设备添加进容器的网络命名空间中并配置ip地址。
SR-IOV网络实践
1. SR-IOV设备使用
查找宿主机上是否有支持SR-IOV的智能网卡
代码语言:javascript复制# 插件所有的pci设备
[root@xxx ~]# lspci -nn | grep Eth
1a:00.2 Ethernet controller [0200]: Intel Corporation Ethernet Connection X722 for 1GbE [8086:37d1] (rev 09)
1a:00.3 Ethernet controller [0200]: Intel Corporation Ethernet Connection X722 for 1GbE [8086:37d1] (rev 09)
3b:00.0 Ethernet controller [0200]: Intel Corporation 82599ES 10-Gigabit SFI/SFP Network Connection [8086:10fb] (rev 01)
3b:00.1 Ethernet controller [0200]: Intel Corporation 82599ES 10-Gigabit SFI/SFP Network Connection [8086:10fb] (rev 01)
3d:00.0 Ethernet controller [0200]: Intel Corporation 82599ES 10-Gigabit SFI/SFP Network Connection [8086:10fb] (rev 01)
3d:00.1 Ethernet controller [0200]: Intel Corporation 82599ES 10-Gigabit SFI/SFP Network Connection [8086:10fb] (rev 01)
86:00.0 Ethernet controller [0200]: Intel Corporation 82599ES 10-Gigabit SFI/SFP Network Connection [8086:10fb] (rev 01)
86:00.1 Ethernet controller [0200]: Intel Corporation 82599ES 10-Gigabit SFI/SFP Network Connection [8086:10fb] (rev 01)
af:00.0 Ethernet controller [0200]: Intel Corporation 82599ES 10-Gigabit SFI/SFP Network Connection [8086:10fb] (rev 01)
af:00.1 Ethernet controller [0200]: Intel Corporation 82599ES 10-Gigabit SFI/SFP Network Connection [8086:10fb] (rev 01)
# 查看设备1a:00.2具备的能力
[root@WXBY01-IRONIC-ZTE02 ~]# lspci -s 1a:00.2 -vvv | grep Capabilities
Capabilities: [40] Power Management version 3
Capabilities: [50] MSI: Enable- Count=1/1 Maskable 64bit
Capabilities: [70] MSI-X: Enable Count=129 Masked-
Capabilities: [a0] Express (v2) Endpoint, MSI 00
Capabilities: [e0] Vital Product Data
Capabilities: [100 v2] Advanced Error Reporting
Capabilities: [150 v1] Alternative Routing-ID Interpretation (ARI)
Capabilities: [160 v1] Single Root I/O Virtualization (SR-IOV) !!!!!有这行说明网卡支持SRIOV
Capabilities: [1a0 v1] Transaction Processing Hints
Capabilities: [1b0 v1] Access Control Services
2. sriov-device-plugin使用
sriov-device-plugin原理
SR-IOV设备的pf资源和vf资源需要发布到k8s集群中以供pod使用,所以这边需要用到device-plugin,device-plugin的pod是用daemonset部署的,运行在每个node节点上,节点上的kubelet服务会通过grpc方式调用device-plugin里的ListAndWatch接口获取节点上的所有SR-IOV设备device信息,device-plugin也会通过register方法向kubelet注册自己的服务,当kubelet需要为pod分配SR-IOV设备时,会调用device-plugin的Allocate方法,传入deviceId,获取设备的详细信息。
sriov-device-plugin的部署
a. 新建configmap
后面部署的daemonset需要使用configmap,主要是用于筛选节点上的SR-IOV的vf设备。resourceList里面装的每一个对象都是用于搜索节点上的vf设备的筛选条件,比如selectors.vendors是厂商号,selectors.devices是设备号,selectors.drivers是驱动信息,device-plugin的pod就会在本地查询满足这几个信息的设备,kubelet会调ListAndWatch接口获取这些设备信息
代码语言:javascript复制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", "1889"],
"drivers": ["i40evf", "iavf", "ixgbevf"]
}
},
{
"resourceName": "intel_sriov_dpdk",
"selectors": {
"vendors": ["8086"],
"devices": ["154c", "10ed", "1889"],
"drivers": ["vfio-pci"],
"pfNames": ["enp0s0f0","enp2s2f1"]
}
},
{
"resourceName": "mlnx_sriov_rdma",
"selectors": {
"vendors": ["15b3"],
"devices": ["1018"],
"drivers": ["mlx5_ib"],
"isRdma": true
}
}
]
}
b. 确定设备的厂商号、设备号和驱动信息
选择一张SR-IOV设备,生成vf设备:
代码语言:javascript复制[root@xxx ~]# echo 4 > /sys/bus/pci/devices/0000:1a:00.2/sriov_numvfs
查看厂商号和设备号是8086和37cd
代码语言:javascript复制[root@xxx ~]# lspci -n | grep 1b:0a.0
1b:0a.0 0200: 8086:37cd (rev 09)
查看vf设备的驱动信息是i40evf
代码语言:javascript复制[root@xxx ~]# ls -l /sys/class/pci_bus/0000:1b/device/0000:1b:0a.0 | grep driver
lrwxrwxrwx 1 root root 0 4月 24 17:01 driver -> ../../../../../../bus/pci/drivers/i40evf
-rw-r--r-- 1 root root 4096 4月 24 17:56 driver_override
c. 新建daemonset
代码语言:javascript复制wget https://raw.githubusercontent.com/k8snetworkplumbingwg/sriov-network-device-plugin/master/deployments/k8s-v1.16/sriovdp-daemonset.yaml
kubectl apply -f sriovdp-daemonset.yaml
过三分钟查看node对象是否存在sriov的资源:
代码语言:javascript复制kubectl describe node zhounanjun
Allocatable:
cpu: 47
ephemeral-storage: 320296325k
hugepages-1Gi: 0
hugepages-2Mi: 0
intel.com/intel_sriov_netdevice: 4 ## 有这项说明插件生效了
memory: 390116500Ki
pods:
3. sriov-cni使用
SR-IOV设备在k8s中专门用作pod的网卡,所以intel也提供了sriov-cni插件,该插件满足k8s cni规范,可以作为cni插件使用,为pod配置网络环境。
部署sriov-cni:
代码语言:javascript复制wget https://raw.githubusercontent.com/k8snetworkplumbingwg/sriov-cni/master/images/k8s-v1.16/sriov-cni-daemonset.yaml
kubectl apply -f sriov-cni-daemonset.yaml
sriov-cni主要做的事情:
- 首先sriov-cni部署后,会在/opt/cni/bin目录下放一个sriov的可执行文件。
- 然后,当kubelet会调用multus-cni插件,然后multus-cni插件里会调用delegates数组里的插件,delegates数组中会有SR-IOV信息,然后通过执行/opt/cni/bin/sriov命令为容器构建网络环境,这边构建的网络环境的工作有:
- 根据kubelet分配的sriov设备id找到设备,并将其添加到容器的网络命名空间中
- 为该设备添加ip地址
4. network-attachment-definition使用
需要定义network-attachment-definition对象才能使用sriov设备:
代码语言:javascript复制apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
name: sriov-net2
annotations:
k8s.v1.cni.cncf.io/resourceName: intel.com/intel_sriov_netdevice
spec:
config: '{
"cniVersion": "0.3.1",
"name": "sriov-network",
"plugins": [
{
"type": "sriov",
"ipam": {
"type": "host-local", # 使用host-local作为ipam插件
"subnet": "192.168.133.0/24", # 设置子网
"rangeStart": "192.168.133.150",# 设置可分配的ip段
"rangeEnd": "192.168.133.189",
"routes": [
{ "dst": "0.0.0.0/0" }
],
"gateway": "192.168.133.254"
}
},
{
"type": "sbr" #设置sbr
}
]
}'
5. 创建测试pod
代码语言:javascript复制apiVersion: v1
kind: Pod
metadata:
name: pod5
labels:
environment: production
app: MyApp
annotations:
k8s.v1.cni.cncf.io/networks: '[
{ "name" : "sriov-net2",
"interface": "sriovnet2" }
]'
spec:
containers:
- name: appcntr1
image: busybox:latest
imagePullPolicy: IfNotPresent
command:
- tail
- -f
- /dev/null
resources:
requests:
intel.com/intel_sriov_netdevice: '1'
limits:
intel.com/intel_sriov_netdevice: '1'
进入pod查看网络:
代码语言:javascript复制[root@xxx]# kubectl get po
NAME READY STATUS RESTARTS AGE
pod5 1/1 Running 0 35h
[root@xxx]# kubectl exec -ti pod5 -- ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
310: eth0@if311: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1300 qdisc noqueue
link/ether 00:00:00:27:9e:92 brd ff:ff:ff:ff:ff:ff
inet 10.222.0.32/16 brd 10.222.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fb11:10:16::12/64 scope global
valid_lft forever preferred_lft forever
inet6 fe80::200:ff:fe27:9e92/64 scope link
valid_lft forever preferred_lft forever
66: sriovnet2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq qlen 1000
link/ether 42:7a:dc:a8:ff:9f brd ff:ff:ff:ff:ff:ff
inet 192.168.133.150/24 brd 192.168.133.255 scope global sriovnet2
valid_lft forever preferred_lft forever
inet6 fe80::407a:dcff:fea8:ff9f/64 scope link
可以看到pod中有三张网卡:lo,eth0和sriovnet2。其中sriobnet2网卡是sriov网络的网卡。
SR-IOV优化实践
优化ipam
上面我们在创建nad对象时,ipam插件使用的是host-local,host-local的话,只能做单节点的ip分配,当时用SR-IOV设备的pod分布在不同的节点上时,节点上的host-local会给予nad中的subnet进行分配,不会和其他节点上的host-local沟通,这会导致不同节点上的pod的SR-IOV网卡的ip地址存在冲突的可能。这个问题的根源是缺少集中式ip地址分配的机制。k8snetworkplumbingwg社区提供了whereabouts插件是专门解决这个问题,whereabouts插件就是一个集中式的ip地址管理插件。
a. whereabouts部署和使用
代码语言:javascript复制git clone https://github.com/k8snetworkplumbingwg/whereabouts && cd whereabouts
kubectl apply
-f doc/crds/daemonset-install.yaml
-f doc/crds/whereabouts.cni.cncf.io_ippools.yaml
-f doc/crds/whereabouts.cni.cncf.io_overlappingrangeipreservations.yaml
-f doc/crds/ip-reconciler-job.yaml
部署完成后,可以在/opt/cni/bin目录下看到whereabouts的可执行文件,这个可执行文件是在sriov-cni中需要给容器分配ip时被调用。
使用whereabouts作为ip地址分配插件:
代码语言:javascript复制apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
name: sriov-net2
annotations:
k8s.v1.cni.cncf.io/resourceName: intel.com/intel_sriov_netdevice
spec:
config: '{
"cniVersion":"0.3.1",
"name":"nsnetwork-sample",
"plugins":[
{
"ipam":{
"gateway":"192.168.6.254",
"range":"192.168.6.0/24",
"type":"whereabouts" # 使用whereabouts
},
"type":"sriov"
},
{
"type":"sbr"
}
]
}'
b. whereabouts说明
- 在使用whereabouts时,会传入ipam的配置,然后获取集群中查找以192.168.6.0/24命名的ippool对象,若是没有,就创建ippools对象
- whereabouts分配IP时,使用了领导者选举机制来获取分布式锁,然后获取ippool对象,ippool对象中会存在着已使用ip和对应的pod,然后从剩下的可分配ip中找一个作为分配结果,分配完成后更新ippool对象中已使用的ip地址列表。
c. 遇到的问题
若是nad对象中定义的子网信息和宿主机不在一个网段,那么不同主机上的pod是无法通信的,所以若是想pod做跨主机通信,必须将子网信息设置成宿主机网段,需要在使用前规划好网段。
由于笔者时间、视野、认知有限,本文难免出现错误、疏漏等问题,期待各位读者朋友、业界专家指正交流。
周宇:云原生研发工程师,有多年容器及K8s产品架构设计开发经历,具有丰富的harbor、容器网络等方向研发经验。