上周,我们的Kubernetes集群升级到1.30版本之后,线上出了一个诡异的Bug。

部分Pod会随机出现网络不通的情况,持续几分钟之后又自动恢复。这个Bug没有明显的规律,复现困难,排查起来非常棘手。我排查了整整一夜,终于找到了原因。

这篇文章,我想分享一下这次排查的过程,以及最终的解决方案。希望能给遇到类似问题的朋友一些参考。

Bug现象

先说说Bug的现象。

我们的集群有一百多个节点,跑着几百个服务。升级到K8s 1.30之后,用户开始反馈,部分服务偶尔会出现超时和502错误。

一开始我们以为是应用的问题,但排查之后发现,出问题的Pod,日志里没有任何异常。Pod本身是正常运行的,但就是网络不通,从其他Pod访问它会超时,从它访问外部服务也会超时。

更奇怪的是,这个问题持续几分钟之后,会自动恢复。恢复之后,一切正常,就像什么都没发生过一样。而且,出问题的Pod是随机的,没有规律,今天这个Pod出问题,明天那个Pod出问题。

我们一开始以为是网络抖动,但监控显示网络没有问题。也以为是节点的问题,但出问题的Pod分布在不同的节点上。

这个Bug最让人头疼的地方就是:没有明显的规律,复现困难,而且自动恢复。每次我们想深入排查的时候,它就自己好了。

初步排查

拿到Bug之后,我开始了初步排查。

第一步,查看Pod的状态。出问题的时候,Pod的状态是Running,重启次数没有增加,说明Pod本身没有崩溃。容器内的进程也在正常运行,没有异常。

第二步,查看节点的状态。出问题的节点,CPU、内存、磁盘都正常,没有资源耗尽的情况。节点的网络也正常,没有丢包和延迟。

第三步,查看kubelet的日志。在出问题的节点上,kubelet的日志里有一些警告,但都是一些不相关的信息,没有明显的错误。

第四步,查看kube-proxy的日志。kube-proxy负责Service的负载均衡,网络问题可能和它有关。但kube-proxy的日志里也没有明显的错误。

第五步,查看CNI插件的日志。我们用的是Calico作为CNI插件。Calico的日志里有一些关于路由的信息,但看起来都是正常的。

初步排查没有找到原因,问题变得更诡异了。

深入排查

初步排查没有结果,我开始深入排查。

第一步,在出问题的Pod里抓包。当某个Pod出现网络不通的时候,我立刻kubectl exec进去,用tcpdump抓包。发现Pod发出的数据包,没有任何回应。说明数据包发出去了,但没有回来。

第二步,在节点上抓包。我在出问题的节点上,同时抓Pod的虚拟网卡和物理网卡的包。发现Pod发出的数据包,到了虚拟网卡,但没有到物理网卡。说明数据包在节点内部被丢弃了。

第三步,查看节点的iptables规则。K8s的网络很大程度上依赖iptables。我对比了正常节点和出问题节点的iptables规则,发现出问题的节点上,有一些KUBE-SERVICES链的规则不见了。

这是一个重要的线索。kube-proxy负责维护iptables规则,如果规则不见了,说明kube-proxy出了问题。

第四步,查看kube-proxy的详细日志。我把kube-proxy的日志级别调到debug,然后等待问题复现。等了两个小时,问题终于复现了。在日志里,我发现了一个错误:"Failed to update iptables rules: iptables-restore failed"。

原来,kube-proxy在更新iptables规则的时候失败了,导致部分规则丢失,所以部分Pod的网络不通。过了几分钟,kube-proxy重试成功,规则恢复了,网络也就恢复了。

根因分析

找到了kube-proxy更新iptables失败这个线索之后,我开始分析根因。

为什么kube-proxy更新iptables会失败呢?我查看了kube-proxy的完整日志,发现失败的原因是:"iptables-restore: line 42 failed: No chain/target/match by that name"。

也就是说,iptables-restore在执行的时候,引用了一个不存在的链。这说明,在kube-proxy生成iptables规则的时候,有一些依赖的链还没有创建,或者已经被删除了。

我继续追查,发现这个问题和K8s 1.30的一个变更有关。

