问卷链接(https://www.wjx.cn/jq/97146486.aspx)
作者:黄久远,网易数帆深开发工程师
Terminating Pod 是业务容器化后遇到的一个典型问题,诱因不一。本文记录了网易数帆-轻舟 Kubernetes 增强技术团队如何一步步排查,发现 Docker Volume 目录过多导致 Terminating Pod 问题的经历,并给出了解决方案。希望本文的分享对读者排查及规避同类问题有所帮助。
最近用户的集群中又出现了某个节点上的 Pod 长时间处于 Terminating 状态的问题。起初我们以为是 18.06.3 版本的几个经典的 Docker 和 Containerd 问题导致的,但是登陆出现问题的节点后发现环境如下:
Component | Version |
---|---|
OS | Debian GNU/Linux 10 (buster) |
Kernel | 4.19.87-netease |
Docker | 18.09.9 |
Containerd | 1.2.10 |
Kubernetes | v1.13.12-netease |
Terminating Pod 的元数据如下:
在节点上通过 docker
命令查看发现 Terminating Pod 下的业务容器 de6d3812bfc8
仍然未被删除:
再通过 ctr
命令查看发现 Containerd 中仍然存有该容器的元数据:
我们怀疑是 Shim 的进程回收出现了问题,通过 ps
命令查找 de6d3812bfc8
容器的 Shim 进程准备获取栈进行分析,但是此时无法找到该容器的 Shim 进程以及业务进程。日志中查看到 Docker 和 Containerd 已经处理容器退出:
此时又有多个新的业务 Pod 被调度到该节点上,新调度 Pod 的容器一直处于 Created 状态。该现象和我们已知的几个 Docker 和 Containerd 问题是不同的:
综上所述,当前观察到的现象如下:
通过查看监控我们发现出现问题时该节点的磁盘利用率非常高并且 CPU 负载异常:
我们初步猜测该问题和异常的节点磁盘利用率有关。
新调度 Pod 的容器一直处于 Created 状态是我们在 Docker 版本为 18.09.9 的环境遇到的新现象。针对该现象入手,我们在 Docker 栈中发现多个阻塞在包含 github.com/docker/docker/daemon.(*Daemon).ContainerCreate
函数的 Goroutine,并且阻塞的原因是 semacquire
。其中一个 Goroutine 内容如下:
从栈的内容中我们发现该 Goroutine 阻塞在地址为 0xc000aee820
的 Mutex 上,并且该地址与 github.com/docker/docker/volume/local.(*Root).Get
的 Function Receiver 相同。让我们通过代码看看这个 Root
是怎样的数据结构:
Root
是 Volume 驱动的实现,用于管理 Volume 的生命周期。它缓存了所有的 Volume 并且由 Mutex 保护缓存数据的安全。github.com/docker/docker/volume/local.(*Root).Get
阻塞在237行等待 Mutex 的逻辑上,所以节点上新创建的容器一直处于 Created 状态:
看来新创建的容器一直处于 Created 状态只是结果,那么是谁持有这个地址为 0xc000aee820
的 Mutex 呢?
通过搜索阻塞在地址为 0xc000aee820
的 Mutex,我们找到了持有该 Mutex 的 Goroutine:
从 Goroutine 栈中我们看到 github.com/docker/docker/volume/local.(*Root).Remove
函数持有地址为 0xc000aee820
的 Mutex,并且执行到了217行,该函数负责调用 os.RemoveAll
函数删除指定的 Volume 以及数据:
通过观察 Goroutine 栈可以发现,os.RemoveAll
函数在栈中出现了两次,查看源码我们得知 os.RemoveAll
的实现采用了递归的方式。在109行包含递归调用的逻辑:
Goroutine 栈的最上层是 syscall.unlinkat
函数,即通过系统调用 unlinkat
删除容器的文件系统目录。我们发现了一个 Terminating Pod 的容器 Volume 有异常:
该目录文件大小超过了 500MB 但是 Link 计数只有1,通过查看 ext4
文档发现以下内容:
即当一个 ext4
文件系统下目录中的子目录个数超过64998时,该目录的 Link 会被置为1来表示硬链接计数已超过最大限制。对该目录下的文件进行遍历后我们发现有超过500万个空目录,已经远超过64998的限制。所以在第一次触发删除 Pod 逻辑后该节点的磁盘利用率一直居高不下并且 CPU 负载异常,Volume 文件删除过程非常缓慢导致所有相同业务的容器删除逻辑阻塞。通过查看相关代码可以确认在 Kubelet 删除容器时 Volume 也是一起被回收的:
还有一个疑问,为什么 ctr
命令可以查到需要被删除的容器元数据呢?我们发现了另一类等待该 Mutex 的 Goroutine:
该 Goroutine 栈中包含 github.com/docker/docker/daemon.(*Daemon).Cleanup
函数并且执行到了257行,该函数负责释放容器网络资源并反挂载容器的文件系统:
而该函数调用 Containerd 删除元数据在257行的 github.com/docker/docker/container.(*Container).UnmountVolumes
函数之后,这也解释了为什么通过 ctr
命令查看发现 Containerd 中仍然存有该容器的元数据。
这些 Volume 多达 500MB 的容器是怎么来的呢?通过和用户沟通后我们得到了答案,原来用户没有理解 Docker Volume 的含义和使用场景,在 Dockerfile 中使用了 Volume:
用户在业务逻辑中频繁的向 Volume 写入数据并且未进行有效的垃圾回收,导致一段时间后大量空目录泄漏而触发 Terminating Pod 的问题。至此我们问题的原因就清晰了,Terminating Pod 问题产生的流程如下:
.metadata.deletionTimestamp
。unlinkat
系统调用被大量执行。os.RemoveAll
递归删除 Volume 目录时大量执行 unlinkat
系统调用导致该节点的磁盘利用率非常高并且 CPU 负载异常。Root
缓存的 Mutex,因函数 os.RemoveAll
删除 Volume 目录时递归处理500万个文件过慢而无法返回,该节点上后续对 Volume 的操作均阻塞在等待 Mutex 的逻辑上。最后我们在线上环境采用了节点下线进行磁盘格式化再重新上线的方案进行紧急恢复,并且建议用户尽快弃用 Docker Volume 而改用 Kubernetes 本地盘方案。用户在我们的建议下修改了 Dockerfile 和编排模板,并对业务代码中的逻辑进行了优化,杜绝了此类问题。
在此过程中,轻舟 Kubernetes 增强技术团队同样受益匪浅,从技术的角度去解决问题只是我们工作的一个维度,用户对云原生技术的认知与服务方推行规范之间的差距更值得关注。虽然当前我们解决了用户使用 Kubernetes 的基本问题,但是在帮助用户切实解决云原生落地过程中的痛点、加深用户对新技术的理解、降低用户的使用成本并让用户真正享受到技术红利的道路上,我们还有很长的路要走。
作者简介
黄久远,网易数帆资深开发工程师,专注于云原生以及分布式系统等领域,参与了网易云音乐、网易传媒、网易严选、考拉海购等多个用户的大规模容器化落地以及网易轻舟容器平台产品化工作,主要方向包括集群监控、智能运维体系建设、Kubernetes 以及 Docker 核心组件维护等。当前主要负责网易轻舟云原生故障自动诊断系统的设计、开发以及产品商业化工作。