R86S 万兆版提供了一个万兆网卡 CX341,而这款网卡有一个 SR-IOV 功能,这个功能可以实现(建议)硬件交换机,可以用来替换虚拟机和宿主机之间的软网桥,能大幅提升性能。

SR-IOV 是什么

SR-IOV 是 Single Root I/O Virtualization 的缩写,是网卡的一种虚拟化支持技术。它可以每让一个物理网口(PF)虚拟出多个网口(VF),这些 VF 都能于对应访问对应 PF 连接的设备。VF 之间, VF 和 PF 之间也能互相通讯。也就是说这时候就相当于虚拟了一个交换机。这些 VF 能直通给虚拟机,然后虚拟机之间能通过这个虚拟交换机通讯。由于这个虚拟交换机是内置在网卡硬件上的,所以它对比软网桥性能和带宽更好(注意,PF 本身是有速率设置的,比如可以设置 10G 或者 1G 等,但是这个并不影响网桥的转发速率,它始终都是以最高速率转发,即 10G 速率)。

启用 CX341 的 SR-IOV 功能

首先需要先安装网卡管理工具。

apt update && apt install mstflint

然后找到网卡设备 ID

lspci

找到 Ethernet controller: Mellanox Technologies MT27500 Family [ConnectX-3] 那一行,前面那串东西就是设备 ID 了,我这里是 05:00.0,后面的命令自行根据自己替换这串东西。
然后,需要保证网卡的固件版本足够高。

mstflint -d 05:00.00 query

现在的最新版是 2.42.5000。如果太低,可以去 https://network.nvidia.com/support/firmware/connectx3en/ 下载最新固件。XCGN 或者 XCCN 自己根据需求选择。
下载之后,使用命令

mstflint -d 05:00.00 -i 固件路径 b

刷入固件并重启。
保证固件最新之后,通过

mstconfig -d 05:00.00 set SR_IOV=1 NUM_OF_VFS=8

其中 8 是 VF 的数量,这里可以按照自己的需求修改。
固件开启之后,还需要在驱动开启 SR-IOV。新建文件 /etc/modprobe.d/mlx4_core.conf,内容如下:

options mlx4_core port_type_array=2,2 num_vfs=8 probe_vf=8

这里说明一下参数的意思。

  1. port_type_array=2,2 这个不要改。意思是两个网口的类型分别是 2 和 2。2 即以太网口。
  2. num_vfs 标识 VF 数量,格式为 a,b,c。其中 a 表示第一个网口的 vf 数量,b 表示第二个网口的 vf 数量,c 表示两个网口的 vf 数量。特别说明一下,这里两个网口 vf 数量不是说 a+b=c。这个配置生效之后,系统会创建 a+b+c 个虚拟 PCI 设备。创建的前 a 个设备直通其中一个到虚拟机后,里面会显示一个网口,这个网口能和第一个 pf 及其 vf 通讯。接着的 b 个设备同理,但是是第二个 pf 的。而剩下的 c 个设备,直通其中一个到虚拟机后,虚拟机能看到两个网口,这两个网口分别对应到两个 pf 的。如果只有一个数字,则跟 0,0,c 一样。也就是说,num_vfs=8num_vfs=0,0,8 一样。
  3. probe_vf 格式和 num_vfs 一样,这个表示在宿主机 ip link 显示的网卡的数量。如果 vf 并不准备给宿主机使用(毕竟宿主机可以直接使用 pf)则可以写成 0。

创建完这个文件(以及后面每次更新这个文件)后,运行一下命令应用修改:

update-initramfs -u

配置 VF

创建 SR-IOV 之后,重启机子之后,运行

ip link show

