K8s Node 从垃圾回收到资源残留

2024/11/14 20:14 pm posted in  Kubernetes

众所周知,K8s 的控制器是面向终态的控制循环模式,一般的控制循环不会考虑复杂状态流转,而是计算当前和预期的状态差异,进行调整。但 Kubelet 是个例外,作为实际的资源控制者,有太多需要考虑的细节,因此许多非核心操作均是异步执行,包括资源的垃圾回收。

什么是垃圾

Kubelet 视角的 Pod 状态

Kubelet 的控制循环中有大量的状态参数,这些异步的操作均依赖于这些状态的准确性。但 Pod 依赖的组件和资源多种多样,早期的 K8s 版本,因为状态不准确、不一致、遗漏、竟态条件,导致的 Kubelet 行为异常的 Bug 比比皆是。

为了解决这些问题,Kubelet 提供了两个不同的状态子系统::

  • podManager:反映 Pod 从 APIServer 角度看的状态,也就是期望状态
  • podWorkers:反映 Pod 从 Kubelet Worker 透出的状态,也就是事实状态

根据这两个状态以及对比,就可以算出当前 Pod 需要进行的操作,以及某些操作是否安全。资源垃圾回收和两个 podWorkers 状态相关:

状态名 含义 可能的情况
ShouldPodContentBeRemoved 是否应该积极清理所有和 Pod 相关的资源 Pod 被驱逐 or Pod 被删除并且容器停止
ShouldPodRuntimeBeRemoved 是否应该积极清理 Pod 的运行时资源 所有的容器已停止

容器回收策略

容器的回收以 1分钟为间隔定期触发,不支持修改频率。其规则受到很多条件的限制,但基本可以分为下面几个情况。

  1. 清理退出 Pod 的容器,如果 Pod 在 podWorkers 中处于下面的状态,就触发容器回收:
    1. Pod 处于ShouldPodContentBeRemoved
    2. Full GC 并且 Pod 处于 ShouldPodRuntimeBeRemoved
  2. 单个Pod 的非运行容器数超出限制--maximum-dead-containers-per-container
  3. 总非运行容器数超出限制--maximum-dead-containers
  4. 如果 2 和 3 无法同时满足,则优先保证 3

对于 k8s 管理的容器来说,不要通过 docker 等工具自行进行容器回收,有一些退出容器起到占位符的作用,如果被删除,k8s 可能会重启把容器拉起。比如 docker system prune 会清理掉 init container 的退出容器,Kubelet 会误认为 init container 未执行,尝试将 init container 重新拉起(如果你的 Pod 无法重入,那就惨了)。

Sandbox 回收策略

Pod 生命周期主流程里并没有同步的去删除 Sandbox,而是依赖 GC 的能力完成 Sandbox 清理。

Sandbox 残留是 containerd 场景下常见的问题,如果 containerd 部分资源释放有问题,比如挂载点问题,偶见容器已经清理但 Sandbox 还在。如果经年累月,可能会出现这么一个错误日志(https://github.com/kubernetes/kubernetes/issues/63858):

grpc: received message larger than max (4195017 vs. 4194304)

Sandbox GC 发生在 Container GC 之后,基本策略是:

  1. 清理退出 Pod 的 Sandbox:
    1. Pod 处于ShouldPodContentBeRemoved,删除和 Pod 关联的全部 Sandbox
    2. Full GC 并且 Pod 处于 ShouldPodRuntimeBeRemoved,删除和 Pod 关联的全部 Sandbox
  2. 如果 Pod 还在运行中,则只保留一个 Sandbox

镜像回收策略

镜像的垃圾回收策略是个爱恨功能,有时候你觉得他太勤快了,仿佛容器刚停掉,镜像就需要重新再拉取一遍,有时候你感觉他太保守了,节点频繁在磁盘压力徘徊。

其实为了保持二者的平衡,镜像的回收策略略微复杂。和容器回收一样,镜像回收也是以一个固定的时间间隔触发,目前是 5min ,不支持自定义配置。

影响 image 回收的配置:

  • imageMaximumGCAge:如果设置,超过这个时间未使用镜像则回收
  • imageMinimumGCAge:虽然未使用,但小于这个值不会回收(避免清理刚拉取的镜像)
  • imageGCHighThresholdPercent:大于该阈值则进行清理
  • imageGCLowThresholdPercent:小于该阈值则结束清理

工作流程

Contaienr GC

Container GC 过程:

  1. 按创建时间,依次判断需要 GC 的容器
  2. 优雅退出,和执行 pre stop hook
  3. 调用 container runtime 操作:StopContainer
  4. 清理容器日志
  5. 调用 container runtime 操作:RemoveContainer

Sandbox GC 过程:

  1. 按创建时间,依次判断需要 GC 的 Sandbox
  2. 如果 Sandbox 包含任何容器,则跳过 GC
  3. 对需要 GC 的 Sandbox 依次执行 container runtime 操作:
    1. StopPodSandbox
    2. RemovePodSandbox

Image GC

镜像回收有两个重要的数据来源:

  1. imagesInUse :每次触发时获取正在使用的 image,并更新 imageRecords 中的使用时间
  2. imageRecords:记录了全量的 image,基于此数据进行可回收判断
    1. 使用中的不回收
    2. 标记为 pinned 的不回收(https://github.com/containerd/containerd/pull/7944
    3. 根据发现镜像和使用时间进行排序

基本回收流程:

  1. 如果开启了 imageMaximumGCAge 配置,则超过该时间的镜像会被回收
  2. 如果没有超过 imageGCHighThresholdPercent 则结束
  3. 尝试清理镜像到足够满足 imageGCLowThresholdPercent
    1. 在开始 gc 后拉取的镜像不清理
    2. 小于 imageMinimumGCAge 的镜像不清理
    3. 调用 container runtime 操作:RemoveImage