基于K8s的SR-IOV网络实践

2022-05-25 10:07:33 浏览数 (1)

在传统的虚拟化中,虚拟机的网卡通常是通过桥接(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、容器网络等方向研发经验。

0 人点赞