之后,应该能看到有两个网口(就是两个 pf)下面多出了一串 vf。这里说明一下,无论怎么配置 num_vfs,ip link 都会显示这两个网口的数量一样,并且都是 a+b+c 这么多个。但是我们知道,第一个网口应该只有 a+c 个 vf,而第二个只有 b+c 个。这是因为其中的有些是占位符,并不是真正的 vf。所以,对第一个 pf 而言,0 到 a 和 a+b 到 a+b+c 的 vf 才能用,对第二个而言只有 a 到 a+b+c 的 vf 才能用。
另外,我们可以看到 vf 的 mac 地址都是 0。如果都是 0,即直通虚拟机之后,它会自动随机分配 mac 地址。如果设置了,那么这个 mac 地址可以进入到虚拟机里面。下面来说一下 vf 的配置。
实例配置命令如下:

ip link set enp5s0 vf 0 mac f4:52:14:d9:81:a0 spoofchk on state enable vlan 41

这里设置第一个网口 enp5s0 的第 0 个 vf 的 mac 地址为 f4:52:14:d9:81:a0,并且打开了 spoofchk,state 设置成 enable,vlan 是 41。

  1. mac 大家都懂这里就不赘述了,注意不要设置成组播的 mac 就行。
  2. spoofchk 是指是否开启 mac 地址欺骗检查,默认关闭。如果打开之后,这个 vf 只能只能发出源 mac 地址为设置值的包。如果关掉,那这个 vf 什么源 mac 地址的包都能发出。这里建议关闭,没必要打开。
  3. state 表示状态,可以设置三种:auto, enable, disable。默认是 auto,表示只有 pf 启用(也就是说物理网口真的连上了模块)时候,对应的 vf 才能用。enable 表示无论 pf 是否启用(其实啥都没插)vf 也是启用状态(表现为看起来始终都插着模块)。disable 表示禁用这个 vf (始终显示没有插入模块)。
    这里补充一点,笔者测试时候,发现 enable 之后,即使链接信息显示正常,接口也成功启用,但是通讯会出现错误,具体在 dmesg 会显示 ``。应该是网卡本身的缺陷,无法解决。所以只能在物理口上始终插入一个设备了。
  4. vlan,大家都懂。要注意的是,这个 vlan 是硬件帮你 tag 的。也就是说 untag 的数据包进入 vf 会自动给 tag 上然后传到其他 vf 或者 pf,而这个 tag 的数据包进来会自动 untag。vlan 似乎是可以设置多个值,但是配置比较麻烦这里建议用多个 vf 来实现。既然是硬件 tag,那这个 tag 效率肯定是比软 vlan 要高的,这个是可以用来实现 PPPoE 拨号等的。

这里根据自己的实际拓扑进行配置。可以把配置命令放到 /etc/network/if-pre-up.d/vf 文件里,这会在网络接口(所有)启动时候自动运行。记得 chmod +x /etc/network/if-pre-up.d/vf

直通 VF

把 VF 弄出来之后就需要直通给虚拟机了。直通很简单,只需要在内核参数里面打开就可以了。编辑 /etc/default/grub,把里面的 GRUB_CMDLINE_LINUX_DEFAULT="quiet" 替换成 GRUB_CMDLINE_LINUX_DEFAULT="quiet iommu=pt intel_iommu=on" 即可。其实就是加入iommu=ptintel_iommu=on(用空格分开,需要放在引号内)。
然后应用修改:

update-grub

重启之后,在 PVE 里面就可以添加网卡直通了。刚才记下的网卡设备 ID 会出现多个,它们点后面的数字不同,比如我这里会有 05:00.01 这样的网卡,他就是第一个 vf 啦。对应前面 ip link 的 vf 顺序我们可以知道它对应几个和哪个物理网口了。

VF 放虚拟网桥

