第14章 节点就绪状态异常(二)

上一章介绍了一个集群节点就绪状态异常的问题。在那个问题中,我们的排查路径是从Kubernetes集群到容器运行时,到系统组件,再到D-Bus和Systemd,不可谓不复杂。

在本章中,我们和大家分享另外一个集群节点就绪状态的问题,以及问题的解决过程。这个问题和前一章的问题相比,排查方案完全不同,所以作为姐妹篇分享给大家。

14.1 问题介绍

这个问题的现象,也是集群节点会变成NotReady状态。问题可以通过重启节点暂时解决,但是在经过20天左右之后,问题会再次出现。问题出现之后,如果我们重启节点上的Kubelet,则节点会变成Ready状态,但这种状态只会持续三分钟。这是一个特别的情况,如图14-1所示。

图14-1 节点状态异常

示意图展示节点状态在重启Kubelet后短暂变为Ready,随后又变回NotReady。具体图片无法在此呈现,保留原图描述。

14.2 节点状态机

在具体分析这个问题之前,我们先来看一下集群节点就绪状态背后的大逻辑。在Kubernetes集群中,与节点就绪状态有关的组件主要有四个,分别是集群的核心数据库Etcd、集群的入口API Server、节点控制器(Node Controller),以及驻守在集群节点上,直接管理节点的Kubelet,如图14-2所示。

一方面,Kubelet扮演的是集群控制器的角色,它定期从API Server那里获取Pod等相关资源的信息,并依照这些信息,控制运行在节点上的Pod的执行;另外一方面,Kubelet作为节点状况的监视器,获取节点信息,并以集群客户端的角色,把这些状况同步到API Server中。

在这个问题中,Kubelet扮演的是第二种角色。Kubelet会使用上图中的NodeStatus机制,定期检查集群节点状况,并把节点状况同步到API Server中。而NodeStatus判断节点就绪状况的一个主要依据就是PLEG

PLEG是Pod Lifecycle Events Generator的缩写,它的执行逻辑,基本上是定期检查节点上Pod的运行情况,如果发现感兴趣的变化,PLEG就会把这种变化包装成Event,发送给Kubelet的主同步机制syncLoop处理。

但是,在PLEG的Pod检查机制不能定期执行的时候,NodeStatus机制就会认为,这个节点的状况是不对的,从而把这种状况同步到API Server中。

图14-2 节点状态机

展示Kubelet、API Server、Node Controller与Etcd之间的交互,以及PLEG在其中的位置。保持原图描述。

而最终把Kubelet上报的节点状况落实到节点状态的是节点控制这个组件。这里我们故意区分了Kubelet上报的节点状况和节点的最终状态,因为前者其实是我们describe node时看到的Condition,而后者是真正的节点列表里的NotReady状态,如图14-3所示。

图14-3 节点状态事件的上报

流程显示Kubelet上报Condition,Node Controller据此更新节点状态到NotReady。

14.3 就绪三分钟

在问题发生之后,我们重启Kubelet,节点三分钟之后才会变成NotReady状态。这个现象是分析问题的一个关键切入点。在解释它之前,请大家看一下官方PLEG实现架构图,第13章的图13-4。

这幅图片主要展示了两个过程。一方面,Kubelet作为集群控制器,从API Server处获取Pod定义的变化,然后通过创建Worker线程来创建或结束Pod;另一方面,PLEG定期检查容器状态,然后把状态以事件的形式反馈给Kubelet。在这里,PLEG有两个关键的时间参数,一个是检查的执行间隔,另一个是检查的超时。

在默认情况下,PLEG的检查间隔是1秒钟,换句话说,每一次检查过程执行之后,PLEG会等待1秒钟,然后进行下一次检查;而每一次检查的超时是3分钟,如果一次PLEG检查操作不能在3分钟内完成,那么这个状况会被上一节提到的NodeStatus机制当作集群节点NotReady的凭据,同步给API Server。

而我们之所以观察到节点会在重启Kubelet之后处于就绪状态3分钟,是因为Kubelet重启之后第一次PLEG检查操作就没有顺利结束,节点就绪直到3分钟超时之后才被同步到集群中。如图14-4所示,上面一行表示正常情况下PLEG的执行流程,下面一行则表示有问题的情况。relist是检查的主函数。

图14-4 relist操作流程

上方正常流程:relist每1秒执行,完成即开始下一轮。下方异常流程:relist卡住超过3分钟,触发NotReady。

14.4 止步不前的PLEG

了解了原理之后,我们来看一下PLEG的日志。日志基本上可以分为两部分,其中skipping pod synchronization这部分是Kubelet同步函数syncLoop输出的,说明它跳过了一次Pod同步;而剩余的PLEG is not healthy: pleg was last seen active <time> ago; threshold is 3m0s,则很清楚地展现了上一节提到的relist超时3分钟的问题。

能直接看到relist函数执行情况的,是Kubelet的调用栈。我们只要向Kubelet进程发送SIGABRT信号,golang运行时就会帮我们输出Kubelet进程的所有调用栈。需要注意的是,这个操作会“杀死”Kubelet进程。但是因为在这个问题中重启Kubelet并不会破坏重现环境,所以影响不大。

