containerd 磁盘占用分析与清理通用笔记

containerd 磁盘占用分析与清理通用笔记

适用范围:Linux 上的 containerd、Kubernetes CRI 节点,以及使用 nerdctl 的独立容器主机。命令以 root 或具备 sudo 权限为前提;rootless、K3s、RKE2、MicroK8s 等环境必须先确认各自的 socket、存储路径与管理服务。

本文是通用操作手册。只读命令可用于排查;删除、截断和修改配置的命令均属于后续人工执行步骤。编写过程中未执行容器、镜像、快照、日志清理,也未重启服务。不要整篇一次性粘贴执行。

1. 先记住结论

推荐顺序:确认磁盘和管理方式 → 找出占用类型 → 保留证据和恢复来源 → 定向清理退出容器/无用镜像 → 等待 GC → 核验空间和业务 → 建立轮转与容量策略。

  • Kubernetes 节点优先依赖 kubelet 的镜像和容器垃圾回收;临时人工处理应走 CRI,并明确影响。
  • nerdctl 管理的容器使用相同 socket、namespace 的 nerdctl 清理,优先按名称或 ID 定向操作。
  • 不要直接删除 containerd 的 blobs、snapshots、metadata.db 或整个数据目录。
  • crictl ps -a 为空,不等于 containerd 没有容器;k8s.io 也不等于该 namespace 中的容器全部由 Kubernetes 管理。
  • “未使用镜像”只表示工具当前判断无容器引用,不表示离线部署、扩容或回滚不再需要。
  • 删除前确认镜像可重新拉取或从离线包恢复;清理结果以文件系统实际增加的可用空间为准。

2. containerd 的空间都在哪里

占用类型 常见位置 如何处理
镜像压缩内容、manifest、配置 <ROOT>/io.containerd.content.v1.content 通过上层工具删除不再需要的镜像,让 GC 处理无引用内容
解压镜像层、容器可写层 <ROOT>/io.containerd.snapshotter.v1.overlayfs 删除对应无用容器或镜像;可写层增长要治理应用
元数据和引用关系 <ROOT>/io.containerd.metadata.v1.bolt 不手工删除或编辑
运行时状态、挂载点 常见为 /run/containerd 不当作普通缓存清空,单独排查 tmpfs/inode
Kubernetes stdout/stderr 常见为 /var/log/pods、/var/log/containers kubelet 日志轮转;后者通常包含指向前者的符号链接
nerdctl 容器日志 由 nerdctl inspect 的 LogPath 确定 使用日志轮转策略
emptyDir、卷和宿主机绑定目录 kubelet 数据目录、PVC、bind mount 源目录 按业务数据处理,镜像清理通常无效
BuildKit 构建缓存 BuildKit 自己的数据目录 确认构建服务后单独管理
离线镜像包、安装包、仓库数据 自定义交付目录 与 containerd 分开盘点,不能因文件大就删除

镜像可能同时具有压缩 blob 和解压后的 snapshot,这是正常存储结构。多个镜像和容器共享层,所以镜像列表的 SIZE、容器的 virtual size 不能直接相加;删除 10GB 的逻辑镜像不保证释放 10GB 磁盘。

3. 第一步:确认到底是哪块盘紧张

3.1 检查空间、inode 与挂载

# 只读
hostname
date -Is
df -hT
df -ih
findmnt

判断方法:

  • 容量接近耗尽:查看大目录、大文件。
  • inode 接近耗尽但容量充足:查看大量小文件、日志、socket 等。
  • /run 满:它可能是 tmpfs,与磁盘上的 containerd root 不是同一资源。
  • 多行 overlay 的容量往往来自同一底层文件系统,不能把这些行的 Used 相加。
  • loop 挂载显示 100%:先用 findmnt、losetup -l 判断镜像文件及只读属性,不据此认定 containerd 满了。

3.2 确认服务启动参数、socket 和数据目录

# 只读;输出可能带环境信息,外发前脱敏
containerd --version
crictl --version
nerdctl --version
systemctl show containerd -p ExecStart
systemctl cat containerd
ps -eo pid,args | grep '[c]ontainerd'

确认启动参数是否指定了自定义配置、root、state 或 address。若使用标准配置文件,再查看:

grep -nE '^(root|state)[[:space:]]*=|snapshotter' /etc/containerd/config.toml
cat /etc/crictl.yaml

