Keepalived 脑裂问题排查

用户报告问题,说在虚拟机里面启动的 Keepalived 有脑裂问题,一启动就开始脑裂了。

Keepalived1 是一个基于 LVS 的负载均衡和高可用框架。负载均衡主要是通过 VRRPv2 协议来实现的。VRRP 协议2在这个博客中介绍过,主要场景是两个路由器可以通过 VRRP 协议来协商出来一个 master,对外提供服务,当 master 挂的时候,slave 会成为 master 继续提供服务。

VRRP 的全称是 Virtual Router Redundancy Protocol,Keepalived 可不是 router,为什么用这个协议呢3?简单来说,Keepalived 要提供的是一个高可用的 VIP 服务,而 Virtual Router 本质上也是一个 VIP 来给其他的 Host 当作网关,这样一想,就很合理了。

Keepalived 实例之间无法达成一致,肯定是 VRRP 协商失败,而 VRRP 又是机器简单的协议,只有一种包的类型。出现 2 个 master 节点,那就肯定是 slave 的节点收不到 master 的 VRRP 协议包,认为 master 挂了,所以站出来当 master。

首先,用 tcpdump 检查了一下两个 keepalived,确实都在发送 VRRP 的包。而且VRRP 包的内容,如 auth,组 id,权重等,都正确。并且都只看得到自己发送出去的包,无法收到对方的包。

那接下来就来排查为什么这个包无法发送给 VRRP 的 slave。

虚拟机环境的网络简化如下。

虚拟机环境的网络架构

这个图看起来复杂,其实逻辑很简单。物理机 Host 使用 bonding 连接交换机,在 bond0 接口上配置 VLAN 封装,和交换机做 trunking,这一层对物理机内的网络使用几乎是透明的。然后虚拟机内网络使用 TAP4,所有 VM 内 eth0 发送的包会出现在 macvtap1 上,然后从 Host 的 bond0.1000 出去。 macvtap 其实就是 TAP 设备加上一个 bridge5,macvtap 基于 macvlan driver,使用一个 module 解决了原来 TAP + Bridge 的工作。

下一步就是定位 VRRP 包丢在了哪里,虽然涉及的网络 interface 很多,但是一个一个来排查就行了。首先给这个网络做了个简单的体检套餐,发现 ICMP 还是 TCP 都是一点问题没有,只有 VRRP 协议有问题。VRRP 协议是基于组播 Multicast 的,直觉上觉得可能是什么 ACL 把组播网络给 DROP 了。

从 Keepalived 发包的实例开始抓包,下一步直接抓发包的物理机接口 eth0 (二分查找定位么),确认 VRRP 正确发出去了,然后去收包的物理机上抓 eth0,也收到了,直接排除了交换机的问题。再抓 bond0.1000 接口,也抓得到,排除了 bonding 和 VLAN 问题。

下一步是 [email protected],也抓得到,然后去收包的 VM2 抓包 eth0,抓不到。那丢包就发生在 macvtap1 -> eth0 中。感觉已经接近真相了!

实际上并没有……

定位到这里之后,后面花了几个小时来研究为什么 macvtap1 的包没有转发到 eth0 中。

期间研究了 macvtap 设备的原理,libvirt 相关的文档,中间还有 chatGPT 这个半吊子瞎出主意,甚至把相关的 sysctl 参数都看了一遍,rp_filterxx_forwarding 之类的,仍然没有解决问题。这些没有用的排查就不记录了。

物理机的 macvtap 的 TAP 设备是由 qemu libvirt 接管的,负责转发到虚拟机中去。在研究 libvirt 和 multicast 流量的时候,发现了这个问题和回答6,虽然没有直接解决我的问题,但是感觉脑中一道闪电划过,有一个重要的本质问题被我忽略了——VRRP 是组播网络

单播是点对点发,广播是点对所有点群发,组播的特点是点对多点群发,「多点」指的是哪些点呢?怎么知道多点包括了哪些点呢?本质的原理是「订阅」,订阅了特定流量则会收到,如果不订阅,则收不到。

在网络上,多个设备之间订阅组播是通过 IGMP 协议7实现的。在同一个设备上,就是设备自己的实现了。

