13.4 Systemd是硬骨头

Systemd是相当复杂的一个组件,尤其是对没有做过相关开发工作的人来说。排查Systemd的问题我们用到了四个方法:使用(调试级别)日志、core dump、代码分析,以及live debugging。其中第一个、第三个和第四个结合起来使用,让我们在经过几天的“鏖战”之后,找到了问题的原因。但是这里我们先从“没用”的core dump说起。

13.4.1 “没用”的core dump

因为重启Systemd解决了问题,而这个问题本身,是runC在使用dbus和Systemd通信的时候没有了响应,所以我们需要验证的第一件事情,就是Systemd是不是有关键线程被锁住了。core dump里所有的线程,只有图13-12中的线程,此线程并没有被锁住,它在等待dbus事件,以便做出响应。

图13-12 Systemd主线程调用栈

13.4.2 零散的信息

因为无计可施,所以只能做各种测试、尝试。使用busctl tree命令,可以输出所有bus上对外暴露的接口。从输出结果看来,org.freedesktop.systemd1这个bus是不能响应接口查询请求的,如图13-13所示。

图13-13 D-Bus总线报错

使用下面的命令观察org.freedesktop.systemd1上接收到的所有请求,可以看到,在正常系统里,有大量unit创建、删除的消息,但是在有问题的系统里,这个bus上完全没有任何消息,如图13-14所示。

图13-14 D-Bus监控数据

分析问题发生前后的系统日志,runC在重复地运行一个测试(test),如图13-15所示,这个测试非常频繁,但是当问题发生的时候,这个测试就停止了。所以直觉告诉我们,这个问题可能和这个测试有很大的关系。

图13-15 Systemd可疑信息输出

另外,我们使用systemd-analyze命令打开了Systemd的调试级别日志,发现Systemd有Operation not supported的报错,如图13-16所示。

图13-16 Systemd不相关报错

根据以上零散的信息,可以得出一个大概的结论:org.freedesktop.systemd1这个bus在经过大量unit创建、删除之后,没有了响应。而这些频繁的unit创建、删除测试,是runC某一个改动引入的。这个改动使得UseSystemd函数通过创建unit来测试Systemd的功能。UseSystemd在很多地方被调用,比如创建容器、查看容器性能等操作。

13.4.3 代码分析

这个问题在线上所有Kubernetes集群中,发生的频率大概是一个月两例。问题一直在发生,且只能在问题发生之后通过重启Systemd来处理,风险极大。我们分别向Systemd和runC社区提交了bug,但是一个很现实的问题是,他们并没有像阿里云这样的线上环境,重现这个问题的概率几乎是零,所以这个问题没有办法指望社区来解决。硬骨头还得我们自己啃。

在上一节最后我们看到了,问题出现的时候,Systemd会输出一些Operation not supported报错。这个报错看起来和问题本身风马牛不相及,但是直觉告诉我们,这或许是离问题最近的一个地方,所以我们决定,先搞清楚这个报错因何而来。

Systemd的代码量比较大,而报这个错误的地方非常多。通过大量的代码分析,我们发现有几处比较可疑的地方,发现了这些可疑的地方,接下来需要做的事情就是等待。在等了三周以后,终于有线上集群再次出现了这个问题。

13.4.4 Live Debugging

在征求用户同意之后,我们下载Systemd调试符号,将gdb挂载到Systemd上,在可疑的函数下断点,然后继续执行。经过多次验证,发现Systemd最终“踩”到了sd_bus_message_seal这个函数里的EOPNOTSUPP报错,如图13-17所示。

这个报错背后的道理是,systemd使用了一个变量cookie,来追踪自己处理的dbus message。每次在加封一个新的message的时候,systemd会先给cookie的值加1,然后再把这个值复制给这个新的message。

我们使用gdb打印出dbus->cookie这个值,可以很清楚地看到,这个值超过了0xffffffff。看来问题是Systemd在加封过大量message之后,cookie这个值溢出了,导致新的消息不能被加封,从而使得Systemd对runC没有了响应,如图13-18所示。

图13-17 Systemd消息处理函数

图13-18 cookie溢出

另外,在一个正常的系统中,使用gdb把bus->cookie这个值改到接近0xffffffff,然后就能观察到,问题在cookie溢出的时候立刻出现,这就证明了我们的结论。

13.4.5 怎么判断集群节点NotReady是这个问题导致的

首先我们需要在有问题的节点上安装gdb和Systemd debuginfo,然后用命令gdb/usr/lib/systemd/systemd 1把gdb attach到Systemd,在函数sd_bus_send上设置断点,然后继续执行。等Systemd“踩”到断点之后,用p/x bus-> cookie查看对应的cookie值,如果此值超过了0xffffffff,那么cookie就溢出了,则必然导致节点NotReady的问题。确认完之后,可以使用quit来detach调试器。