由 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 ↩︎
 

数据中心网络高可用技术:ECMP

在之前的文章中,已经介绍过很多次 ECMP1 了,它的原理非常简单:在路由协议中,如果下一跳有多个路径可以选择,并且多个路径的 cost metric 相等,那么路由器就会根据包的 header,计算一个 hash 值,然后根据这个 hash 值对这个 flow 选择一条固定的路径,作为下一跳。

这里的要点是:

  • 路径的 cost metric 要相等。需要注意,Administrative Distance 决定协议的优先级,而 ECMP 只适用于同一协议内部的路径选择,例如 OSPF 使用 Cost 值,BGP 使用 AS-Path 长度;
  • 如我们前面讨论过的 bonding 技术2,ECMP 选路的时候,也是基于 hash 的,这样可以对一个 flow 尽量保证包到达的顺序一致;
  • ECMP 只影响在某一跳选择下一跳,不影响全局。即,它在每一跳之间做均衡,而不是全局做均衡。

我们拿实际的问题来分析。

负载不均衡的 ECMP 部署

如果如图所示部署,一共有三台服务器,部署在两个 rack,这样会造成右边的服务器承担的流量是其他服务器的两倍,流量是不均衡的。因为第一层 ECMP 是发生在两个 Rack 之间,每一个 Rack 的交换机都是拿到 1/2 的流量。然后再进行服务器和 Rack Switch 之间的 ECMP,服务器之间均分 Rack 分得的流量。

所以 ECMP 对部署结构是有要求的,要保证每一个 Rack 的服务器数量大致相当

ECMP 环境排错要点:理解 flow hash

因为环境中有多条 path,所以在排查问题的时候,要考虑到这一点。

多条 path 之间是通过 flow 的 hash 结果来选路的。ICMP 包主要通过 Src IP, Dst IP, Type, Code, Identifier 来确定一个 flow,如果使用 mtr3,那么一次 mtr 的链路是固定的,因为 mtr 使用固定的 Identifier 来发送 ICMP,在 ECMP 链路中,总会 hash 到一条特定的路线。有的时候,网络存在问题,但是 ping 和 mtr 可能显示没有问题,原因就是恰好 ICMP 被 hash 到了好的线路上去。

如果使用 mtr --tcp --port 80,就可以看到链路中所有的线路。因为 mtr 使用 tcp 的时候,每次使用的 src port 是不固定的,这样就导致每次发出来的 TCP 包都 hash 到不同的线路上。

mtr 默认使用 ICMP,只能看到一条路线
如果使用 --tcp,会显示出来(第8跳)所有的路线

假设就是想测试和客户端一样的固定线路,可以使用 tcptraceroute4,这个工具可以指定 src port,这样 TCP 四元组就固定了。在 ECMP 环境中就会走一样的路线。

另外一点排错经验,如果是 25% 丢包,50% 丢包,通常和某条线路丢包有关。

连接池问题

我们常用的很多 SDK,比如 redis,和 db 的 SDK,都会老道的创建一个连接池来复用连接,减少 tcp 创建的 overhead。还有一个细节,就是这些 SDK 一般会给连接加上最大存活时间,如果超过之后,就会关闭这个连接并删除。

这样做是有好处的,我们自己写的代码中访问 HTTP 服务也会用连接池,但是很少会注意要重建连接。就导致一个连接建立起来,会存在数天。有一些交换机有 Sticky ECMP 的功能,假设多路网络中有一个设备下线,流量就会分散到剩余的设备中。当这个设备回来的时候, Sticky ECMP 会保证这期间创建的连接都依然走一样的路线(不会考虑重新加入的设备)。

这样就会造成连接的带宽使用不均衡。如果连接一直不关闭,就会一直不均衡。在总带宽利用远小于 100% 的时候,就会出现丢包的情况。

所以我们在写代码的时候,也要注意定时重建连接。

  1. 四层负载均衡漫谈 ↩︎
  2. 数据中心网络高可用技术之从服务器到交换机:802.3 ad ↩︎
  3. 使用 mtr 检查网络问题,以及注意事项 ↩︎
  4. https://linux.die.net/man/1/tcptraceroute ↩︎

数据中心网络高可用技术系列

  1. 数据中心网络高可用技术:序
  2. 数据中心网络高可用技术之从服务器到交换机:active-backup
  3. 数据中心网络高可用技术之从服务器到交换机:balance-tlb 和 balance-alb
  4. 数据中心网络高可用技术之从服务器到交换机:链路聚合 (balance-xor, balance-rr, broadcast)
  5. 数据中心网络高可用技术之从服务器到交换机:802.3 ad
  6. 数据中心网络高可用技术之从交换机到交换机:MLAG, 堆叠技术
  7. 数据中心网络高可用技术之从服务器到网关:VRRP
  8. 数据中心网络高可用技术:ECMP
 