有了这个想法之后,我马上在 VM 中和 Host 中运行了 ip maddr show dev 命令进行验证。结果如下。

在 VM 中的运行结果
在 Host 中的运行结果

这条命令的含义是,列出来当前这个接口订阅的组播流量。通过结果可以看到,在 VM 中订阅了 VRRP 协议规定的组播地址,但是在物理机的 macvtap 接口上,就没有订阅这个地址。所以物理机的接口在收到发给 224.0.0.1 的 VRRP 流量之后,会认为当前这个接口没有订阅过这个组播,所以不需要这些流量,直接忽略。这是组播协议预期的工作方式,所以当我在排查接口的丢包参数的时候,都没有发现什么异常,因为这不算做是丢包吧,而算作是正常的处理方式。

那么为什么会出现这种情况呢?

按照组播协议的工作方式,当需要组播流量的时候,需要向操作系统通过 syscall 来发出「订阅」8,因为协议是由操作系统来处理的。但是我们这里存在 2 个操作系统,VM 是一个操作系统,Host 是一个操作系统。如上图所示,虽然 VM 知道订阅了这个组播地址,但是 Host 操作系统并不知情,两个系统是隔离的。所以当 Host 收到组播流量的时候,直接忽略了。

解决办法是,对接口设置 ip link set dev macvtap8 allmulticast on,意思是告诉接口,把所有的 multicast 都给收了,这样 VM 内的接口决定处理还是忽略,就正常了。(libvirt 也有 trustGuestRxFilters9 的配置选项)

VRRP 脑裂问题需要避免,在物理网络中,网关之间的 vrrp keepalive 会使用专用的 keepalive 线路,并且多条物理线路做 LACP 高可用。

  1. https://keepalived.readthedocs.io/en/latest/introduction.html ↩︎
  2. 数据中心网络高可用技术之从服务器到网关:首跳冗余协议 VRRP ↩︎
  3. Keepalived 的系统设计:https://keepalived.readthedocs.io/en/latest/software_design.html ↩︎
  4. TAP 设备在 VPN 和虚拟机网络中比较常见:https://en.wikipedia.org/wiki/TUN/TAP ↩︎
  5. https://virt.kernelnewbies.org/MacVTap ↩︎
  6. https://superuser.com/questions/944678/how-to-configure-macvtap-to-let-it-pass-multicast-packet-correctly ↩︎
  7. https://en.wikipedia.org/wiki/Internet_Group_Management_Protocol ↩︎
  8. 如何使用组播的教程 https://tldp.org/HOWTO/Multicast-HOWTO-6.html ↩︎
  9. https://libvirt.org/formatnetwork.html ↩︎
 

Linux interface Vlan 和 Bond 配置错误问题排查

昨天同事报告了一个 Linux 机器网络问题,现象是:一台服务器无法 ping 192.168.1.253,但是可以 ping 192.168.1.252 和 192.168.1.254. 这三个 IP 都是交换机的 IP,并且和和服务器的 IP 在同一个子网下。

服务器使用了 bond1 分别连接两台交换机2,两台交换机通过 VRRP 协议提供一个高可用的网关 IP3。其中,网段的最高位一般是 VRRP 的 VIP,即 192.168.1.254,而最高位 -1 和 -2 分别是两个交换机的物理 IP,即 192.168.1.253 和 192.168.1.252 分别是两台交换机。

于是,看到这个现象,自然而然地想到是其中一台交换机有问题,192.168.1.253 已经挂了,192.168.1.252 还存活,并且担任了 192.168.1.254 的 VIP 的责任。

先去这台服务器 ping 了一下,果然是 ping 不通的,ping 显示的错误信息是 Destination Host Unreachable。然后在服务器抓包,确认下 ICMP reply 确实没有发送回来。tcpdump -i bond0 icmp. 抓包确实没有看到 ICMP reply 包,但是奇怪的是,居然连 ICMP echo 也没有抓到

之后又去检查了交换机的配置,包括 channel-group,VLAN 配置,ACL 等等,也确认了下两台交换机之间的横连状态是正常的。这时候看起来不像是交换机的问题了。使用另一台服务器 ping 了一下这三个 IP,.252, .253, .254 都是通的。那应该是服务器的问题而不是交换机的问题。

