Kubernetes 节点存储治理:容器镜像清理的原理、实践与自动化策略

1. 被忽视的“镜像蔓延”

随着 CI/CD 流水线的日益繁忙,Kubernetes 节点上的容器运行时(Container Runtime)会不断拉取新的应用镜像。然而,旧版本的镜像层、构建中间产物以及不再使用的依赖库往往会滞留在节点的本地存储中。这种“镜像蔓延”(Image Sprawl)现象会导致两个直接后果:

  1. 节点不稳定性: 当磁盘使用率达到阈值(默认 85%),Kubelet 会触发 DiskPressure 污点,阻止新 Pod 调度,甚至驱逐现有工作负载。
  2. 安全隐患与合规风险: 节点上残留的旧版本镜像可能包含已修复的 CVE 漏洞。即使未运行,这些镜像的存在也为潜在的攻击提供了利用工具。

本文将提供一套完整的指南,帮助运维人员从被动应对转向主动治理。


2. Kubelet 原生垃圾回收机制

Kubernetes 默认包含了一套基于水位的被动垃圾回收(Garbage Collection, GC)逻辑。理解其工作方式是进行任何干预的前提。

2.1 迟滞循环

Kubelet 的 GC 并非实时触发,而是基于 imagefs(镜像文件系统)的使用率阈值。为了防止在临界点频繁触发清理造成系统震荡,K8s 引入了高低水位机制:

  • 触发阈值 (imageGCHighThresholdPercent):默认为 85%。当磁盘使用率超过此值,GC 启动。
  • 目标阈值 (imageGCLowThresholdPercent):默认为 80%。GC 会持续删除镜像,直到使用率降至此值以下。

2.2 驱逐算法

当 GC 触发时,Kubelet 并非随机删除,而是遵循 LRU(最近最少使用)原则:

  1. 过滤: 排除当前被容器引用的镜像。
  2. 保护: 排除“太新”的镜像(由 imageMinimumGCAge 控制,默认 2 分钟),防止刚刚拉取尚未启动的镜像被误删。
  3. 排序与执行: 按最后使用时间排序,优先删除最久未使用的镜像。

痛点: 原生 GC 是盲目的。它无法响应“清理特定高危镜像”或“释放特定空间”的需求,且默认阈值对于某些存储敏感型业务来说过高。


3. 实践篇:CRI 环境下的精准清理

自 Kubernetes v1.24 移除 Dockershim 后,我们无法再通过 docker rmi 管理镜像。现在的标准操作是使用符合 CRI(容器运行时接口)标准的工具,即 crictl。

crictl的全称是 container runtime Interface Control ,明白全名称后可以方便记忆这个命令

3.1 核心工具:crictl

crictl 是 Kubernetes 社区官方推荐的 CLI 工具,直接与 containerd 或 CRI-O 通信。

常用命令:

# 查看所有镜像及其 ID
crictl images

# 删除指定镜像(推荐使用 Image ID 以避免歧义)
crictl rmi sha256:7a80323521...

# 清理所有未使用的镜像(Prune)
crictl rmi --prune
# 这里注意,crictl会 默认等待一段时间,如果有大量镜像需要删除,磁盘响应慢,这个命令就会报错,可以通过timeout 参数增加超时时间
crictl --timeout 300s rmi --prune

3.2 进阶技巧:无 SSH 访问节点的“隔空取物”

在生产环境中,直接 SSH 登录节点通常被安全策略禁止。此时,我们可以利用 kubectl debug 启动一个特权临时容器,挂载宿主机根目录来执行清理。

操作步骤:

  1. 启动特权调试 Pod:

    kubectl debug node/<节点名称> -it --image=ubuntu --profile=sysadmin
    

    注:--profile=sysadmin 会自动配置特权模式。

  2. 切换根目录(Chroot):
    进入 Pod 后,宿主机文件系统挂载在 /host。

    chroot /host
    
  3. 执行清理:
    此时可以直接调用宿主机的 crictl 进行操作。

    crictl rmi --prune
    

4. 自动化篇:引入 Eraser 进行声明式管理

对于大规模集群,手动清理是不可持续的。Eraser 是一个 CNCF Sandbox 项目,旨在通过声明式的方式自动清理节点镜像。

4.1 工作原理

Eraser 采用 Controller-Worker 架构。当管理员下发策略时,Eraser 会在每个节点调度一个 Pod,按顺序执行“扫描 -> 过滤 -> 删除”的流水线。

4.2 实战:清理特定镜像

通过应用 ImageList CRD,我们可以强制集群所有节点删除指定镜像(例如修复 Log4j 漏洞的旧版镜像):

apiVersion: eraser.sh/v1
kind: ImageList
metadata:
  name: remove-vulnerable-alpine
spec:
  images:
    - docker.io/library/alpine:3.7.3

Eraser 甚至支持接入 Trivy 等扫描器,实现“自动删除包含高危漏洞且未运行的镜像”,将运维从繁琐的清理脚本中解放出来。


5. 警示:镜像清理的安全风险

在执行清理(特别是手动清理)时,必须警惕 Kubernetes 的 imagePullPolicy: IfNotPresent 策略带来的潜在风险。

风险场景

假设节点上缓存了镜像 A(私有镜像),且某 Pod 正依赖该缓存运行(配置为 IfNotPresent 且未配置 imagePullSecrets)。

  1. 清理前: Pod 正常运行,因为 Kubelet 直接使用了本地缓存,跳过了鉴权。
  2. 清理后: 管理员清理了镜像 A。
  3. 故障爆发: Pod 重启或发生故障转移。Kubelet 尝试重新拉取镜像,但因缺少 imagePullSecrets 而鉴权失败,导致 ImagePullBackOff,业务中断。

最佳实践:

  • 显式声明 Secrets: 无论镜像是否存在本地,Deployment 中都应配置正确的 imagePullSecrets。
  • 避免滥用 IfNotPresent: 生产环境尽量使用 Always 策略(配合镜像仓库的缓存机制),或确保所有节点的一致性。
  • 离线环境慎重: 在无法访问公网的 Air-gapped 环境中,过度激进的 GC 可能导致无法恢复的故障,建议调高 GC 阈值或禁用自动 GC。

6. 总结

Kubernetes 的节点镜像管理需要从“被动依赖 GC”转向“主动生命周期管理”。

  • 对于紧急磁盘释放,使用 kubectl debug 配合 crictl rmi --prune 是最快手段。
  • 对于长期合规与治理,建议引入 Eraser 进行自动化管理。
  • 对于日常配置,根据业务负载合理调整 Kubelet 的 imageGCHighThresholdPercent 参数(建议 70%-80%)。

更多推荐