一般来说,我们创建 vf 之后,两个网口一个是 wan 一个是 lan 了。lan 口出来接个光电交换机就能联通更多设备。然而,r86s 上面会还有三个 2.5G 的口,这些口有时候也是作为 lan 使用的,这时候需要把光口和电口桥接起来了。直通之后,就是说我们要把 vf 和另外两个电口软桥接起来。而 r86s 里面同样,我们一般还会弄一个电口的管理口,管理口需要同时能访问 r86s 和光口的设备,这时候也需要桥接。但是如果我们直接把 vf 放在虚拟网桥里面(比如在 Openwrt 上,把直通后的 vf 加入 br-lan 中)之后,pf 和其他 vf 并不能访问 br-lan 的 IP。而且还有一个神奇的现象,就是 pf 和其他 vf 能通过 DHCP 获取 IP。

这是为什么呢?

这时候需要我们回顾小学学过的二层交换机的原理。跟集线器不一样,二层交换机一个很重要的特性就是 mac 地址学习。就是说,mac 地址会通过 ARP 或者收到的包来学习哪些 mac 地址对应到哪个网口,然后能把数据包发到那个网口上,这时候其他网口就收不到不属于自己的包,减少了网络负担,也减少了干扰(就是不用 CSMA/CD 了,网线可以更长,带宽可以更高)。
但是,这个网卡 SR-IOV 的 PF 和 VF 之间并不是一个真正的二层交换机,但也不是一个集线器,而是一个半残废的交换机。具体来说,它没有 mac 地址学习功能,它的 mac 地址表是静态的,这个表就是记录我们给 vf 设置的 mac 地址(ip link 设置的)。那么,在关闭 spoofchk 的时候,我们能发送以另一个 mac 地址为源的包出去,但是,由于没有 mac 地址学习功能,当收到这个 mac 地址为目标的包的时候,网卡内部交换机并不知道应该发到哪个 vf 上,而是直接发到 物理口(注意这里物理口和 PF 不一样,物理口上的物理设备能收到包,但是系统内 PF 接口也是收不到的)上,所以这时候 vf 就收不到响应包了。而由于网桥一般有自己的 mac,所以这时候网卡并不知道这个 mac 的目的地是这个 vf,所以就收不到响应包,所以就不能通讯啦。由于 DHCP 请求是广播包,所有 vf 都能收到,br-lan 自然可以响应,包也能正常发出,所以客户端能获取到 IP 地址。但是真正访问 br-lan 的时候,过了一次 ARP (ARP 也是广播)之后,就有明确 mac 地址了,而这时候网卡不会转发这个到 br-lan 的 vf 上,自然也就不能通讯了。

那么如何解决这个问题呢,最简单的办法就是把 br-lan 的 mac 设置成 vf 设置的 mac,这时候,vf 就能接收到目标是 br-lan 的包了。

但是,
要注意的是,这样设置之后,只能解决 vf 下联设备访问 br-lan 的问题,并不能解决 br-lan 其他下联设备访问 vf 下联设备。因为 vf 下联设备访问 br-lan 下联设备时候,mac 不是 br-lan 本身,所以还是会转到 pf 上。

那么怎么解决这个问题呢?我们最多只能给 vf 设置一个 mac 地址,不可能把所有下联设备的 mac 都设置成 vf 的地址,也不能给 vf 设置多个 mac 地址。

然而,
vf 确实能设置多个 mac 地址。以下命令

bridge fdb add f4:52:14:d9:81:a0 self permanent dev enp5s0v0

就能让 enp5s0v0 这个 vf 接受目标 mac 地址为 f4:52:14:d9:81:a0 的数据包了。
也就是说,即使我不把 br-lan 的 mac 地址设置成 vf 的地址,我只需要用上面命令把 br-lan 的 mac 地址添加到桥接到 br-lan 的 vf 上,就能让 vf 也接受目标是 br-lan 的包,然后 br-lan 就能正确接收到包了。

另外,官网上的文档说 vf/pf 能设置混杂模式,设置方法为

ifconfig enp5s0v0 promisc

这样就能给 enp5s0v0 这个 pf/vf 开启混杂模式。开启混杂模式之后,无论目标 mac 地址是否匹配这个 pf/vf,都会把包转发过来。然而实测并没用,应该和 state enable 一样是网卡本身的缺陷,这里不讨论这个。