其实这部分有些走弯路,因为 ping 明确显示 Destination Host Unreachable,说明这个包并没有发出去;而且 tcpdump 也没有抓到包,也可以印证。

接下来继续在服务器上定位问题。

ICMP 发包有问题,就先检查一下发包链路。之前遇到过类似错误,是 iptables 的 OUTPUT chain 把包 drop 了,于是先检查了 iptables,确认没有相关的 DROP。

ICMP 是基于 IP 层的协议,IP 层的协议依赖 ARP 协议来找到 MAC 地址,然后封装成二层 Frame,才能发出去,接下来去检查 ARP。(其实上一步直接检查 iptables 是不合理的,ARP 是第一步,有了 ARP 才可能构造出来完整的 Frame 开始发送,应该先从 ARP 开始排查)。

检查 arp -a | grep .253,发现 ARP 的 cache 结果是 <incomplete>. 然后用 arping 192.168.1.253 验证 ARP request 是否能得到正常的 reply,发现结果都是 Timeout。

到这里已经知道为什么 ping 会失败了,因为服务器得不到这个 IP 对应的 ARP 请求,所以 ping 无法将 ICMP request 的包发送出去,直接报错了。

接下来就定位为什么 ARP 会失败。

正常来说,ARP 应该从 bond0 接口发送出去一个 request,然后收到一个 reply,刷新服务器的 ARP cache entry。

服务器的 interface 配置如下,服务器所在的 VLAN 是 1000,和交换机做了 Trunking4,发送包的路由是走 bond0.1000@bond0 这个 interface,bond0.1000@bond0 是一个虚拟 interface,主要的功能是,发包的时候对包进行 802.1Q VLAN 封装,然后通过底层的 interface——在这里是 bond0——发送出去,收包的时候对 VLAN 进行解封装。

接口的逻辑图

我首先在 bond0 抓包,确认 ARP 的发送和接收在协议上是正常的。

结果在这一步就发现问题了,bond0 抓包发现,只有发出去的包,没有收到的包。

为啥交换机不响应 ARP 了呢?

这时候又怀疑是交换机的问题,去检查了交换机的两个端口配置。没有发现问题。而且在其他机器上,ping 和 arping 都是没有问题的,交换机设备的问题可能性比较小。

也不会是服务器安全策略的问题,如果是的话,tcpdump 也会先抓到包的,在后面才会被 iptables 之类的 DROP 掉。

于是仔细想一想交换机和服务器之间经过了哪些组件,网卡收包,中断,网卡 driver,bond driver,协议栈处理。抓包都没抓到,说明问题出在协议栈之前,于是怀疑到 bond driver 头上去。

下一步,在物理 interface 上抓包,确认物理 interface 到底收到了 ARP reply 了没有。结果是,发现 eth0 这个 interface 收到了 ARP reply!

ARP reply 在 eth0 上收到了,但是 bond0 上没收到。这下感觉快要得到答案了。bond 有两个 slave,我把 eth0 shutdown 了,只留下 eth1,然后网路正常了。那要么是 bond driver 真的有问题,要么是我们的配置有问题。从经验上看,Linux driver 存在 bug 的概率要远远小于我们的配置错误。于是我去检查 bond 相关的配置。

检查 bond 状态 (/proc/net/bonding/bond0 文件), bond 配置,都没发现问题。可能是 eht0 这个接口有问题?

在重新看 interface 的时候(即上面的 ip link 命令和输出),我发现了可疑的一条 interface:

这里多出来一个 VLAN interface。

所以,实际上的 interface 配置应该是如下这样。由于 eth0.1000 的存在,我怀疑 eth0 收到的 ARP reply 实际上是送给了 eth0.1000@eth0 而不是 bond0,然后在 ARP 协议处理的时候,Linux 认为我们没有从 eth0.1000 发送出去 ARP request,但是却收到了 ARP 响应,属于 Gratuitous ARP5. 而发送 ARP request 的 bond0,从来没有收到 ARP reply。ARP cache 是 per interface 的,所以 bond0 无法发送 ICMP 出去。

eth0.1000 的配置

证明这个猜测很简单,只要在 eth0.1000@eth0 抓包,看是否有 ARP reply 就好了。抓包发现果然有。