配置文件应以实际启动入口为准;不要输出整个配置中的仓库认证信息。常见 root 是 /var/lib/containerd,但交付环境可能是 /export/containerd,发行版也可能使用其他路径。

以下变量只是示例,必须根据实际检查结果修改。建议在同一个 Bash 会话中使用:

ROOT=/var/lib/containerd
SOCK=/run/containerd/containerd.sock
NS=k8s.io

# 只读;变量应是已经确认存在的真实目录
findmnt -T "$ROOT"
du -xhd1 "$ROOT" | sort -h

du -x 不跨不同文件系统,但并不能排除所有 bind mount 或同设备重复视图;不要对 /run/containerd/.../rootfs 做汇总后再与 root 相加。大目录扫描会消耗 I/O,繁忙时分目录执行,必要时加 timeout 120 限时。

3.3 确认管理方式,而不是只看 namespace 名字

# 只读
systemctl is-active kubelet
ps -eo pid,args | grep '[k]ubelet'
ctr --address "$SOCK" namespaces list
ctr --address "$SOCK" --namespace "$NS" containers list
ctr --address "$SOCK" --namespace "$NS" tasks list
nerdctl --address "$SOCK" --namespace "$NS" ps -a
crictl --runtime-endpoint "unix://$SOCK" \
       --image-endpoint "unix://$SOCK" ps -a

对 ctr namespaces list 中每个 namespace 分别检查。注意:

  1. socket 决定连接哪一个 containerd 实例。
  2. namespace 决定查看哪个逻辑空间;nerdctl 默认 namespace 常为 default。
  3. CRI 容器列表、containerd 容器元数据、运行中的 task 是不同视图。
  4. kubeconfig 可能指向远端集群。kubectl get nodes 成功不证明当前 SSH 主机就是集群节点,要核对本机 IP 与 Node 地址。
  5. 混合管理的 namespace 不适合套用批量 prune 命令,应先确认每个对象的管理来源。

典型误判: 主机 kubelet 未运行,crictl ps -a 为空,但 ctr tasks list 和 nerdctl -n k8s.io ps 仍有运行任务。这类主机不能按“CRI 看不到,所以全部闲置”的逻辑清理。

4. 第二步:区分镜像、可写层、日志和业务数据

4.1 镜像和 snapshot 占用

# 只读;overlayfs 仅适用于该 snapshotter
sudo du -xsh "$ROOT/io.containerd.content.v1.content"
sudo du -xsh "$ROOT/io.containerd.snapshotter.v1.overlayfs"
ctr --address "$SOCK" --namespace "$NS" images list
ctr --address "$SOCK" --namespace "$NS" snapshots list
ctr --address "$SOCK" --namespace "$NS" leases list
nerdctl --address "$SOCK" --namespace "$NS" images
nerdctl --address "$SOCK" --namespace "$NS" ps -a --size

ps --size 的可写层小,不代表镜像层小;virtual size 也不能按容器数量累加。lease 可能来自拉取、导入、构建任务,不能只因为存在时间长就删除。

不是所有 nerdctl 版本都支持 Docker 风格命令;例如一次现场使用的 nerdctl 2.1.3 不支持 nerdctl system df。执行前先看本机 --help,不要把它当作必备检查项。

4.2 日志和业务挂载

# 只读
sudo du -xhd1 /var/log | sort -h
sudo journalctl --disk-usage
nerdctl --address "$SOCK" --namespace "$NS" inspect \
  --format '{{.Name}} {{json .Mounts}} {{.LogPath}}' CONTAINER_NAME

把 CONTAINER_NAME 替换为真实名称。inspect 用于区分可写层、bind mount、volume 和日志路径,不要将含敏感环境变量的完整输出公开。

确认目标目录后再找大文件,例如:

# 只读;可能较慢,按需缩小目录
sudo find /var/log -xdev -type f -size +100M \
  -printf '%s\t%p\n' | sort -nr | head -30

应用向容器内部普通文件写日志,会增长可写层;向 PVC/bind mount 写入,会增长对应卷。stdout 轮转无法解决应用内部日志文件增长。

4.3 df 很大,但 du 找不到

# 只读;需要系统已安装 lsof
sudo lsof -nP +L1

