硬件Bonding卸载场景下的ARP/ND双发
大约在2019年的时候,公司的服务器接入网络架构开始向双上联去堆叠方向迁移,相比于之前老的接入网络而言,新的网络架构在各方面的提升都非常明显,尤其是在带宽利用率和冗余性方面,关于网络架构的部分,这里暂时就不多做介绍了,具体的可以参考京东以及H3C的相关分享和文档:异构去堆叠 | 一种完美提升网络高可用SLA的方案 ,H3C数据中心交换机S-MLAG最佳实践。
在新的网络架构中,我们的方案是通过ARP转主机路由方式来实现网络层面的负载均衡和高可用的,这个方案有个依赖,需要主机实现ARP/ND相关协议包的双发。
Bonding的双发问题
为什么会有这个双发的需求呢?因为在我们选择的去堆叠方案中,主机的网卡是通过Bonding的方式来实现双上联的,要想实现对负载均衡和高可用的同时支持,Bonding的流量分载只能是基于hash的模式,这会带来一个问题:对于Host侧的ARP请求/响应报文,只会发送给某一台交换机,假设Host连接了LeafA和leafB两台交换机,当LeafA交换机的ARP表项即将过期,需要发送ARP请求探测Host状态时,Host回复的响应报文可能因为hash原因回复给LeafB了,这样一来,LeafA就无法及时获取到Host的最新状态信息,从而影响到整个网络的稳定性和可靠性。
因为上面的这个原因,Host侧内核的Bonding模块必须作出相应的调整,将所有ARP的请求和响应报文,都进行复制,并在所有的子接口上进行发送。当然实现的方法并不麻烦,具体的可以参考龙蜥社区的实现anolis: bond: broadcast ARP or ND messages to all slaves。
至此就基本解决了物理服务器层面对新接入网络架构融合的问题。
虚拟化的网卡Bonding
作为第一个接入新网络架构的虚拟化平台,只解决物理服务器层面的问题还不够,因为虚拟化的核心产品:VM的网络也需要解决。对于虚拟化业务的网卡,选择的都是Mellanox(现NVIDIA)的产品,不得不说,Mellanox的网卡对于虚拟化的场景来说,还是非常友好的,相比于普通的网卡而言,支持很多虚拟化相关的Offload特性,完全可以称得上是SmartNIC。
对于Mellanox的网卡,有个比较重要的特性是OVS Offload Using ASAP² Direct,基于这个特性,可以将虚拟化网络常见的封装等等都卸载到硬件,极大的提升网络的性能,这个特性里还有一个功能SR-IOV VF LAG,可以将Bonding的功能卸载到硬件,这两个特性,就是虚拟化平台最需要的能力。
对于大部分公有云的场景,存在VPC的概念,不同用户的VM之间有租户的隔离,VM与VM之间的通信,会通过overlay网络通信,针对这种场景,ASAP²+SR-IOV VF LAG的功能可以很好的满足需求。VM的流量通过硬件卸载,Bonding通过硬件卸载,VM感知不到Bonding的存在,但是却可以享受到Bonding带来的高可用和负载均衡。于此同时,因为VM所有的流量都会额外封装一层隧道,所以对于交换机来说,只能看到宿主机的隧道端点IP地址,因此,对于类似的场景来说,只需要针对宿主机的IP进行ARP/ND双发即可。而这个需求,通过上面的patch就已经可以实现了。
Underlay网络的ARP双发
我们的场景和公有云有些不同,因为我们是一个私有云,并没有VPC的概念,所有的VM也是直接通过underlay网络通信的,这个网络模型,其实有点像早期公有云的经典网络。相比overlay网络而言,underlay网络的最大好处就是简单,所有VM的流量,只需要额外打上一个VLAN TAG即可,整个网络是扁平的,可以规避很多overlay的问题。并且作为一个私有云,所有的用户就是我们自己,也没有像公有云那样的租户隔离需求,因此使用这个模型是很自然的。
但是这也带来了一个原本不存在的问题:因为VM的流量是直接通过underlay网络通信的,所以VM的ARP/ND请求也需要“双发”了。一个可行的办法是把网络的结构暴露给VM,在VM里也打上bonding,然后就可以复用上面的patch,和物理服务器一样实现ARP/ND双发。很显然这不是想要的,既然提供了VM服务,就应该给用户更好的体验,尽可能隔离掉底层的这些细节。
这时候SR-IOV VF LAG的功能就派上用场了,SR-IOV VF LAG的功能可以将Bonding的功能卸载到硬件,这样一来,VM就感知不到Bonding的存在了。但是用上VF LAG之后,VM的流量都会被卸载到硬件里,就无法直接在网络层面实现ARP/ND双发了,因为硬件卸载的Bonding并不支持ARP/ND双发。当然修改网卡的固件也许能够解决这个问题,但定制固件的方案在时间和成本上都不太可行。
我们先看看在VF LAG的场景下,网卡硬件是如何转发流量的。在ASAP²的场景下,网卡工作在一个叫做“switchdev”的模式下。在这个模式下,网卡化身为一个交换机,这个交换机,可以根据“流表”来转发流量。这个流表是由OVS来管理的,当网卡中一条流表规则也没有的时候,网卡默认会把网络包原封不动的转发给OVS,靠OVS实现流表的学习和转发,一旦OVS学习到流表之后,再通过Linux的TC flower或者DPDK的rte_flow下发给网卡,当有后续的流量可以匹配到对应的流表规则,网卡会直接根据流表规则转发流量,不再把流量转发给OVS。
那流表长啥样?下面是一个线上实际的OVS流表例子和一些解释:
# 00:11:22:33:44:01是交换机(网关)的MAC地址,port(1)是交换机侧在OVS里的代表口
# 52:11:22:33:44:55是VM的MAC地址,port(2)是VM侧在OVS里的代表口
# 交换机发送的ARP请求,将VLAN TAG去掉,转发给VM
recirc_id(0),in_port(1),eth(src=00:11:22:33:44:01,dst=52:11:22:33:44:55),eth_type(0x8100),vlan(vid=1000,pcp=0),encap(eth_type(0x0806)), packets:7, bytes:448, used:0.067s, actions:pop_vlan,2
# VM发送的ARP请求,打上VLAN TAG,转发给交换机
recirc_id(0),in_port(2),eth(src=52:11:22:33:44:55,dst=00:11:22:33:44:01),eth_type(0x0806), packets:2, bytes:104, used:0.081s, actions:push_vlan(vid=1000,pcp=0),1
# 交换机发送的IPv4数据包(eth_type 0x0800是IPv4),将VLAN TAG去掉,转发给VM
recirc_id(0),in_port(1),eth(src=00:11:22:33:44:01,dst=52:11:22:33:44:55),eth_type(0x8100),vlan(vid=1000,pcp=0),encap(eth_type(0x0800)), packets:22772, bytes:4038655, used:2.350s, actions:pop_vlan,2
# VM发送的IPv4数据包,打上VLAN TAG,转发给交换机
recirc_id(0),in_port(2),eth(src=52:11:22:33:44:55,dst=00:11:22:33:44:01),eth_type(0x0800), packets:26714, bytes:3151911, used:0.300s, actions:push_vlan(vid=1000,pcp=0),1
# 交换机发送的IPv6数据包(eth_type 0x86dd是IPv6),将VLAN TAG去掉,转发给VM
recirc_id(0),in_port(1),eth(src=00:11:22:33:44:01,dst=52:11:22:33:44:55),eth_type(0x8100),vlan(vid=1000,pcp=0),encap(eth_type(0x86dd)), packets:111019, bytes:140906750, used:1.490s, actions:pop_vlan,2
# VM发送的IPv6数据包,打上VLAN TAG,转发给交换机
recirc_id(0),in_port(2),eth(src=52:11:22:33:44:55,dst=00:11:22:33:44:01),eth_type(0x86dd), packets:96546, bytes:8451184, used:1.490s, actions:push_vlan(vid=1000,pcp=0),1
可以看到,因为这几条流表的存在,交换机和VM之间的流量几乎都可以被匹配上,直接在硬件中进行转发。
由于ARP是作为独立的一个EtherType 0x0806进行传输的,仔细观察流表就不难发现ARP的流量有两条单独的流表规则,默认情况下,ARP的流表也会被写入到硬件中,从而实现硬件的offload加速,而我们需要进行ARP的双发,那第一步肯定是不希望ARP的流表被写入到硬件中,这样一来,ARP的流量会被硬件上送到OVS,由OVS来处理,刚好一个关键点是在流表的卸载方式选择上,我们使用了OVS-Kernel
的方式,OVS处理的流量最终会通过内核进行转发,这样一来,ARP相关的报文自然而然的进入到了内核Bonding模块处理,而双发的逻辑也就起了作用。
那理论上分析可行,剩下的就是需要修改一下OVS的代码了,实现起来倒是也不困难,OVS针对硬件流表的下发,都放在lib/netdev-offload-tc.c
这个文件中。所有的流表下发逻辑在netdev_tc_flow_put
函数中,因此只需要在这个函数中添加一个判断,如果是ARP的流表,直接返回EOPNOTSUPP
即可。
Underlay网络的ND双发
相比IPv4的ARP来说,IPv6的ND双发就有些麻烦了,一方面,用于IPv6邻居发现的Neighbor Discovery(ND)协议是通过ICMPv6来实现的,另一方面,ICMPv6的报文并没有单独的EtherType,而是和IPv6数据包一起共用了0x86DD
这个EtherType,从上面的流表也可以看出来,对于IPv6的流量,默认情况下只会有一组流表规则,根本区分不出业务的TCP/UDP流量和ICMPv6的数据报文,更区分不出ICMPv6中更细的ND相关的报文。
但是理论上,只要类似ARP那样,让硬件匹配不到,或者让硬件匹配到,然后上送给OVS处理,就可以类似ARP那样实现ND的双发了。
通过NVIDIA提供的文档Classification Fields (Matches),可以看到网卡是支持匹配IPv4/IPv6的TCP/UDP/ICMP/ICMPv6这几个Protocol的,既然可以匹配ICMPv6,那就尝试手动添加一条流表,看看是否可以匹配上。
# 添加一条ICMPv6的流表规则,匹配ICMPv6
# ovs-ofctl add-flow ovs-sriov "table=0,priority=200,icmp6 actions=normal"
# 再尝试dump当前学习到的流表
# ovs-appctl dpctl/dump-flows|grep 0x86dd
recirc_id(0),in_port(2),eth(src=52:11:22:33:44:55,dst=00:11:22:33:44:01),eth_type(0x86dd),ipv6(proto=58), packets:1, bytes:1, used:0s, actions:push_vlan(vid=1000,pcp=0),1
recirc_id(0),in_port(1),eth(src=00:11:22:33:44:01,dst=52:11:22:33:44:55),eth_type(0x8100),vlan(vid=1000,pcp=0),encap(eth_type(0x86dd),ipv6(proto=58)), packets:1, bytes:1, used:0s, actions:pop_vlan,2
recirc_id(0),in_port(2),eth(src=52:11:22:33:44:55,dst=00:11:22:33:44:01),eth_type(0x86dd),ipv6(proto=17), packets:3, bytes:210, used:1.560s, actions:push_vlan(vid=1000,pcp=0),1
recirc_id(0),in_port(1),eth(src=00:11:22:33:44:01,dst=52:11:22:33:44:55),eth_type(0x8100),vlan(vid=1000,pcp=0),encap(eth_type(0x86dd),ipv6(proto=17)), packets:0, bytes:0, used:1.560s, actions:pop_vlan,2
recirc_id(0),in_port(2),eth(src=52:11:22:33:44:55,dst=00:11:22:33:44:01),eth_type(0x86dd),ipv6(proto=6), packets:49, bytes:3923, used:2.850s, actions:push_vlan(vid=1000,pcp=0),1
recirc_id(0),in_port(1),eth(src=00:11:22:33:44:01,dst=52:11:22:33:44:55),eth_type(0x8100),vlan(vid=1000,pcp=0),encap(eth_type(0x86dd),ipv6(proto=6)), packets:411, bytes:605503, used:2.850s, actions:pop_vlan,2
可以看到,IPv6的TCP(proto=6)、UDP(proto=17)和ICMPv6(proto=58)都被单独匹配出来了,现在的流表规则,已经类似ARP的流表规则了,那接下来理论上参考ARP的修改,继续让ICMPv6的流表不卸载到硬件,就可以实现双发了。
但相比于ARP来说,ICMPv6本身包含的功能还是比较多的,这样修改的话,除了ND相关的报文,IPv6 Ping相关的报文也会被上送OVS处理了,总体来说还是会影响一些效率,是否可以继续拆分呢?比如只把ND相关的报文拆成单独流表不下发,剩下的Ping报文依然走硬件转发。OVS是支持这样的配置的,可以尝试一下,稍微修改一下下发的流表:
# 添加一条ICMPv6的流表规则,匹配ICMPv6的ND报文,icmp_type=135是ND协议中的NS包,也是我们需要双发的包
# ovs-ofctl add-flow ovs-sriov "table=0,priority=200,icmp6,icmp_type=135 actions=normal"
# ovs-appctl dpctl/dump-flows|grep 0x86dd
recirc_id(0),in_port(2),eth(src=52:11:22:33:44:55,dst=00:11:22:33:44:01),eth_type(0x86dd),ipv6(proto=58),icmpv6(type=135), packets:0, bytes:0, used:0s, actions:push_vlan(vid=1000,pcp=0),1
recirc_id(0),in_port(1),eth(src=00:11:22:33:44:01,dst=52:11:22:33:44:55),eth_type(0x8100),vlan(vid=1000,pcp=6),encap(eth_type(0x86dd),ipv6(proto=58),icmpv6(type=136/0xfc)), packets:0, bytes:0, used:0s, actions:pop_vlan,2
recirc_id(0),in_port(2),eth(src=52:11:22:33:44:55,dst=00:11:22:33:44:01),eth_type(0x86dd),ipv6(proto=58),icmpv6(type=type=128/0xfc), packets:0, bytes:0, used:0s, actions:push_vlan(vid=1000,pcp=0),1
recirc_id(0),in_port(1),eth(src=00:11:22:33:44:01,dst=52:11:22:33:44:55),eth_type(0x8100),vlan(vid=1000,pcp=6),encap(eth_type(0x86dd),ipv6(proto=58),icmpv6(type=type=129/0xfc)), packets:0, bytes:0, used:0s, actions:pop_vlan,2
可以看到,NS(type=135)和NA(type=136)的流表规则已经被拆分出来了,同样的Ping的Echo Request(type=128)和Echo Reply(type=129)的流表规则也被拆分出来了,理论上也可以如法炮制,只让NS报文相关的流表不下发到硬件,这样一来,ICMPv6的双发就可以实现了。但这里有一个问题,就是网卡其实不支持匹配更细致的报文了,直接看一下下发到硬件中的流表和对应的状态:
# 由于OVS-Kernel使用TC下发流表规则到网卡驱动,所以可以通过tc命令查询流表和对应的状态
# tc filter show dev vf0 ingress
filter protocol ipv6 pref 3 flower chain 0
filter protocol ipv6 pref 3 flower chain 0 handle 0x1
dst_mac 00:11:22:33:44:01
src_mac 52:11:22:33:44:55
eth_type ipv6
ip_proto icmpv6
icmp_type 135
not_in_hw
action order 1: vlan push id 3042 protocol 802.1Q priority 0 pipe
index 8 ref 1 bind 1
no_percpu
action order 2: mirred (Egress Redirect to device bond0) stolen
index 11 ref 1 bind 1
cookie b332255e9d440e02192dbb9b04d20332
no_percpu
可以发现,虽然OVS生成了NS报文的流表,并且也成功的转成TC规则并下发,但dump出来的规则里not_in_hw
表示这条规则并没有下发到硬件中,网卡在这种场景下不支持匹配ICMPv6的type字段,所以即使像之前一样修改OVS不下发该流表,最终网卡的行为都是一致的。
虽然没办法精确的将NS报文从硬件中拾取出来,但将ICMPv6报文全部上送软件处理,也算是可以接受的方案了,即使有些不完美,考虑到大部分业务的流量还是走的TCP/UDP,对业务的影响应该微乎其微。
总结
虽然最终没有达到最完美的状态,但最终还是实现了在硬件Bonding卸载的场景下,ARP和ND的双发。经过这几年的实践,这个网络方案在我们的私有云上运行的非常稳定,一方面,因为场景和公有云的差异,选择了一个比较简单的Underlay+OVS-Kernel卸载的方案,而不是基于DPDK的方案,另一方面,正是基于这样的选择,带来了额外的VM双发需求,只能说环环相扣,处处充满妥协了😀。
那么未来呢,未来的虚拟化网络是属于DPU(或者说IPU)的,借助DPU的帮助,终于可以在固件层面做一些想做的事了,类似的问题也就不是问题了,未来值得期待。