iptables 拦截 bridge 包的问题排查

最近排查的一个网络问题,两个 IP 之间的网络不通,经过在 Linux 上一个一个 interface 上抓包,发现包丢在了本地的 bridge 上。

Bridge 就是一个简单的二层设备,虽然是虚拟的,但是应该逻辑也很简单,怎么会丢包呢?

经过一通乱查,发现 Bridge 的包跑到了 iptables 里面去,被 iptables 的 FORWARD chain DROP 了。

iptables dropped pakcet

说到这里跑个题,我有一个排查 iptables 是哪一条 rule 丢包的妙计,就是 watch -d "iptables -nvL | grep DROP,watch 会监控引号中的脚本,脚本会过滤出来所有会丢包的 rule,-d 参数很关键,它可以让 watch 每次对比和上一次命令的不通,然后高亮出来。一眼定位到问题。

话说回来,bridge 一个二层的设备怎么会跑到 iptables 里面去?iptables 可是 IP tables,这是三层呀。

在 Linux 中有一个机制,可以让 layer 2 的 bridge 代码调用 iptables, arptables, ip6tables 等。这样能做的事情就比 BROUTING chain (Bridge Routing1) 更多。可以在 bridge 上通过 iptables 做 dnat, stateful firewall, conntrack 等2

如果不需要 bridge 上的包跑到 iptables 上过一遍,可以通过 kernel 参数关闭:

sysctl -w net.bridge.bridge-nf-call-iptables=0

0 的意思是 bridge 的包不会去 iptables,1 就是会去 iptables,默认是 1. 也是执行完这行命令,网络果然就通了。

Bridge call iptables, 之前是在 kernel 实现的一个功能,但是显然这样会有性能问题。后来就独立出来作为一个独立的 kernel module 了 (br_netfilter3)。(如果使用 physdev 4就会自动启用这个 module)。

另外,nftablesiptables-nft 也会受到影响,layer violation 会有很多复杂的问题。新的 kernel module – nf_conntrack_bridge5 可以做到直接在 bridge layer 实现 connection track. nftables6 是下一代的 iptables。

前面说过这个功能是一个 kernel module,所以在关闭的时候有一个小小的问题。即我们关闭的时候,可能还没有这个 module load,所以会告诉我们无法设置这个参数:error: "net.bridge.bridge-nf-call-iptables" is an unknown key。如果后来创建一个 bridge,那么这个 module 会自动 load,那么包就又会跑到 iptables 里面去。

libvert 给出的解决方案7是,通过 udev 来创建一个事件,每次创建 bridge 就执行 sysctl 来 disable net.bridge.bridge-nf-call-iptables.

  1. ebtables https://linux.die.net/man/8/ebtables ↩︎
  2. ebtables/iptables interaction on a Linux-based bridge:https://ebtables.netfilter.org/br_fw_ia/br_fw_ia.html ↩︎
  3. https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=linux-3.18.y&id=34666d467cbf1e2e3c7bb15a63eccfb582cdd71f ↩︎
  4. physdev: https://manpages.debian.org/bookworm/iptables/iptables-extensions.8.en.html#physdev ↩︎
  5. nf_conntrack_bridge: https://www.kernelconfig.io/config_nf_conntrack_bridge ↩︎
  6. nftables wiki: https://wiki.nftables.org/wiki-nftables/index.php/Main_Page ↩︎
  7. https://wiki.libvirt.org/Net.bridge.bridge-nf-call_and_sysctl.conf.html ↩︎
 

Docker 命令行小技巧:runlike

事情要从上周的一次事故说起,我们用 docker 部署的程序有一点问题,要马上回滚到上一个版本。

这个 docker 是一个比较复杂的和 BPF 有关的程序,启动时候需要设置很多 mount 和 environments,docker run 的命令特别长。所以我用 Ansible 来配置好这些变量,然后启动 docker,一个实例要花费 3~5 分钟才能启动。

同事突然说,某实例他手动启动了,当时我就震惊了,怎么手速这么快?!

请教了一下,原来是用的 runlike 工具,项目地址是:https://github.com/lavie/runlike

这个工具的原理是,docker inspect <container-name> | runlike --stdin ,就会生成这个容器的 docker run 命令。这个思路简直太棒了。就和 Chrome 的 copy as cURL 功能一样好用!