containerd 与 CRI 生产运维及磁盘爆满排障
在 Kubernetes 节点上,containerd 同时连接 kubelet、镜像仓库、OCI Runtime、快照文件系统和本地磁盘。很多“节点磁盘满”“镜像删不掉”“Pod 一直 ContainerCreating”“重启 containerd 后业务异常”的问题,根因并不在 Kubernetes 对象,而在 containerd 的内容、镜像、容器、快照和租约关系。
本文从运行时结构入手,给出日常巡检、容量分析、安全清理和故障恢复方法。文中的清理步骤必须先确认引用关系,禁止直接删除 /var/lib/containerd。
一、理解 containerd 在节点中的位置
典型调用链如下:
kubelet
└─ CRI gRPC
└─ containerd CRI plugin
├─ image service
├─ content store
├─ metadata database
├─ snapshotter
└─ OCI runtime(通常为 runc)
几个容易混淆的概念:
| 对象 | 含义 |
|---|---|
| Content | 按 digest 保存的镜像层、配置和 manifest 数据 |
| Image | 名称到目标 descriptor 的引用,不等于实际层文件 |
| Container | 容器元数据,不代表进程一定运行 |
| Task | 正在运行或已停止的运行时任务 |
| Snapshot | 容器可写层或镜像层解包后的文件系统视图 |
| Lease | 临时保护一组资源不被垃圾回收 |
| Namespace | containerd 内部隔离域,Kubernetes 通常使用 k8s.io |
删除 Image 引用不一定马上释放空间,因为内容层可能仍被其他镜像、快照、容器或 lease 引用。
二、先确认目录与配置
containerd config dump > /tmp/containerd-effective-config.toml
containerd --version
runc --version
crictl version
systemctl status containerd --no-pager
常见路径:
- 配置:
/etc/containerd/config.toml; - 根目录:
/var/lib/containerd; - 状态目录:
/run/containerd; - 日志:systemd journal;
- kubelet 数据:
/var/lib/kubelet。
不要假设所有发行版路径都相同,应以有效配置中的 root、state 和插件设置为准。
三、crictl、ctr与nerdctl怎么选
crictl
crictl 面向 CRI,看到的是 kubelet 视角的 Pod Sandbox、容器和镜像,生产排障优先使用。
crictl info
crictl pods
crictl ps -a
crictl images
crictl stats
ctr
ctr 是 containerd 低层调试工具,必须显式指定命名空间:
ctr namespaces list
ctr -n k8s.io images list
ctr -n k8s.io containers list
ctr -n k8s.io tasks list
ctr -n k8s.io snapshots list
忘记 -n k8s.io 时看到空列表,不代表节点没有镜像和容器。
nerdctl
nerdctl 提供更接近 Docker 的体验,适合人工调试和独立 containerd 环境,但不要用它绕开 kubelet直接修改 Kubernetes 正在管理的容器。
四、建立运行时基线
1. 配置基线
至少审查:
- CRI 插件是否启用;
- sandbox image 是否来自可用仓库;
SystemdCgroup是否与 kubelet 一致;- registry mirrors、TLS 和认证配置;
- snapshotter 类型;
- 日志级别、OOM 分值和资源限制;
- 是否配置了不安全仓库;
- pause 镜像架构是否匹配节点。
2. 健康基线
crictl info
sudo ctr plugins list
sudo journalctl -u containerd --since '-30 min' --no-pager
sudo journalctl -u kubelet --since '-30 min' --no-pager
ctr plugins list 中关键插件应为 ok。CRI 未加载时,kubelet 通常会报告连接 runtime service 失败。
3. 容量基线
df -hT
df -ih
du -xhd1 /var/lib/containerd 2>/dev/null | sort -h
du -xhd1 /var/lib/kubelet 2>/dev/null | sort -h
磁盘使用率、inode 使用率和文件系统预留空间都要观察。大量小文件可能先耗尽 inode,而不是字节容量。
五、磁盘爆满的定位顺序
第一步:确认满的是哪个文件系统
findmnt -T /var/lib/containerd
findmnt -T /var/lib/kubelet
df -hT /var/lib/containerd /var/lib/kubelet
df -ih /var/lib/containerd /var/lib/kubelet
如果 containerd 和 kubelet 位于不同挂载点,必须分别判断。
第二步:区分内容、快照、日志还是空目录卷
du -xhd2 /var/lib/containerd 2>/dev/null | sort -h | tail -30
du -xhd2 /var/lib/kubelet 2>/dev/null | sort -h | tail -30
常见大户包括:
io.containerd.content.v1.content:压缩镜像层;io.containerd.snapshotter.v1.overlayfs:解包层和容器可写层;- Pod 日志目录;
emptyDir;- CSI 挂载或残留目录;
- 已删除但仍被进程打开的日志文件。
第三步:检查已删除但未释放的文件
lsof +L1
如果大文件链接数为 0 但仍被进程打开,重启对应进程或使其重新打开日志后空间才会释放。不要看到 deleted 就立即重启整个节点,应先确认进程和业务影响。
第四步:检查运行时对象
crictl images
crictl ps -a
crictl pods
ctr -n k8s.io leases list
ctr -n k8s.io snapshots list
结合镜像创建时间、大小、当前 Pod 引用和拉取策略判断是否真正无用。
六、理解垃圾回收
containerd 使用标记与引用关系判断资源是否可回收。镜像、容器、快照和 lease 都可能成为 GC 根或引用链的一部分。
Kubernetes 还由 kubelet 管理镜像垃圾回收。应优先让 kubelet 和 containerd 的正常机制工作,而不是手工删除数据目录。
需要重点理解:
- 删除镜像名称只是移除一个引用;
- 共享层仍被其他镜像引用时不会释放;
- 活跃容器的快照不能删除;
- 未过期 lease 可能保护临时资源;
- 元数据和磁盘文件必须保持一致;
- GC 可能为降低锁占用而分批执行,不一定瞬间释放全部空间。
七、安全释放空间的操作顺序
1. 先停止空间继续增长
- 暂停大量镜像构建或预拉取任务;
- 限制异常日志输出;
- cordon 节点,避免调度更多工作负载;
- 保留足够空间让 containerd 和 kubelet继续工作。
kubectl cordon <node-name>
2. 清理 Kubernetes 已不使用的容器和镜像
先确认容器状态:
crictl ps -a
crictl pods
crictl images
仅删除明确无用的已停止容器或镜像。不要使用不加筛选的批量命令,也不要删除当前运行 Pod 所需镜像后立刻断开仓库连接。
3. 处理日志
检查 kubelet 容器日志轮转配置、应用日志输出方式和 journald 占用:
journalctl --disk-usage
journalctl -u containerd --since today --no-pager | tail -200
find /var/log/pods -xdev -type f -printf '%s %p\n' 2>/dev/null | sort -n | tail -30
应通过日志轮转和保留策略治理,避免用截断正在写入文件的方式作为长期方案。
4. 触发和观察正常GC
修复异常引用或删除无用对象后,观察 containerd 日志和目录占用。不要为了“强制GC”随意修改未知参数。GC 配置位于 containerd scheduler 插件中,调整前需做压测和版本核对。
5. 节点恢复
systemctl is-active containerd kubelet
crictl info
kubectl get node <node-name>
kubectl uncordon <node-name>
恢复后继续观察镜像拉取、Pod 创建和磁盘曲线,确认问题没有再次增长。
八、严禁直接删除的数据
不要直接执行以下操作:
rm -rf /var/lib/containerd/*
rm -rf /var/lib/kubelet/*
这样做可能造成:
- 元数据库与快照目录不一致;
- 正在运行容器丢失可写层;
- kubelet 看到 Pod 存在但运行时对象消失;
- CSI 挂载和设备引用异常;
- 节点只能通过重建恢复。
如果节点已严重损坏,通常更安全的做法是:隔离节点、迁移业务、确认有状态数据安全、按标准流程重置或重装节点,再重新加入集群。
九、镜像拉取故障
ImagePullBackOff
依次检查:
- 镜像名称、tag 和 digest;
- DNS 与仓库连通性;
- 仓库证书链;
- imagePullSecrets;
- containerd registry 配置;
- 代理和 NO_PROXY;
- 仓库限流;
- 节点磁盘与 inode。
crictl pull registry.example.com/project/image:tag
journalctl -u containerd -n 200 --no-pager
x509证书错误
不要通过长期关闭 TLS 验证解决。正确做法是安装受信任 CA、修复仓库证书和主机名,并确认 containerd 是否需要重启才能加载信任链。
镜像解包失败
重点检查 snapshotter、文件系统空间、inode、OverlayFS 支持、SELinux/AppArmor 和内核日志。
十、CRI连接故障
常见报错包括:
container runtime is down
failed to connect to containerd
unknown service runtime.v1.RuntimeService
排查步骤:
systemctl status containerd --no-pager
ss -xl | grep containerd
crictl --runtime-endpoint unix:///run/containerd/containerd.sock info
ctr plugins list
journalctl -u containerd -b --no-pager | tail -300
确认 kubelet 与 crictl 指向相同 socket,并检查 CRI 插件是否被配置为 disabled。修改配置后先执行配置审查,再滚动处理节点。
十一、快照与OverlayFS问题
快照目录增长
快照ID与 Kubernetes Pod 名称不是一一对应,不能根据目录名猜测后删除。应从容器、快照和内容引用关系逐层确认。
overlay mount失败
检查:
- 内核是否支持 overlay;
- backing filesystem 是否兼容;
- dmesg 中是否有 I/O、只读或 xattr 错误;
- SELinux 上下文;
- 挂载点是否残留;
- 是否发生非正常断电。
dmesg -T | tail -200
findmnt -t overlay
ctr -n k8s.io snapshots list
十二、运行时升级与变更
生产升级前应核对:
- Kubernetes 与 CRI 版本兼容性;
- containerd 配置版本差异;
- runc 安全公告;
- cgroup v1/v2 和 systemd cgroup 驱动;
- snapshotter 数据兼容性;
- registry 配置迁移;
- pause 镜像版本。
标准流程:
- 在测试节点验证;
- cordon、drain 单个节点;
- 备份配置并升级;
- 验证 CRI、CNI、CSI 和样例 Pod;
- uncordon 并观察;
- 再进入下一批。
不要同时升级全部节点的运行时。
十三、监控与告警
至少监控:
- containerd 服务状态和重启次数;
- CRI 操作延迟与错误;
- 镜像拉取失败率;
- Pod sandbox 创建失败;
- 节点文件系统容量和 inode;
- 容器日志增长率;
- kubelet DiskPressure;
- 镜像层和快照目录增长趋势。
告警应包含节点名、文件系统、剩余容量、增长速度和最近异常事件,而不是只报一个百分比。
十四、典型案例:overlay快照持续增长
现象
/var/lib/containerd持续增长;crictl images显示镜像数量并不多;- 重启 containerd 不能明显释放空间。
分析
- 确认主要空间位于 overlayfs snapshotter;
- 检查是否存在大量退出容器和 sandbox;
- 检查 lease 与 snapshot 引用;
- 对照 kubelet 垃圾回收日志;
- 检查异常客户端是否不断创建临时资源;
- 判断是否为已删除但仍打开的文件。
修复
- 隔离节点并停止异常创建源;
- 通过 CRI 删除确认无用的对象;
- 修复失效 lease 或客户端逻辑;
- 等待并观察正常 GC;
- 如元数据损坏,迁移业务并重建节点,而不是手工拼接数据目录。
十五、生产检查清单
日常
- [ ] containerd、kubelet状态正常;
- [ ] 关键插件均为
ok; - [ ] 磁盘与 inode 有充足余量;
- [ ] 没有持续镜像拉取和 sandbox 创建错误;
- [ ] 日志轮转有效;
- [ ] 镜像和快照增长趋势可解释。
故障处理
- [ ] 已确认具体满盘挂载点;
- [ ] 已区分内容、快照、日志、emptyDir 和 CSI;
- [ ] 已检查 deleted-open 文件;
- [ ] 已 cordon 节点并阻止继续增长;
- [ ] 只删除确认无引用的对象;
- [ ] 没有直接删除 containerd 或 kubelet 数据目录;
- [ ] 恢复后已验证 Pod 创建、镜像拉取和业务流量。
参考资料
- containerd:Garbage Collection
https://github.com/containerd/containerd/blob/main/docs/garbage-collection.md - containerd:CRI Configuration
https://github.com/containerd/containerd/blob/main/docs/cri/config.md - Kubernetes:Container Runtimes
https://kubernetes.io/docs/setup/production-environment/container-runtimes/ - Kubernetes:Garbage Collection
https://kubernetes.io/docs/concepts/architecture/garbage-collection/