在K8s 1.30中,kube-proxy引入了一个新的功能:iptables规则的增量更新。以前,kube-proxy每次更新规则的时候,都是全量刷新,把所有规则删掉再重新写。1.30之后,改成了增量更新,只更新变化的部分,这样可以减少iptables的操作,提升性能。

但这个增量更新有一个Bug:在某些并发场景下,规则的更新顺序可能出错,导致引用了还没创建的链。具体来说,当Service和Endpoint同时变化的时候,kube-proxy可能先更新了Service的规则,但Endpoint对应的链还没创建,导致iptables-restore失败。

这个Bug在K8s 1.30的早期版本中存在,后来在1.30.3中修复了。我们当时用的是1.30.1,所以遇到了这个问题。

解决方案

找到了根因之后,解决方案就很简单了。

第一个方案是升级K8s版本。把集群从1.30.1升级到1.30.3,这个Bug已经被修复了。我们在测试环境验证了1.30.3,确认问题不再复现之后,就把生产集群升级了。

第二个方案是降级kube-proxy。如果暂时不能升级整个集群,可以只把kube-proxy降级到1.29版本,因为1.29还没有引入增量更新的功能,不会有这个Bug。我们在升级之前,临时用了这个方案,先把kube-proxy降级,稳定了线上环境。

第三个方案是关闭增量更新。在kube-proxy的配置中,有一个参数可以关闭增量更新,回到全量刷新的模式。虽然性能会差一些,但可以避免这个Bug。如果既不能升级也不能降级,可以用这个方案临时规避。

我们最终采用了第一个方案,把集群升级到了1.30.3。升级之后,问题再也没有出现过。

排查经验总结

这次排查,花了我整整一夜的时间。总结一下经验:

第一,对于随机出现、自动恢复的Bug,要有耐心。这种Bug最难排查,因为你不知道什么时候会复现。要做好长期作战的准备,把监控和日志都准备好,等待复现的时机。

第二,网络问题要分层排查。从Pod到节点,从虚拟网卡到物理网卡,从iptables到路由,一层一层地排查,找到数据包在哪里丢了。抓包是最有效的手段,能让你看到真实的网络流量。

第三,关注版本变更。升级版本之后出现的问题,大概率和版本变更有关。要仔细阅读版本更新日志,了解有哪些重大变更,尤其是网络、存储、调度这些核心组件的变更。

第四,不要忽视日志中的警告。很多时候,Bug的线索就藏在那些看起来不相关的警告日志里。要仔细阅读日志,不放过任何异常信息。

第五,善用社区资源。K8s是开源项目,遇到问题的时候,可以去GitHub的issue里搜索,看看有没有人遇到过类似的问题。很多时候,你遇到的Bug,别人已经遇到过,甚至已经有解决方案了。

后续改进

这次Bug之后,我们做了一些改进,避免类似问题再次发生。

第一,完善了版本升级流程。以后升级K8s版本之前,要仔细阅读changelog,了解所有重大变更。在测试环境充分验证之后,再升级生产环境。而且,不要升级到太新的版本,至少要等第一个补丁版本(.1)之后再考虑。

第二,加强了监控。我们增加了对kube-proxy的监控,包括iptables规则数量、规则更新失败次数、规则更新延迟等。如果出现规则更新失败,会立刻告警,不需要等用户反馈。

第三,建立了快速回滚机制。如果升级之后出现问题,可以快速回滚到之前的版本。这次我们就是因为有快速回滚机制,才能在发现问题之后,先降级kube-proxy稳定环境,再慢慢排查。

第四,定期进行混沌工程测试。我们会定期在测试环境中注入故障,比如删除iptables规则、断开网络、杀死进程等,验证系统的容错能力。这样可以在问题影响线上之前,就发现和修复潜在的问题。

写在最后

K8s是一个复杂的系统,版本升级可能会引入各种意想不到的Bug。这次的iptables增量更新Bug,就是一个典型的例子。

排查这种Bug,需要耐心、细心,还有扎实的技术基础。要从现象出发,一层层深入,直到找到根因。这个过程虽然辛苦,但找到原因的那一刻,所有的付出都是值得的。

希望这篇文章能给正在排查K8s问题的朋友一些参考。如果你也有类似的排查经历,欢迎在评论区交流。