Atlantis
GitHub Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

8. 容器日志丢失

1. 前言

日志轮转的逻辑一般都是设置一个日志文件大小上限,然后由外部工具或者程序本身实现:删除最老日志文件、重命名日志文件、重新打开日志文件,像 nginx 是依赖 logrorate 进程发送信号重新打开日志文件,而较新版本的 kubelet 是调用 CRI 接口中的 ReopenContainerLog 来触发重新打开日志文件,这里记录一个老版本 kubelet 与 Docker 日志轮转之间的矛盾。

2. STAR

2.1 Situation

某客户的集群中部署了 CLS 产品用于收集容器日志,该产品利用 fluent-bit 采集 /var/log/containers 目录下的容器日志文件上报到日志服务后台,容器日志文件名使用正则表达式匹配,客户反馈某些节点上的容器日志缺失。

2.2 Task

确定容器日志丢失原因,先手动修复解决使用问题,再将代码修改合入下一个版本。

2.3 Action

2.3.1 初步排查

日志丢失的原因可能是日志服务异常,也可能是日志文件异常,初步排查后确认到一个异常 Pod,该 Pod 所在节点上 /var/log/containers 目录下日志文件软链接缺失,但 /var/log/pods 目录下的 Pod 目录、容器目录、容器日志文件软链接都存在,正常指向 /var/lib/docker/containers 目录下的容器日志,执行 kubectl logs 命令查看日志也有正常输出。看起来需要排查一下源码了。

2.3.2 源码分析

客户使用的 K8s 集群版本为 1.18.18,容器运行时为 Docker,这样组合下,容器日志文件的软链接关系如下:

flowchart TD
    A["/var/log/containers/容器日志.log"]
    B["/var/log/pods/容器组/容器/x.log"]
    C["/var/lib/docker/containers/容器ID/容器ID-json.log"]
    A ---> B
    B ---> C

如果容器发生过重启,/var/log/pods 的容器组目录下会有多个软链接,数字最大的那个指向当前运行中的容器:

alt text

每个目录各自按 pod 名、容器名、容器 ID规划路径:

  1. /var/log/containers:pod名-命名空间-容器名-容器ID.log
  2. /var/log/pods:pod名/容器名/x.log,x为数字,每重启一次容器,数字+1
  3. /var/lib/docker/containers:容器ID/容器ID-json.log

Pod 是 K8s 的最小资源管理单位,在 Pod 创建并正常运行后,若 Pod 中的容器发生了重启,那么日志发生变化的目录是 /var/log/containers 与 /var/lib/docker/containers,/var/log/pods 起到了一个衔接的作用,使用软链接串联起两个目录。

进一步分析源码,可以看到启动容器后,有一步是创建软链接,当 /var/log/pods 下的容器日志文件存在时,就会创建一个软链接到 /var/log/containers 下,这一步如果出错,/var/log/containers 下就没有对应的软链接了。

alt text

当我们删除 Pod 时也会删除日志软链接:

alt text

还有一种情况是垃圾回收:

alt text

继续排查发现官方也有一个问题单,初步判断是 kubelet 的垃圾回收机制存在问题,1.18.18 版本的 kubelet 尚未合入以下问题修复:

  1. OCP 问题单: https://bugzilla.redhat.com/show_bug.cgi?id=1814859
  2. K8s 问题修复单: https://bugzilla.redhat.com/show_bug.cgi?id=1814859

简单来说,就是容器日志输出太快,当日志轮转撞上 kubelet 的容器垃圾回收(1分钟执行一次)时,垃圾回收逻辑中看到日志软链接指向了一个不存在的文件(日志轮转期间需要移动和重命名日志文件导致原始文件暂时不存在),就有可能删除软链接,导致出现日志丢失问题,问题单中的复现方法如下:

alt text

由于工作节点上的 Docker 默认配置了 100MB 的容器日志文件限制,只需创建三个大量输出日志 Pod,确保在 1 分钟内可触发数次轮转即可复现问题,我的办法是使用 shell 脚本循环输出一个超长的 base64 字符串。

2.4 Result

修复方法不复杂,在 kubelet 的垃圾回收逻辑中,删除日志软链接前先检查对应的 Pod 和容器是否存活,比较麻烦的是需要替换存量节点的 kubelet 二进制文件文件并重启 kubelet 进程,可能影响业务。

我先编译出了修复后 kubelet 二进制文件,重新制作添加节点时使用的容器镜像(添加节点时会拉取镜像拷贝一些二进制文件到本地),更新到现场的公共镜像仓库,确保新增节点不再出现类似问题,然后提供了一个在节点上运行的脚本,该脚本会执行拉取镜像、拷贝 kubelet 二进制、重启 kubelet 的操作,理论上重启 kubelet 并不影响 Pod 运行,驻场同事在业务低峰期完成了存量集群的问题修复。