containerd是容器运行时接口(CRI),将(很快)成为Zendesk的支持者。
新的容器运行时意味着需要新的调试技术。
其中许多技术是从我们在使用containerd 1.6.2时在暂存环境中观察到的问题中学习到的。
containerd堆栈由几个不同的二进制文件组成,所有这些二进制文件都执行一定的任务。
要正确调试containerd,我们需要知道如何检查每个组件。
非常基本的堆栈概述如下:
例如,当要创建一个pod时,kubelet使用CRI接口与containerd通信。containerd将fork / exec一个containerd-shim实例,该实例再调用runc二进制文件。由runc创建,运行,删除和清理容器。
1. Kubelet
从kubelet的角度调试containerd非常简单。我们要检查kubelet日志,看是否有任何明显的问题。
请注意,来自较低级别的问题(例如runc)希望冒泡到kubelet日志中。如果由于runc问题而无法创建容器,则kubelet可能会抱怨pod创建的截止日期已过。
确认kubelet已配置为实际使用containerd。需要设置标志--container-runtime=remote
和--container-runtime-endpoint=unix:///run/containerd/containerd.sock
。
2. Containerd
首先要检查的是服务状态和日志,与kubelet类似。
更进一步,我们可以查看containerd正在使用的配置文件,并可以将其与默认配置进行比较。如果你注意到任何问题,无论是日志行还是一些意外的配置,请查看问题。
crictl工具非常有用,因为它使你可以以与kubelet相同的方式与容器运行时进行交互。请参阅https://github.com/kubernetes/cri-api。
启用containerd上的调试非常简单,只需编辑containerd配置以启用套接字。
这允许你执行诸如转储containerd启动的所有goroutine,获取跟踪等操作。
3. containerd-shim
这里的调试方法可能有点过度,但如果你从kubelet进入此文章,并且现在到达此处,那么它可能值得!
每个shim都负责单个pod。如果你运行上面的ps aux --forest
命令,则应该看到:
- 全能的暂停容器
- 容器入口点中定义的进程
以上示例有点复杂,我们正在运行暂停容器,加上已运行bash脚本的sh。然后,这启动了presumably正在被馈入tee的aws-k8s-agent。最后,所有这些都通过logrecycler进行管道传输。
shim还充当子reaper,即它将reap zombie进程。如果shim进程消失,则下面的进程不会停止,而是被孤立到containerd。
如果由于某种原因containerd-shim未运行暂停容器,则可能存在创建pod沙箱的问题。请对有关节点进行健全性检查,并查看性能(这是一篇启发了本文格式的好文章)。
如果你怀疑containerd-shim出现问题,例如它消耗大量资源(它们通常会自愿睡眠),则可以执行许多操作。即,转储goroutine。
4. runc / container
这就是关键:调试的下一步取决于你要调试的问题类型。在这个级别上特定调试的位置非常依赖于你要调试的问题。接下来,我将提供有关runc的工作原理以及可以查找进一步故障排除的位置的一些见解。当一个容器作为 Kubernetes 进程的一部分被创建时,容器会在 <a class="af np" href="http://k8s.io" rel="noopener ugc nofollow" target="_blank"><em>http://k8s.io</em></a>
命名空间下创建(这不是一个 Kubernetes 命名空间,而是存储与 Kubernetes 相关的所有容器运行时 pod 状态的地方)。
要运行各种 runc 子命令,通常需要提供一个 --root
标志。这将是位于 <em>/run/containerd/runc/k8s.io/</em>
的状态目录。
当 runc 启动一个容器时,实际上是一些不同的动作:
runc create
创建一个容器,并在其中启动一个裸的runc init
进程。然后,这个runc init
等待另一侧打开 exec FIFO 文件,作为同步机制。一旦打开,它会向其中写入一个0
字节,并继续执行容器的入口点。runc start
实际上启动了该容器(通过打开 exec FIFO 文件并从中读取数据),并向runc init
发出信号,表示它应该继续进行。
来源:
@ https://github.com/opencontainers/runc/issues/3448
如果出现问题,例如 containerd 处于死锁状态就像我们之前看到的:
runc init
进程将永远等待runc start
runc init
进程会每隔几分钟创建一次- Pod 无法运行
- Pod 无法终止
感谢
非常感谢 Fu Wei 和 containerd 团队,他们帮助我们调试并最终解决了在我们的 staging 环境中遇到的死锁问题。
附录
可以通过运行 gdb,附加到进程并运行 generate-core-dump 来创建核心转储。我建议在传输前对这些核心转储进行 gzip 压缩(它们非常冗长,但是很适合压缩)。
这些核心转储可以与 delve 一起使用,但是如上所述,这些二进制文件必须具有调试符号!
请注意,附加调试器到任何正在运行的进程可能会显著减慢它的速度。这是因为调试器(如 delve 或 gdb)将钩子插入进程和导入库的各个部分。这些钩子是额外的代码路径,会增加执行时间。例如,可能已经存在性能问题的进程将受到更大的影响。
在生产环境中运行这些操作要小心。
链接和进一步阅读
- https://iximiuz.com/en/posts/implementing-container-runtime-shim/
- https://netflixtechblog.com/linux-performance-analysis-in-60-000-milliseconds-accc10403c55
译自:https://zendesk.engineering/debugging-containerd-a20f28a2a8bf
评论(0)