并且把这个接口的 arp_accept 打开,让其接受 Gratuitous ARP,发现 ARP cache 出现了如下记录:

说明这个结论是正确的。到这里就发现,其实问题不仅仅是 ARP 的问题,因为 bond 的两个 slave 有一个不对,收包的时候可能是从 eth0 收,也可能是从 eth1 收,取决于交换机的 hash 策略6。如果从 eth0 进来,那么协议栈的 skb 的 device 就会是 eth0.1000@eth0,所有有连接的协议处理都匹配不上。

于是我 shutdown eth0.1000@eth0 这个接口,理论上机器的配置应该都是对的了。

结果不是,问题依然存在,有点让人怀疑人生。由于接口 down 了就无法抓包了,不太好确认包是不是还在往 eth0.1000@eth0 送了。此处又花了一些时间排查,因为怀疑自己的推论是错误的,是不是有别的地方导致这个问题?一通误打误撞,决定删除这个多余的接口,然后网路就完全恢复了。从结果看,只 shutdown 这个接口不能阻止包往这个 vlan 接口送,得删除才行。

事后我们得知,这台服务器在 infra 团队交付的时候存在问题,应该配置 bonding,但是没有配置,只是在一条线(eth0)上配置了 VLAN。我们的同事拿到机器之后修复了 bonding 问题,但是并没有删除 eth0.1000@eth0 这个 VLAN 虚拟接口,导致产生了非预期的行为。

后来看了下源代码,发现 VLAN 的处理确实优先级比较高,在 __netif_receive_skb_core7 这里就会执行 vlan_do_recieve8,然后会把 device 的 id 设置在 skb 上。这个逻辑比 bond driver 的逻辑靠前,导致后续协议栈的处理,会认为这个包是从 eth0.1000@eth0 收到的,而不是从 bond0 收到的。

  1. 数据中心网络高可用技术之从服务器到交换机:802.3 ad ↩︎
  2. 数据中心网络高可用技术之从交换机到交换机:MLAG, 堆叠技术 ↩︎
  3. 数据中心网络高可用技术之从服务器到网关:首跳冗余协议 VRRP ↩︎
  4. VLAN Trunking Protocol ↩︎
  5. 特殊的 ARP 用法:Gratuitous ARP, ARP Probe 和 ARP Announce ↩︎
  6. 数据中心网络高可用技术之从服务器到交换机:链路聚合 (balance-xor, balance-rr, broadcast) ↩︎
  7. https://elixir.bootlin.com/linux/v6.12.6/source/net/core/dev.c#L5457 ↩︎
  8. https://elixir.bootlin.com/linux/v6.12.6/source/net/8021q/vlan_core.c#L10 ↩︎
 

菠萝

体检的诊所送了我们两张 Toast Box 的券,Toast Box 和 BreadTalk (面包新语)是一家,是专卖吐司的新加坡经典早餐店。

新加坡注重效率,不像澳大利亚,买一杯咖啡都要和你寒暄一下今天怎么样,打算做什么。一般来说,早餐店的店员甚至都不会说出一个没有用的字。而且不管你是不是外国人,都只会用本地的语言要你点餐,也不会跟你解释。

但我可是来了新加坡 4 年1了,已经身经百战。

店员看了看券,说,”Coffee?”

“Kopi and Kopi C Kosong, Shao”,我已经把南洋咖啡的逻辑熟记于心,难不倒我,甚至还帮欣点好了。

南洋咖啡的逻辑

店员说,”Toast?”

我点了个最贵的,店员说这个不能用券。

我问能点啥。

“Kaya, Butter, Bo Luo.”

Kaya 齁甜,我点了个 Butter。但是 Bo Luo 是什么玩意?我想,哪有人在吐司上放菠萝的,难道美国人的夏威夷披萨启发了新加坡人吗?这也能吃?虽然很不可思议,但是麦当劳在新加坡推出了菠萝汉堡,如果菠萝汉堡能合法,那么菠萝吐司应该也不奇怪了吧。不过,真的会有点这种东西吗?

这样想着,欣说,”Bo Luo”.

我震惊,她是怎么想的。