重点看位于目标文件系统、链接数为 0、仍由进程打开的普通大文件。文件被删除后,持有它的进程未关闭 fd,空间仍可能占用。不要直接重启所有容器;定位具体服务后,采用它支持的日志重新打开方式或安排单服务重启。现场没装 lsof 时,该检查应标记为未完成,不要假设没有此类占用。

5. 清理前的共同准备

  1. 暂停或避开并发拉取、构建、发布与离线导入,避免盘点后对象状态变化。
  2. 保存容器列表、镜像引用、挂载、业务恢复配置和必要日志。
  3. 明确必须保留的运行镜像、交付缓存、多架构镜像、回滚版本。
  4. 验证恢复来源:仓库可达和认证有效,或离线镜像包确实可用。只有镜像名称清单不等于镜像备份。
  5. 记录清理前 df -hT、df -ih、root 下的 du。

可将只读清单保存在有足够空间的独立位置;inspect 文件可能包含敏感信息,应限制权限。镜像导出会额外占空间,磁盘紧张时不要导出到同一块满盘。

6. nerdctl 主机:推荐的清理方式

6.1 优先定向删除已完成的临时容器

适用:确定由 nerdctl 管理,任务已完成,退出容器无需再次启动,必要日志和可写层数据已保留。

# 只读:再次确认状态、挂载和空间
nerdctl --address "$SOCK" --namespace "$NS" ps -a --size
nerdctl --address "$SOCK" --namespace "$NS" inspect FINISHED_CONTAINER

# 写操作:人工确认后,替换真实名称执行;不使用 -f,不附加 -v
nerdctl --address "$SOCK" --namespace "$NS" rm FINISHED_CONTAINER

删除容器会删除其可写层,相关日志也可能随之移除;不会自动清理 bind mount 源目录里的业务数据。若可写层只有几十 KiB,删除大量退出容器也未必能释放多少空间。

批量方案只在全部退出容器均可删除时考虑:

# 写操作:删除该 namespace 下全部停止容器,保留交互确认
nerdctl --address "$SOCK" --namespace "$NS" container prune

它会扩大删除范围,不能替代人工业务确认。

6.2 优先按完整引用删除明确不再需要的镜像

# 只读
nerdctl --address "$SOCK" --namespace "$NS" images
nerdctl --address "$SOCK" --namespace "$NS" ps -a

# 写操作:示意引用,替换后执行;不加 --force
nerdctl --address "$SOCK" --namespace "$NS" rmi registry.example.com/team/app:OLD_TAG

amd64 主机上出现 arm64 镜像,可能是合法的多架构交付缓存;先确认是否承担其他架构的交付工作。多个 tag 指向同一内容时,删除一个 tag 可能几乎不释放空间。

6.3 prune 是扩大范围的选择,不是首选按钮

# 写操作:默认处理悬空镜像,仍需事先确认恢复来源
nerdctl --address "$SOCK" --namespace "$NS" image prune

# 写操作:扩大为所有无容器引用的镜像,可能删除预加载和回滚缓存
nerdctl --address "$SOCK" --namespace "$NS" image prune --all

两条命令是不同范围的备选方案,不要求连续执行。先查看本机 image prune --help,保留交互确认。不要把 system prune --all --volumes 作为通用清理入口,也不要定时无差别清空缓存。

7. Kubernetes CRI 节点:优先 kubelet GC

7.1 先看节点和回收失败原因

# 只读;kubectl 需要对应集群权限,先核对节点身份
kubectl get nodes -o wide
kubectl describe node NODE_NAME
sudo journalctl -u kubelet --since '2 hours ago' --no-pager \
  | grep -Ei 'ImageGC|image garbage|FreeDiskSpace|DiskPressure|evict'

crictl --runtime-endpoint "unix://$SOCK" \
       --image-endpoint "unix://$SOCK" imagefsinfo
crictl --runtime-endpoint "unix://$SOCK" \
       --image-endpoint "unix://$SOCK" images
crictl --runtime-endpoint "unix://$SOCK" ps -a

先排查 kubelet 未运行、GC 报错、无法获得文件系统统计、镜像持续被使用、频繁拉取等问题。Kubernetes 官方建议避免使用外部垃圾回收工具干扰 kubelet 的管理。

7.2 必要时人工定向删除镜像

适用:确认是 CRI 管理的节点和镜像,排除非 CRI 容器引用,确认扩容、重建和回滚不依赖该缓存。