上面能让 vf 接受网桥自身发出的包还没完,网桥还会连接其他设备,以其他设备的包的 mac 为目标的怎么办呢?我们没办法枚举所有 mac 地址并且一个一个加入进去。这时候,我们要监听网桥加入的设备,然后当有新设备接入网桥(被网桥的 mac 学习功能学习到了)的话,就添加进去。当被移除的话,就添加进去。监听我们可以利用 bridge monitor 功能来监听,每当加入或者删除 mac 地址时候,都会输出一行日志,我们解析日志,把 mac 和网桥名字提取出来,然后扫描网桥下的设备,根据驱动判断是否是 vf/pf,然后用命令添加网桥。我写了一个脚本:

#/bin/bash
reg='(([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2})[ ]+[^ ]+[ ]+[^ ]+[ ]+master[ ]+([^ ]+)'

for if in $(ls /sys/class/net); do
    if ! [ -e /sys/class/net/${if}/device/driver ]; then continue; fi
    driver=$(basename $(readlink /sys/class/net/${if}/device/driver))
    if [ "$driver" = "mlx4_core" ] && [ -d /sys/class/net/${if}/brport ]; then
        mac=$(cat /sys/class/net/${if}/brport/bridge/address)
        echo "bridge fdb add ${mac} permanent self dev ${if}"
        bridge fdb add ${mac} permanent self dev ${if}
    fi
done

mac_add() {
    mac=$1
    br=$2
    echo "mac ${mac} added to br ${br}"
    ifs=($(ls /sys/class/net/${br}/brif))
    for if in ${ifs[@]}; do
        driver=$(basename $(readlink /sys/class/net/${if}/device/driver))
        if [ "$driver" = "mlx4_core" ]; then
            echo "bridge fdb add ${mac} permanent self dev ${if}"
            bridge fdb add ${mac} permanent self dev ${if}
        fi
    done
}

mac_del() {
    mac=$1
    br=$2
    echo "mac ${mac} deleted from br ${br}"
    ifs=($(ls /sys/class/net/${br}/brif))
    for if in ${ifs[@]}; do
        driver=$(basename $(readlink /sys/class/net/${if}/device/driver))
        if [ "$driver" = "mlx4_core" ]; then
            echo "bridge fdb delete ${mac} permanent self dev ${if}"
            bridge fdb delete ${mac} permanent self dev ${if}
        fi
    done
}

bridge monitor | while read -r line;
do
    arr1=($(echo "$line" | sed -nr "s/^$reg$/\1 \3/p"))
    arr2=($(echo "$line" | sed -nr "s/^Deleted $reg$/\1 \3/p"))
    if [ ${#arr1[@]} -eq 2 ]; then
        mac_add ${arr1[0]} ${arr1[1]}
    elif [ ${#arr2[@]} -eq 2 ]; then
        mac_del ${arr2[0]} ${arr2[1]}
    fi
done

就是完成这个工作的。另外还有 C++ 写的版本,并且我预先编译了一个静态版本,可以放 PVE 或者 openwrt 上都是能直接运行的。直接设置开机自动运行这个程序就能非常自动添加 mac 到 VF/PF 上了。

当然,我这里是在 PVE 上测试的,因为我希望 PVE 上的 Openwrt 死了我也能访问 PVE。这时候我需要把电口桥接到 PVE 的 vmbr 上,所以我的桥接其实是在 PVE 上完成的(vmbr 桥接了所有电口以及两个物理口的其中一个 VF)。因此这个脚本是在 PVE 上跑的。如果有时间,我会写一个 C 语言版本的,到时候就不用这么麻烦的解析了。不过也是有时间再说啦。

硬 VLAN 拨号

TODO

20G 端口聚合

TODO

发表新评论

© 2022 Powered by Typecho & Theme Quark
粤ICP备17055048号