在等餐的时候,我还在做着思想斗争,这菠萝会酸吗?菠萝是热过的还是鲜菠萝?早上吃这玩意胃能受的了吗?

漫长的两分钟过去了,Kaya 和 Bo Luo 好了。

我长舒了一口气——“太好了,是菠萝包。”

“你以为呢?” 欣说。

麦当劳的菠萝汉堡
  1. 十六个夏天(为什么会有人在这种文章用上脚注?) ↩︎
 

由 ICMP Redirect 消息引起的丢包问题排查

明天是圣诞节,今天是平安夜,公司给提前下班。但是年假还有二十多天,不用也是浪费了,今天就请假休息一下。

从 10月25日开始,一直在排查一个比较复杂的网络问题。和网络组的同事们奋战了两个月,昨天终于有了答案,记录一下。

背景是我们内部的 一个类似 EKS1 的产品,是使用虚拟机来搭建,虚拟机的网络是一层虚拟的 Overlay 网络,然后虚拟机内搭建 Pod,Pod 也是在一层 Overlay 网络,整体上,这就是两层 Overlay 网络。封装 VXLAN 的网关(VTEP设备)需要相关的包进行两次 Overlay 封装。

为了 VXLAN 网关设备实现的简单,用了一个比较奇怪的路由方式,让普通路由器和网关设备配合完成两次封装:

  1. 路由器收到包,查询路由,发现这个包应该转发给 VXLAN 网关,于是转发给 VXLAN 网关;
  2. VXLAN 网关封装第一层 Overlay,目的 IP 改成 Overlay_IP_1 完成之后,交给路由器;
  3. 路由器收到包,查询路由,发现这个包现在的 IP 地址 Overlay_IP_1 还是在 VXLAN 网关这里,再次转发给 VXLAN 网关,于是转发给 VXLAN 网关;
  4. VXLAN 网关封装第二层 Overlay,目的 IP 改成 Overlay_IP_2 完成之后,交给路由器;
  5. 路由器查询路由,转发给目的地址,完成;

说它奇怪,是因为在第 2 步和第 3 步,这个包的路由明明在 VXLAN 网关这里,但是包从 VXLAN 网关发给了路由器,路由器又转发回来。但是理论上是可行的。

我们遇到的问题是,这种实现方式实际会遇到 25% 或者 50% 的丢包

问题本身就比较复杂,光是描述清楚就比较绕了,所以中间花了很多时间沟通,测试,验证,抓包。

25% 和 50% 这个数字很敏感,像是 4 个端口中的 1 个或者 2 个有问题。

我们的思路还是逐渐缩小问题范围,首先,确认丢包点在哪里——通过 ping 来缩小范围。最后定位到问题发生在第三步上,网关把包发给路由器之后,路由器并没有把包发送回来。

然后我用 scapy2 模拟了最简单的包在网关发出去,发现这个包确实没有从路由器转发回来。

但其实问题的范围还是很大,比如:

  • 网关没有真正发出来这个包,即使我们抓包确认包发出去了,但是可能被网卡驱动,或者网卡硬件丢掉了;
  • 路由器收到之后,没有转发这个包;
  • 路由器转发了,但是被出口端口丢弃了;
  • 路由器转发了,但是被网关丢了;
  • 等等。

为了再缩小问题,我们用 SPAN3 对路由器抓发,最后定位包确实到了路由器,路由器没有转发回来这个包。而且还发现一个细节:网关和路由器之间是有多条连线4的,路由器收到网关的包,然后选择路由器和网关之间的一条线,来把包还给网关,假设选中(hash)的线路恰好是收到包的线路,那么这个包就无法发回来。这也印证了 25% 和 50% 的丢包率。

在测试了几种品牌的交换机之后,发现只有 Cisco 品牌的交换机有这种行为。

到此为止,我们认为是路由器的 BUG。在讨论解决方案的时候,有同事发现丢包和包的一个 Header 有关,只要这个 Header 的值不为 0,就不会发生丢包。于是大家都很高兴,经过验证,确实不会丢包了。于是认为问题解决了。开始交付网络来测试。

测试发现流量稍大又会丢包了。中间又经过了很长时间的排查和验证,都不对,这里就不记录了。