以下调用栈是PLEG relist函数的调用栈。从下往上我们可以看到,relist等在通过grpc获取PodSandboxStatus

使用PodSandboxStatus搜索Kubelet调用栈,很容易找到下面这个线程,此线程是真正查询Sandbox状态的线程,从下往上看,我们会发现这个线程在Plugin Manager里尝试去拿一个Mutex。

而这个Mutex只有在Plugin Manager里边能用到,所以我们查看所有Plugin Manager的相关调用栈。线程中一部分在等Mutex,而剩余的都是等在Terway CNI Plugin

关键发现:PLEG卡住的原因是Kubelet在调用Terway CNI插件时发生了阻塞,导致PLEG的relist超时。

14.5 无响应的Terwayd

在进一步解释这个问题之前,我们需要区分一下Terway和Terwayd。从本质上来说,Terway和Terwayd是客户端和服务器的关系,这跟Flannel和Flanneld之间的关系是一样的。Terway按照Kubelet的定义,实现了CNI接口的插件,如图14-5所示。

图14-5 Terway架构图

展示Kubelet通过CNI接口调用Terway(客户端),Terway通过rpc调用Terwayd(服务端)管理网络。

而在上一节最后我们看到的问题是,Kubelet调用CNI Terway去配置Pod网络的时候,Terway长时间无响应。正常情况下这个操作应该是秒级的,非常快速。而出问题的时候,Terway没有正常完成任务,因而我们在集群节点上看到大量Terway进程堆积,如图14-6所示。

图14-6 Terway进程堆积状态

图示显示节点上存在上千个Terway进程等待Terwayd响应。

同样,我们可以发送SIGABRT给这些Terway插件进程,来打印出进程的调用栈。下面是其中一个Terway的调用栈。这个线程在执行cmdDel函数,其作用是删除一个Pod网络相关配置。

以上线程通过rpc调用Terwayd来真正移除Pod网络,所以我们需要进一步排查Terwayd的调用栈,以进一步定位此问题。

Terwayd作为Terway的服务器端,接受Terway的远程调用,并替Terway完成其cmdAdd或者cmdDel来创建或者移除Pod网络配置。我们在上面的截图里可以看到,集群节点上有上千个Terway进程,它们都在等待Terwayd,所以实际上Terwayd里也有上千个线程在处理Terway的请求。使用下面的命令,可以在不重启Terwayd的情况下输出调用栈。

因为Terwayd的调用栈非常复杂,而且几乎所有的线程都在等锁,直接去分析锁的等待持有关系比较复杂。这个时候我们可以使用“时间大法”,即假设最早进入等待状态的线程,有很大概率是持有锁的线程。经过调用栈和代码分析,我们发现下面这个是等待时间最长(1595分钟),并且拿了锁的线程,而这个锁会阻碍所有创建或移除Pod网络的线程。

14.6 原因

深入分析前一个线程的调用栈,我们可以确定三件事情。第一,Terwayd使用了netlink这个库来管理节点上的虚拟网卡、IP地址及路由等资源,且netlink实现了类似于iproute2的功能;第二,netlink使用socket直接和内核通信;第三,以上线程在recvfrom系统调用上等待。

在这样的情况下,我们需要去查看这个线程的内核调用栈,才能进一步确认这个线程等待的原因。因为从goroutine线程编号不太容易找到线程所对应的系统线程ID,这里我们通过抓取系统的core dump来找出上面线程的内核调用栈。

在内核调用栈中搜索recvfrom,定位到下面这个线程。从下面的调用栈上,我们只能确定此线程在recvfrom函数上等待。

对于这个问题,进一步深入排查是比较困难的,这显然是一个内核问题,或者与内核相关的问题。我们找遍了整个内核core,检查了所有的线程调用栈,看不到其他可能与这个问题相关联的线程。

根因:netlink调用(通过 recvfrom)在内核中卡住,可能是内核bug或netlink socket问题,导致Terwayd永久阻塞。

14.7 修复

这个问题的修复基于一个假设,就是netlink并不是100%可靠的。netlink可能响应很慢,甚至完全没有响应。所以我们可以给netlink操作增加超时,从而保证就算某一次netlink调用不能完成,Terwayd也不会被阻塞。

修复方式:为netlink操作引入超时机制,避免Terwayd被无限阻塞。

14.8 总结

在节点就绪状态这种场景下,Kubelet实际上实现了节点的“心跳”机制。Kubelet会定期把节点相关的各种状态同步到集群管控中,这些状态包括内存、PID、磁盘,当然也包括本章中关注的就绪状态等。Kubelet在监控或者管理集群节点的过程中,使用了各种插件来直接操作节点资源,包括网络、磁盘,甚至容器运行时等插件,这些插件的状况,会直接影响Kubelet甚至节点的状态。

核心启示:底层插件(如CNI网络插件)的稳定性直接影响节点就绪状态;设计时应为可能的慢速或卡住操作增加超时保护。