# 写操作:人工确认真实 IMAGE_ID 后执行
crictl --runtime-endpoint "unix://$SOCK" \
       --image-endpoint "unix://$SOCK" rmi IMAGE_ID

通过 CRI 删除可能影响该镜像 ID 关联的多个 tag,不能把它当作“只删除一个 tag”。运行中的容器通常不会因为镜像记录移除而立即终止,但未来重建、重启或调度可能需要重新拉取。

广泛清理命令仅作为特殊维护方案:

# 先看帮助:不同版本的行为与保护能力应单独确认
crictl rmi --help

# 写操作:只有在确认所有 CRI 视角下未使用镜像都可删除时执行
crictl --runtime-endpoint "unix://$SOCK" \
       --image-endpoint "unix://$SOCK" rmi --prune

不能将 --prune 当作跨管理工具的安全检查;不要假定它知道 nerdctl 的业务意图或能保护所有版本中的 pause 镜像。尤其是 CRI 列表为空、却存在 nerdctl 容器的混合环境,应停止批量清理。

不要手工循环删除 Kubernetes 的退出容器或 sandbox 来代替 kubelet GC;这会丢失排障现场,并可能与 kubelet 生命周期管理冲突。

8. 日志过大时如何处理

8.1 Kubernetes 容器日志

从 kubelet 实际启动参数确定 --config 文件,不要假定固定位置。在现有配置中调整,而不是覆盖整个文件:

# 配置示例,需匹配实际 Kubernetes 版本与容量目标
containerLogMaxSize: 20Mi
containerLogMaxFiles: 5

修改应按节点维护流程生效并验证轮转。以上配置针对 kubelet 管理的容器 stdout/stderr;不涵盖应用内部日志、PVC 日志、审计日志或 nerdctl 容器。阈值也不是严格实时的单容器总量上限。

8.2 nerdctl 容器日志

先查看本机 nerdctl run --help 与现有创建配置。新建或计划重建容器时可使用类似配置:

# 创建参数片段,不是可直接运行的完整业务命令
--log-driver=json-file --log-opt max-size=20m --log-opt max-file=5

将配置加入实际 Compose/启动脚本,按维护流程重建后验证;它不会自动修改现有容器。

8.3 journald

# 只读
sudo journalctl --disk-usage

# 写操作示例:确认旧系统日志不再需要后,轮转并清理归档日志
sudo journalctl --rotate
sudo journalctl --vacuum-size=1G

vacuum 主要作用于归档 journal,不能清理容器镜像,也不一定让总 journal 占用严格等于该数值。长期限制通过 journald 的 SystemMaxUse、SystemKeepFree 等参数按实际文件系统容量设计。

8.4 应急截断只用于已确认的普通文本日志

# 写操作示例:仅替换为已确认可丢弃的真实普通日志文件
sudo truncate -s 0 /absolute/path/to/confirmed-disposable.log

先保留必要证据并确认文件类型及进程写入方式;优先使用服务自身轮转。截断会不可逆丢失日志,应用未以追加模式写入时还可能出现空洞等问题。不要把该命令用于数据库、blob、元数据或未知文件,更不要通配符批量执行。

9. 删除后空间没释放,如何继续定位

现象 可能原因 下一步
删除镜像后只少了一点 共享层、其他 tag/namespace 引用 检查全部 namespace 的镜像与容器
容器删了仍占空间 镜像保留、卷/bind mount 独立存在 分别检查镜像、挂载源与日志
对象已删但 du 尚未下降 GC 异步处理、snapshot 清理较慢 等待后复查 containerd 日志
内容仍被保留 lease、GC root、合法引用 查明创建者与任务状态,不直接删 lease
du 降了但 df 没降 已删除文件仍打开、其他并发写入、文件系统特性 lsof +L1,复查挂载和增长源
inode 仍高 大量小文件或 socket 对目标目录使用 du --inodes 分层排查
# 只读
ctr --address "$SOCK" --namespace "$NS" leases list
sudo journalctl -u containerd --since '30 minutes ago' --no-pager
sudo lsof -nP +L1
df -hT "$ROOT"
df -ih "$ROOT"

不要为了“触发 GC”直接重启 containerd。服务重启不能消除合法引用,还会带来运行时管理中断。异常 lease、残留挂载、损坏元数据应作为专项故障处理,结合版本、日志、任务归属与恢复方案决定操作。

10. 长期治理