最后,由于问题可以清晰地复现:用 scapy 构造一个包给路由器,路由宣告在自己这里,确认路由器不会按照宣告的路由发送回来。也能确认问题就是出在路由器这里了。

于是周一我们联系了 Cisco 的 TAC,提交了问题。下午就得到了回复。我的理解如下。

这并不能说是设备的 BUG,应该说是一个行为。即,在路由器视角看来,这个包的路由明明在网关这里,网关为什么要把这个包发给我呢?但是,路由器依然会按照路由把包转发给网关,但是会额外发送一个 ICMP Redirect5 包给网关,提示它你有更优的路由可以选择,不要再发给我了。ICMP Redirect 消息,在之前的博客中6也讨论过。

但是我们的网关并不会理会路由器发送来的 ICMP 提示消息,因为这个多余的一跳是设计上的预期行为(至少在当前阶段是的,将来可能会优化这里)。所以会继续把包往非最优路由,即路由器,发送。

路由器对于收到的每一个包都会转发并且会发送一个 ICMP Redirect 消息。这个 ICMP Redirect 消息的成本非常高:路由器要从转发的包里面拿到源 IP 地址,然后构造一个 ICMP 包进行发送。这个操作无法在 ASIC 芯片上完成,需要 Line Card 把包上送到 CPU,由 CPU 处理这种复杂的任务。

ASIC 芯片能够直接处理设备接口的包,高速完成简单的转发操作。有点像 GPU,能做的事情不多,但是能做到的都很快。

CPU 能够完成很多复杂的任务,但是比较慢。而且,网络设备通常由多个 ASIC 芯片,一个芯片只负责几个接口,从而性能高到能让所有的接口达到线速。(在《Google 的十年五代网络架构》7一文中讨论过)。但是 CPU 的数量就相对少很多。

网络设备为了实现高速转发,会将大部分的操作都放在 Line Card 上来完成,不必上送到 CPU。

其他一些需要 CPU 来处理的操作,比如:

  • 处理 TTL=1 的包(所以 mtr 来 debug 的时候经常看到中间的网络设备丢包8,但实际上不会造成问题,因为只会影响 TTL=1 的包,不会影响正常的数据包);
  • IP Fragmentation9

此外,CPU 最主要的工作就是运行网络设备的操作系统,以及处理动态路由协议,比如 BGP 和 OSPF 等等。虽然我们使用的路由器的 CPU 已经很强大了,但是设备不会允许 CPU 花费过多的时间来做 ICMP Redirect 这种事情。所以 Nexus 7000 平台设计了 Control Plane Policing (CoPP) 的 feature,来保证路由协议,SSH 这种重要控制面的包能够得到最高的优先级。像 ICMP Redirect 这种包,即使 CPU 空闲,但是如果发送的过多,也会被限制。限制的结果就是不仅 ICMP Redirect 不会发送,连原来的包也不会转发,直接 Drop 掉。

CoPP 是很重要的保护措施,不建议关闭。

较好的解决方法更加简单,就是一行命令,在交换机的 interface 上执行 no ip redirects 就好了,即,让这个接口不要再生成 ICMP Redirect 消息,这样包就可以直接在 Line Card 完成转发,无需 CPU 处理。

Cisco 的工程师推荐了这篇文档:Understand ICMP Redirect Messages10,内容很不错。

  1. https://aws.amazon.com/eks/ ↩︎
  2. https://scapy.net/ ↩︎
  3. 路由器和交换机的包分析方式:https://www.cisco.com/c/en/us/support/docs/switches/catalyst-6500-series-switches/10570-41.html ↩︎
  4. 数据中心网络高可用技术之从服务器到交换机:802.3 ad ↩︎
  5. https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol#Redirect ↩︎
  6. 由一个子网掩码配置错误所想到的 ↩︎
  7. Google 的十年五代网络架构 ↩︎
  8. 使用 mtr 检查网络问题,以及注意事项 ↩︎
  9. 有关 MTU 和 MSS 的一切 ↩︎
  10. https://www.cisco.com/c/en/us/support/docs/ios-nx-os-software/nx-os-software/213841-understanding-icmp-redirect-messages.html#toc-hId–1052318184 ↩︎