一次 k8s 网络抖动问题排查
问题描述
某服务 A,最初只部署在虚拟机上,后来扩充部署了部分容器实例。
在扩充容器实例不久后就发现服务 rt 偶尔会出现短暂超时的情况,
而从监控上看,容器实例的性能明显比虚拟机差,
具体而言就是容器实例频繁出现 rt 超时的情况(rt 超时指 rt 大于特定的时间比如 50ms)。
超时时间一般持续 10s 左右,进一步查看监控数据发现所有超时的情况均出现在容器实例中。
问题排查
1. CFS 排查方向
出问题的时候只有容器上的 A 应用,所以猜测网络层面没有问题,
因为如果网络层面有问题的话那受影响的可能就不只是 A 应用。
又考虑到出问题的都是容器实例,自然应该从容器和虚拟机的差异方面入手。
考虑容器和虚拟机最大的区别,很容易想到容器的 CGroup CPU 资源隔离。
k8s 通过 limit、request 参数来设置容器的 CPU 资源上下限。
limit、request 利用 CGroup CPU 参数配置来实现,
它本质上又是通过内核 CFS 调度算法来实现的。
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 的问题也被排除了,一时间不知道要从哪里入手了。
在我们后来又一次出现超时的时候,容器宿主机连接的交换机的流量被抓包下来了。
通过流量分析,我们发现网络确实存在问题。
从流量数据中我们看到 A 服务的通信流量在故障时间那一刻有一个数据包在不停的被重传,
至少耗费了 13s 这个包才传输成功。
仔细分析这个可以的数据包,我们发现 Id(Identification) 字段都一样,
但是每次 TTL 都减二,
从这里可以进一步得出结论,这个数据包是在两个设备之间来回传递(TTL 减二),
这两个设备应该是核心交换机和接入交换机,
可以合理猜测当时核心交换机和接入交换机之前可能存在环路,
这可能是一个临时的环路,网络硬件本身没问题,可能是某些路由规则出错导致这些环路。
那,这些环路具体是怎么产生的呢?
确定网络有问题后大概花了一天半时间狂补 k8s 网络方面的知识。
当前 k8s 网络的 CNI 用的是 kube-router,
而 kube-router 最大的特定是使用 BGP 协议实现 k8s 集群内外的网络打通,
这也是容器和虚拟机网络方面最大的区别。
会不会是 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 断开瞬间,接入交换机清掉了通向容器实例的路由记录,
而接入交换机的默认路由又设置成了核心交换机,
如此一来,核心交换机会把发送到 A 服务的数据包转发的接入交换机,
接入交换机由于它和服务 A 的路由都被清空了,又把这些数据包发送给默认路由也就是核心交换机,
在核心交换机同步到路由变更之前,它和接入交换机之间就形成了一个临时环路。
为什么 BGP peer 会断掉呢?
这个其实已经很容易猜到了—— kube-router 挂掉。
通过 k8s 日志我们看到很多 kube-router restart 的记录, 时间上和服务超时的也完全吻合。
通过手工的模拟,删除 kube-router 实例,完美复现出 BGP peer 断开的问题。