#0129 性能

测试环境

本周的任务主要是将 xdp 负载均衡程序上线到物理测试环境并测试其性能。

测试环境:

  • 系统:CentOS 7。

  • 内核:4.14.69-sn1.el7.centos.x86_64。

  • 网卡:Intel 82599ES 万兆(driver:ixgbe)、双口,4 网卡做 bond,出入口带宽 40GB/s。

性能方面主要测试的是 director 模块。

其它方面考虑到后面线上环境内核升级会比较慢,所以后续 redirector 模块短期内会复用 glb-redirect。

线上编译?从尝试到放弃

线上系统的内核版本低,各种工具链的版本也比较低,编译 bpf 需要依赖高版本的 clang/llvm,尝试升级会发现一连串的依赖都需要升级,cmake、gcc,……,太复杂,果断选择在高版本系统上编译,然后将可执行文件拖到线上环境直接跑。

另外 ebpf.RewriteConstants 在低版本系统上不可使用,配置传递还得使用 Map。

bond 网口和 xdp

在 4.14 内核上,xdp 程序挂载到 bond 网口不起作用,需要挂载到其下所有的物理网口上。

错配的路由导致入包被丢

调试 director 和 glb-redirect 内核模块的时候遇到个奇怪的问题,tcpdump 看到转发的包到目标机器目标网卡了,格式也正确,但是包在到达 glb-redirect 模块之前被丢了,iptables 中除了 glb-redirect 规则外没有其它规则。

给 iptables glb-redirect 规则前的 表 / 链 加上 LOG,可以看到在 PREROUTING 阶段能够看到包,但之后就没见了。

iptables -t raw  -A PREROUTING -p udp --dport 19523 -j LOG
# iptables -t raw -vL 通过前面的 pkts 计数也可以定位包是在哪个阶段丢的
# ...

检查路由规则发现,路由表中有以下路由规则(误配的):

172.18.208.71   0.0.0.0         255.255.255.255 UH    0      0        0 lo

转发过来的包的源 IP 也是 172.18.208.71,虽然包是在入口被丢不是在出口被丢,但是这个很可疑,先删除,再试,没问题了!看来,确实是这条错误的规则导致包被丢了。

搜索后发现,Linux 有这么一个配置:

https://superuser.com/questions/426682/linux-drops-packet-when-trying-to-route-it-why

$ sysctl net.ipv4.conf.bond0.rp_filter
1

当被设置为 1 的时候,内核会检查入包源地址的出口路由对应的网口,如果和入包的网口不一致则丢弃包。

rp_filter - INTEGER

1 - Strict mode as defined in RFC3704 Strict Reverse Path

Each incoming packet is tested against the FIB and if the interface is not the best reverse path the packet check will fail. By default failed packets are discarded.

https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

使用 wireshark 命令行工具 dump 网络包

调试的时候如果需要解析网络包的情况,使用 wireshark 比 tcpdump 会是个更好的工具。

安装:

# yum install -y wireshark

常用命令:

# 同时捕获多网卡
tshark -i eth0 -i eth1
# dump 出详细的网络包字段信息
tshark -i eth0 -V

影响 xdp 性能的两个点

第一,确保 xdp 程序加载使用的是 driver 模式,如下,第二列显示的就是加载模式。

$ bpftool net
xdp:
p2p1(4) driver id 101
p2p2(5) driver id 101
p1p1(6) driver id 101
p1p2(7) driver id 101
bond0(8) generic id 20

tc:

flow_dissector:

第二,确保 xdp 程序加载的时候开启了 jit,开和不开 jit 的性能差别非常大,几十倍,不开 jit 的 xdp 跟个弱鸡似的。

$ bpftool prog
8: xdp  tag 8c806e4dbe1cf688
    xlated 21856B  not jited  memlock 24576B
20: xdp  tag e663805a60a750bc
    xlated 14960B  not jited  memlock 16384B
101: xdp  tag 760afb84c3168504
    xlated 9456B  not jited  memlock 12288B

如果系统没有开启 jit,可以通过下面的命令开启:

$ echo 1 > /proc/sys/net/core/bpf_jit_enable

开启后需要重新加载下 xdp 程序。


如果考虑牺牲一点安全的话,也可以修改哈希算法为更简单 “but much faster” 的 jhash 或者 half siphash。详细可参见:

xdp 在内核网络栈中执行的位置

4.14 内核:

net_rx_action()
|- napi_poll()
   |- ixgbe_poll()
      |- ixgbe_clean_rx_irq()
         |- ixgbe_run_xdp()
         |  |- bpf_prog_run_xdp ⬅ driver 模式
         |  ...
         |- 创建 skb 结构体
         |  ...
         |- ixgbe_rx_skb
            |- napi_gro_receive
               |- dev_gro_receive
               |- napi_skb_finish
                  |- netif_receive_skb_internal
                     |- do_xdp_generic
                     |  |- netif_receive_generic_xdp
                     |     |- bpf_prog_run_xdp ⬅ generic 模式
                     |- __netif_receive_skb
                       |- __netif_receive_skb_core
                          |- sch_handle_ingress
                             |- tcf_classify
                                |- tp->classify/cls_bpf_classify
                                   |- BPF_PROG_RUN ⬅ tc-bpf ingress hook

generic 模式相比 driver 模式除了执行点靠后之外,还有一个影响性能的点: generic 模式会跳过 gro 。gro 模块一开始会通过 netif_elide_gro 判断是否执行 gro,如果设备上有 generic xdp 程序,也会跳过执行。

static inline bool netif_elide_gro(const struct net_device *dev)
{
    if (!(dev->features & NETIF_F_GRO) || dev->xdp_prog)
        return true;
    return false;
}

tc-bpf 看到的是 gro 之后的 skb。

内核协议栈详细的执行过程可以参见:https://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data/

如何修改 xdp 程序的加载模式

使用 netlink.LinkSetXdpFdWithFlags 接口,通过最后一个参数 flags 控制。

#define XDP_FLAGS_UPDATE_IF_NOEXIST (1U << 0)
#define XDP_FLAGS_SKB_MODE          (1U << 1)
#define XDP_FLAGS_DRV_MODE          (1U << 2)
#define XDP_FLAGS_HW_MODE           (1U << 3)
// https://elixir.bootlin.com/linux/v4.14/source/include/uapi/linux/if_link.h#L892

内核中对应的处理函数:https://elixir.bootlin.com/linux/v4.14/source/net/core/dev.c#L7038

注意,如果要切换加载模式,需要卸载 XDP 后重新加载,使用 XDP_FLAGS_UPDATE_IF_NOEXIST 是无效的,这个参数只对同一模式有用。

如何获取 xdp 程序的加载模式

加载模式不是通过 xdp 属性里的 flag 字段获取的,而是通过 attached 的值来判断的。详细见:https://github.com/torvalds/linux/blob/master/tools/bpf/bpftool/netlink_dumper.c#:~:text=static%20int%20do_xdp_dump_one

go 的 netlink 库需要使用 1.2.0-beta 以上版本才有获取加载模式的功能。

link, _ := netlink.LinkByName(ifaceName)
link.Attrs().Xdp.AttachMode
// 加载模式:
//   XDP_ATTACHED_NONE
//   XDP_ATTACHED_DRV
//   XDP_ATTACHED_SKB
//   XDP_ATTACHED_HW

压测工具

wrk

pktgen

性能指标

$ dstat --bits -cnm --net-packets

xdp 程序是在 ksoftirqd 中执行的,因此查看软中断的 CPU 消耗即是 xdp 程序的 CPU 消耗(即 dstat 中的 siq 列)。