10.1 Kubernetes 镜像 GC 配置示例

修改 kubelet 实际使用的配置文件,先核对对应版本文档:

# 示例值,不是全场景最佳值
imageGCHighThresholdPercent: 80
imageGCLowThresholdPercent: 70
imageMinimumGCAge: 2m

超过高水位后尝试回收未使用镜像,目标降低到低水位;被引用的镜像不会凭空变成可回收空间。高水位要大于低水位,并与驱逐阈值留出余量。这些设置不会治理 nerdctl 主机,也不会自动限制应用日志和 PVC。

10.2 nerdctl、交付与构建主机

  • 为一次性任务设计完成后的容器保留期限,必要时创建时使用 --rm,同时把需要保留的日志和结果输出到明确位置。
  • 明确缓存清单:保留当前、上一稳定版本、离线恢复镜像和所需架构;按版本定向淘汰。
  • BuildKit 缓存单独核算,确认连接正确的 BuildKit 服务和无构建任务后,再按该版本的 builder prune --help 制定策略。
  • 对实际 root 所在文件系统同时监控容量和 inode,并观察增长速度;按业务需要提前告警。
  • 数据目录确需扩容或迁移时,作为单独变更处理:停写、维护窗口、保留 xattr/权限/硬链接、核对挂载与配置、预留回滚。不提供在线复制后直接切换的万能命令。
  • 不要因另一块盘空闲就直接把 containerd 迁过去:该盘可能属于 MinIO、数据库或其他业务。

11. 清理后验收清单

# 只读:对照清理前数据
df -hT "$ROOT"
df -ih "$ROOT"
du -xhd1 "$ROOT" | sort -h
systemctl is-active containerd
ctr --address "$SOCK" --namespace "$NS" tasks list
nerdctl --address "$SOCK" --namespace "$NS" ps -a

# 仅适用于真实的 CRI/Kubernetes 节点
crictl --runtime-endpoint "unix://$SOCK" info
kubectl get nodes -o wide
kubectl get pods -A -o wide

还要验证业务接口、必要的仓库/离线恢复路径及错误日志。active、RUNNING、Node Ready 分别是不同层次的状态,不能代替业务验收。若要通过重建测试镜像恢复,另行安排测试工作负载或维护窗口,不要为验证清理而直接重启生产服务。

建议记录:删除的对象和引用、保留原因、清理前后容量/inode、实际释放量、业务验证结果、未完成检查。不要把逻辑镜像大小作为已释放空间报告。

12. 不推荐的做法

  • rm -rf containerd root、snapshot 目录、content blob 或 /run/containerd。
  • 删除 metadata.db 后期待 containerd 自动正确恢复所有镜像和容器。
  • 仅凭 snapshot 目录编号、mtime 或目录大就判断它是孤儿。
  • 用 ctr images rm、ctr snapshots rm、lease 删除绕开上层生命周期直接批量处理。
  • 以 crictl ps 为空作为清空镜像的依据。
  • 默认删除所有 arm64 镜像、退出容器、卷或离线包。
  • 直接删除 Kubernetes Pod 日志目录、PVC、emptyDir,或者 kubelet 的整个数据目录。
  • 未查明持有者便重启 containerd、kubelet或业务容器。
  • 给生产节点增加无差别 prune --all 定时任务。

13. 快速决策表

主要问题 优先方案
content 大,积累很多旧镜像 按管理来源和保留清单定向删镜像,等待 GC
snapshot 大,退出容器多 看可写层大小,保留必要数据后定向删退出容器
snapshot 大,运行容器可写层持续增长 定位应用内部日志、缓存和临时文件,治理写入源
/var/log 或 nerdctl LogPath 大 配置轮转,必要时经确认处理指定日志
PVC/bind mount 大 按业务数据生命周期处理,镜像 prune 无效
df 与 du 明显不符 查打开的已删除文件、挂载、文件系统及并发写入
kubelet GC 失败 查 kubelet 日志和统计、配置,不先清空 containerd
大量空间是合法运行镜像或业务数据 扩容/迁移和容量规划

14. 参考资料与验证边界

文档结合只读现场观察与官方文档整理,未在所有版本、snapshotter、发行版或 rootless 环境实测。所有清理命令均未实际执行,不能据此承诺释放容量或零业务影响。执行时以目标机的版本、配置、--help、管理来源和现场状态为准。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容