问题描述

某服务 A,最初只部署在虚拟机上,后来扩充部署了部分容器实例。 在扩充容器实例不久后就发现服务 rt 偶尔会出现短暂超时的情况, 而从监控上看,容器实例的性能明显比虚拟机差, 具体而言就是容器实例冲频繁出现 rt 高的情况(rt 大于特定的时间比如 50ms 才会超时)。 超时时间一般持续 10s 左右,进一步查看监控数据发现所有超时的情况均出现在容器实例中。 所以,开始着手排查问题。

问题排查

1. CFS 排查方向

出问题的时候只有容器上的 A 应用,所以猜测网络层面没有问题, 因为如果网络层面有问题的话那受影响的可能就不只是 A 应用。 又考虑到出问题的都是容器实例,自然应该从容器和虚拟机的差异方面入手。 应用层方面,容器和虚拟机最大的区别马上就让人想到 CPU CGroup, 容器的 CPU 分配机制都是基于 CGroup 实现的,CGroup 通过内核 CFS 调度算法来实现 request 和 limit 配置。 limit 则是通过一种限流机制来实现对某一进程对 CPU 使用的上限。 当时最先猜测 CFS 中的上限设置算法有 bug,google 一下,居然真的找到相关的 issue, CFS quotas can lead to unnecessary throttling #67577, 这个 issue 描述的问题和当前问题及其相似,于是我们先做了一个尝试,把 k8s pod 上的 limit 参数去掉。 去掉 limit 参数后,参数似乎生效了,通过监控发现容器中 rt 高的数据点较之前少了大概有 1/3, 但是没过多久有发现了容器超时的告警, 这直接说明 CFS 的这个 bug 不是 rt 超时的本质原因。

2. 网络层排查方向

CFS 问题被排除了,而一开始又把网络层面的可能性也排除了,一时间不知道要从哪里入手了。 在最后一次出现超时的时候,恰好当时容器所在节点所连接的交换机的流量被抓包下来了。 在周末的时候对着当时抓下来的流量看了一上午,得出一个令人兴奋的结论——网络 100% 有问题。 从抓包数据看到和 A 服务的通信流量在故障时间那一刻有一个数据包在不停的被重传, 最后成功发送耗费了至少 13s。 这基本可以推断当时的网络是有问题的,因为,即便是应用层没有响应, 协议层应该也早就给这个包发送 ack 了,一直在重传要嘛这个包传不到,要嘛 ack 没收到 (抓包数据中有丢包的情况,所以没法确定是 ack 是否发送)。

确定网络有问题后大概花了一天半时间狂补 k8s 网络方面的知识。 当前 k8s 网络的 CNI 用的是 kube-router, 而 kube-router 最大的特定是使用 BGP 协议实现 k8s 集群内外的网络打通。 后来又有同事指出我之前没注意到的非常重要的一点——故障发生时网络存在环路。 查看抓包的数据,故障发生时那个被不停重传的 A 服务相关的数据包 Identification 都一样, 而每次 TTL 都减二, 从这里可以进一步得出结论,这个数据包是在两个设备之间来回传递(TTL 减二), 这两个设备应该是核心交换机和接入交换机, 当时网络中似乎存在一个临时环路。 到这里,进一步猜测,网络设备硬件本身没问题, 但是交换机上的某些路由记录可能在短时间内有错导致路由环路出现。 交换机上为什么会出现路由环路? 还是对比容器和虚拟机网络差异,容器方面最大的不同应该就是容器 kube-router 使用了 BGP, 于是继续猜测是 BGP 协议导致交换机上出现了路由环路, 带着这个猜测去翻 BGP 协议资料, 在 Tanenbaum 所编写那本教科书 Computer Networks 的 482 页上发现:

However, and somewhat ironically, it was realized in the late 1990s that despite this precaution BGP suffers from a version of the count-to-infinity problem (Labovitz et al., 2001). There are no long-lived loops, but routes can sometimes be slow to converge and have transient loops.

这段话表明,BGP 协议可能由于路由收敛的问题导致临时环路的出现。 但是如何验证交换机上在特定时刻出现了环路呢,对交换机设备不熟, 当时没想到可以到交换机上看日志,但是把这个情况跟网路的同事反映了。 第二天过来网络的同事从交换上的日志查到了 BGP peer 断开的日志, 而且 BGP peer 断开的每个时间点都能和之前故障发生的是简单对上。 BGP peer 断开瞬间,接入交换机清掉了通向容器实例的路由记录, 又由于接入交换机的默认路由设置成了核心交换机, 在核心交换机同步好这些路由变更之前,两台设备便存在一个路由环路。

接下来还有要搞清楚为什么 BGP peer 会断掉。 这个其实已经很容易猜到了——kube-router 挂掉, 通过 k8s 日志确实看到很多 kube-router restart 的记录. 在通过手工的模拟,删除 kube-router 实例,完美复现出 BGP peer 断开的问题。