前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Kubernetes 实践:勿让 Docker Volume 引发 Terminating Pod

Kubernetes 实践:勿让 Docker Volume 引发 Terminating Pod

作者头像
CNCF
发布2021-01-27 10:50:03
5280
发布2021-01-27 10:50:03
举报
文章被收录于专栏:CNCFCNCF

问卷链接(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 的元数据如下:

代码语言:javascript
复制

在节点上通过 docker 命令查看发现 Terminating Pod 下的业务容器 de6d3812bfc8 仍然未被删除:

代码语言:javascript
复制

再通过 ctr 命令查看发现 Containerd 中仍然存有该容器的元数据:

代码语言:javascript
复制

我们怀疑是 Shim 的进程回收出现了问题,通过 ps 命令查找 de6d3812bfc8 容器的 Shim 进程准备获取栈进行分析,但是此时无法找到该容器的 Shim 进程以及业务进程。日志中查看到 Docker 和 Containerd 已经处理容器退出:

代码语言:javascript
复制

此时又有多个新的业务 Pod 被调度到该节点上,新调度 Pod 的容器一直处于 Created 状态。该现象和我们已知的几个 Docker 和 Containerd 问题是不同的:

代码语言:javascript
复制

综上所述,当前观察到的现象如下:

  • Kubelet 删除 Pod 的逻辑已被触发。
  • Docker 已经接收并处理 Kubelet 删除容器的请求。
  • 该容器的 Shim 进程以及业务进程已经被清理。
  • 某种原因导致 Docker 和 Containerd 中的元数据一直无法被删除。
  • 此后创建的容器一直处于 Created 状态。

原因分析

通过查看监控我们发现出现问题时该节点的磁盘利用率非常高并且 CPU 负载异常:

我们初步猜测该问题和异常的节点磁盘利用率有关。

为什么新调度 Pod 的容器一直处于 Created 状态

新调度 Pod 的容器一直处于 Created 状态是我们在 Docker 版本为 18.09.9 的环境遇到的新现象。针对该现象入手,我们在 Docker 栈中发现多个阻塞在包含 github.com/docker/docker/daemon.(*Daemon).ContainerCreate 函数的 Goroutine,并且阻塞的原因是 semacquire。其中一个 Goroutine 内容如下:

代码语言:javascript
复制

从栈的内容中我们发现该 Goroutine 阻塞在地址为 0xc000aee820 的 Mutex 上,并且该地址与 github.com/docker/docker/volume/local.(*Root).Get 的 Function Receiver 相同。让我们通过代码看看这个 Root 是怎样的数据结构:

代码语言:javascript
复制

Root 是 Volume 驱动的实现,用于管理 Volume 的生命周期。它缓存了所有的 Volume 并且由 Mutex 保护缓存数据的安全。github.com/docker/docker/volume/local.(*Root).Get 阻塞在237行等待 Mutex 的逻辑上,所以节点上新创建的容器一直处于 Created 状态:

代码语言:javascript
复制

看来新创建的容器一直处于 Created 状态只是结果,那么是谁持有这个地址为 0xc000aee820 的 Mutex 呢?

谁持有阻塞其他 Goroutine 的 Mutex

通过搜索阻塞在地址为 0xc000aee820 的 Mutex,我们找到了持有该 Mutex 的 Goroutine:

代码语言:javascript
复制

从 Goroutine 栈中我们看到 github.com/docker/docker/volume/local.(*Root).Remove 函数持有地址为 0xc000aee820 的 Mutex,并且执行到了217行,该函数负责调用 os.RemoveAll 函数删除指定的 Volume 以及数据:

代码语言:javascript
复制

通过观察 Goroutine 栈可以发现,os.RemoveAll 函数在栈中出现了两次,查看源码我们得知 os.RemoveAll 的实现采用了递归的方式。在109行包含递归调用的逻辑:

代码语言:javascript
复制

Goroutine 栈的最上层是 syscall.unlinkat 函数,即通过系统调用 unlinkat 删除容器的文件系统目录。我们发现了一个 Terminating Pod 的容器 Volume 有异常:

代码语言:javascript
复制

该目录文件大小超过了 500MB 但是 Link 计数只有1,通过查看 ext4 文档发现以下内容:

代码语言:javascript
复制

即当一个 ext4 文件系统下目录中的子目录个数超过64998时,该目录的 Link 会被置为1来表示硬链接计数已超过最大限制。对该目录下的文件进行遍历后我们发现有超过500万个空目录,已经远超过64998的限制。所以在第一次触发删除 Pod 逻辑后该节点的磁盘利用率一直居高不下并且 CPU 负载异常,Volume 文件删除过程非常缓慢导致所有相同业务的容器删除逻辑阻塞。通过查看相关代码可以确认在 Kubelet 删除容器时 Volume 也是一起被回收的:

代码语言:javascript
复制

为什么 Containerd 中的元数据一直无法被删除

还有一个疑问,为什么 ctr 命令可以查到需要被删除的容器元数据呢?我们发现了另一类等待该 Mutex 的 Goroutine:

代码语言:javascript
复制

该 Goroutine 栈中包含 github.com/docker/docker/daemon.(*Daemon).Cleanup 函数并且执行到了257行,该函数负责释放容器网络资源并反挂载容器的文件系统:

代码语言:javascript
复制

而该函数调用 Containerd 删除元数据在257行的 github.com/docker/docker/container.(*Container).UnmountVolumes 函数之后,这也解释了为什么通过 ctr 命令查看发现 Containerd 中仍然存有该容器的元数据。

真相大白

这些 Volume 多达 500MB 的容器是怎么来的呢?通过和用户沟通后我们得到了答案,原来用户没有理解 Docker Volume 的含义和使用场景,在 Dockerfile 中使用了 Volume:

代码语言:javascript
复制

用户在业务逻辑中频繁的向 Volume 写入数据并且未进行有效的垃圾回收,导致一段时间后大量空目录泄漏而触发 Terminating Pod 的问题。至此我们问题的原因就清晰了,Terminating Pod 问题产生的流程如下:

  • 某个业务的 Pod 中包含频繁的向 Volume 写入数据的逻辑导致文件硬链接计数超过最大限制。
  • 用户进行了一次滚动更新,触发 Pod 删除的时间被记录到 .metadata.deletionTimestamp
  • 删除 Docker Volume 的逻辑被调用,因 Volume 中的空目录过多导致 unlinkat 系统调用被大量执行。
  • 函数 os.RemoveAll 递归删除 Volume 目录时大量执行 unlinkat 系统调用导致该节点的磁盘利用率非常高并且 CPU 负载异常。
  • 第一个执行 Volume 删除逻辑的 Goroutine 持有保护 Root 缓存的 Mutex,因函数 os.RemoveAll 删除 Volume 目录时递归处理500万个文件过慢而无法返回,该节点上后续对 Volume 的操作均阻塞在等待 Mutex 的逻辑上。
  • 使用 Volume 的容器无法被删除,此时集群中出现多个 Terminating Pod。

总结

最后我们在线上环境采用了节点下线进行磁盘格式化再重新上线的方案进行紧急恢复,并且建议用户尽快弃用 Docker Volume 而改用 Kubernetes 本地盘方案。用户在我们的建议下修改了 Dockerfile 和编排模板,并对业务代码中的逻辑进行了优化,杜绝了此类问题。

在此过程中,轻舟 Kubernetes 增强技术团队同样受益匪浅,从技术的角度去解决问题只是我们工作的一个维度,用户对云原生技术的认知与服务方推行规范之间的差距更值得关注。虽然当前我们解决了用户使用 Kubernetes 的基本问题,但是在帮助用户切实解决云原生落地过程中的痛点、加深用户对新技术的理解、降低用户的使用成本并让用户真正享受到技术红利的道路上,我们还有很长的路要走。

作者简介

黄久远,网易数帆资深开发工程师,专注于云原生以及分布式系统等领域,参与了网易云音乐、网易传媒、网易严选、考拉海购等多个用户的大规模容器化落地以及网易轻舟容器平台产品化工作,主要方向包括集群监控、智能运维体系建设、Kubernetes 以及 Docker 核心组件维护等。当前主要负责网易轻舟云原生故障自动诊断系统的设计、开发以及产品商业化工作。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-01-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CNCF 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题背景
  • 原因分析
    • 为什么新调度 Pod 的容器一直处于 Created 状态
      • 谁持有阻塞其他 Goroutine 的 Mutex
        • 为什么 Containerd 中的元数据一直无法被删除
          • 真相大白
          • 总结
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档