容器运行时快照泄漏:containerd的overlayfs孤立快照问题

概述

在容器化和 Kubernetes 集群的运维中,存储管理是保障系统可靠性和控制成本的关键一环。本文描述了一个在特定版本的 containerd 运行时中发现的严重问题:使用 overlayfs 快照驱动时,在容器(Pod)被正常删除后,其对应的可读写(RW)快照层会残留在磁盘目录和容器元数据的 BoltDB 数据库中,但与之相关的所有挂载引用都已消失,导致这些快照成为“孤立资源”。这类孤立快照不仅无法被 ctr、crictl 等标准工具发现或清理,还会持续消耗宝贵的磁盘空间,且缺乏有效的监控手段,是一个典型的“静默故障”和资源泄漏场景,对 SRE 的容量规划和系统可靠性构成了直接挑战。

核心概念

要理解此问题,需要明确以下几个核心概念:

  1. OverlayFS 快照:containerd 使用快照管理器来构建容器的文件系统层。OverlayFS 是一种常见的联合文件系统,它将多个目录(层)合并为一个统一视图。在运行一个可写容器时,containerd 会创建一个 RW 快照(UpperDir),它基于一个或多个只读(RO)的镜像层(LowerDir)之上。这个 RW 快照是容器写入数据的地方,通常包含 fs/(文件系统视图)和 work/(内部工作目录)子目录。

  2. Containerd 快照管理:containerd 通过其快照插件(如 overlayfs)管理快照的生命周期。每个快照都有唯一的数字 ID 和关联的元数据,存储在一个嵌入式的 BoltDB 数据库(通常是 meta.db)中。这些元数据包括快照名称、父快照引用、子快照列表以及引用计数(snapshot.ref)。

  3. 孤立资源(Orphan):在理想情况下,删除一个容器应触发链式反应:容器终止 -> 停止所有关联进程 -> 卸载所有文件系统挂载点 -> 从 BoltDB 中移除快照元数据 -> 删除磁盘上对应的快照目录。孤立资源指的是上述清理流程不完整时产生的残留物。在本问题中,表现为 BoltDB 记录和磁盘文件均存在,但任何活跃的容器或容器运行时的 API 都不再引用它们。

实践

以下详细描述了该泄漏问题的具体表现和诊断方法,基于 issue 中提供的案例(快照 ID 1384)。

1. 磁盘残留确认 快照目录存在于配置的快照存储路径下,且包含完整的文件结构,占据了实际的磁盘空间(案例中为 1021MB)。

ls /data/k8s/.../io.containerd.snapshotter.v1.overlayfs/snapshots/1384/
# fs/  work/
du -sh /data/k8s/.../io.containerd.snapshotter.v1.overlayfs/snapshots/1384/
# 1021M    .

2. BoltDB 元数据残留 使用 strings 等工具直接查询 BoltDB 文件,可以看到该快照的完整元数据树,包括其父快照(parent)和子快照(children)的引用,表明它曾是镜像层链中的一个中间层。

strings /data/k8s/.../io.containerd.metadata.v1.bolt/meta.db | grep -A2 -B2 '/1384/'
# 输出显示完整的快照元数据,包括 name, parent, children 等字段。

3. 运行时引用丢失 在 /proc/self/mountinfo(或其他挂载信息源)中,找不到任何指向该快照目录(1384)的 upperdir 或 lowerdir 引用。这证明在内核的 VFS 层,已经没有任何活动挂载点使用这个快照层。

grep 'upperdir.*/1384/' /proc/self/mountinfo
# (空输出)

4. 工具链无法感知与清理 所有容器生态系统的标准管理工具都对此孤立快照“视而不见”:

  • ctr -n k8s.io snapshot ls | grep 1384:不显示。
  • ctr -n k8s.io snapshot info 1384:报告“未找到”。
  • crictl rmi --prune:命令成功执行,但并未清理此快照(它主要处理未被引用的镜像层,而非容器 RW 层)。
  • ctr snapshot prune:同样无效。

5. 交叉验证方法 为验证此现象,可以对比一个健康的活跃快照(如案例中的 76747)。在 /proc/self/mountinfo 中能清晰看到其作为 upperdir 被挂载,同时也能通过 ctr 工具查询到其信息。这种“磁盘-数据库-挂载点-工具”四路交叉验证法是诊断此类问题的关键。

SRE 要点

与可靠性、可观测性、事件响应的关联

  • 可靠性与容量管理:此问题直接导致容量泄漏。随着时间推移,泄漏的快照会累积,耗尽磁盘空间,可能导致节点上新的容器无法创建(ENOSPC 错误),影响服务可用性。SRE 需要建立存储使用监控(如节点 disk_space_usage 指标),但此问题绕过了标准的监控和清理流程。
  • 可观测性盲点:由于标准工具无法列出或清理这些快照,它们成了可观测性盲区。SRE 无法通过常规仪表盘或运维脚本获取准确的、可回收的存储空间信息。这增加了故障排查的复杂度和响应时间。
  • 事件响应:当集群出现存储压力时,SRE 事件响应流程可能因无法定位和清理这些“幽灵”资源而受阻。需要依赖非标准、高风险的手段(如手动 rm -rf)进行干预,增加了操作风险。

最佳实践与常见反模式

  • 最佳实践:
    1. 定期执行健康检查:编写自定义脚本,定期交叉比对快照目录 (ls)、BoltDB 内容(使用 strings 或专用库)和容器运行状态,识别潜在的孤立快照。
    2. 预留运维窗口与监控:在监控中增加对 snapshots 目录总大小的独立监控,并设置告警阈值,以便在问题恶化前介入。
    3. 升级与关注:密切关注 containerd 官方对该 issue 的修复进展,及时升级到包含修复补丁的版本。
    4. 谨慎手动清理:如果必须手动清理,应在维护窗口进行,并遵循严格步骤:先确认无挂载引用 -> 尝试用 ctr snapshot rm --sync -> 如失败,最后才考虑 rm -rf 磁盘目录和清理 BoltDB(后者需专业操作,风险极高)。
  • 常见反模式:
    1. 忽视告警:忽视存储空间持续增长但无明显业务增长的告警。
    2. 依赖标准清理命令:错误地认为 crictl rmi --prune 或 ctr snapshot prune 能解决所有存储泄漏问题。
    3. 缺乏根治方案:频繁进行手动 rm 操作而不推动从根本上(升级 containerd 或等待官方修复)解决问题。

相关指标(SLI/SLO/SLA)

  • SLI (服务级别指标):节点可用磁盘空间百分比;容器创建失败率(因 ENOSPC 导致)。
  • SLO (服务级别目标):集群节点可用磁盘空间 > 15%;因存储问题导致的容器创建失败率 < 0.1%。
  • SLA (服务级别协议):内部服务协议可规定,基础设施提供方需确保节点存储的可用性,并承诺在发生此类已知泄漏问题时,在约定时间内(如 4 小时)提供缓解方案(如清理脚本或升级计划)。此问题可作为修订未来 SLA 中相关条款的依据。