前置阅读:建议先阅读 容器运行时深度解析:dockerd 到 containerd 再到 runc 的完整调用链 与 Docker 存储驱动与 OverlayFS 深度解析。本篇在调用链之上,聚焦 containerd 的插件机制与自定义扩展。
关键概念:containerd 是**“核心薄 + 插件厚”**的架构——核心只做内容寻址、元数据与生命周期协调,存储(snapshotter)、运行(shim 运行时)、网络(CNI)、分发(registry)全部插件化。理解"插件的注册与类型",就理解了如何把 containerd 改造成适合自己场景的运行时底座。
1. containerd 插件架构
1.1 核心与插件的边界
containerd 核心(core):
- 内容存储(Content Store):按 digest 寻址的层数据
- 元数据(Metadata):镜像、容器、快照的索引(boltdb)
- 生命周期:API 协调、事件、lease
插件(Plugins):
- 通过 init() 注册,启动时按配置加载
- 各插件通过内部注册表互相调用(依赖注入)
- 可被替换/扩展而不改核心
1.2 插件注册与加载
// 以 snapshotter 插件注册为例(伪代码):包 init 里调用 register
func init() {
plugin.Register("snapshotter.overlayfs", &plugin.Registration{
Type: plugin.SnapshotterPlugin,
Init: func(ic *plugin.InitContext) (interface{}, error) {
return overlay.NewSnapshotter(...)
},
})
}
启动时加载流程:
config 指定 enable/disable → 拓扑排序(按依赖)
→ 初始化每个插件 → 失败则进程退出(daemon 启动即校验)
一句话:containerd 核心只负责"内容与元数据",其余能力全部是插件——注册表 + 依赖注入让每个能力都可替换。
2. 插件类型总览
2.1 六大插件族
| 插件类型 | 职责 | 可扩展点 |
|---|---|---|
| snapshotter | 层→可写快照(OverlayFS 等) | 自定义存储后端/懒加载 |
| diff | 层差异计算与应用 | 自定义压缩/加密层 |
| runtime/shim | 创建容器进程 | 自定义运行时(Wasm/gVisor) |
| registry | 镜像仓库交互 | 自定义鉴权/镜像格式 |
| CNI | Pod 网络 | 自定义网络插件 |
| metadata | 元数据存储 | 换数据库/加索引 |
2.2 常见插件列表
# 查询已加载插件
ctr plugins ls
# NAME TYPE
# io.containerd.snapshotter.v1.overlayfs io.containerd.snapshotter.v1
# io.containerd.runtime.v2.task io.containerd.runtime.v2
# io.containerd.grpc.v1.cri io.containerd.grpc.v1
2.3 插件的启用/禁用
config.toml 里按需裁剪(边缘设备省内存):
[plugins."io.containerd.grpc.v1.cri"]
disabled = false # K8s 需要 CRI 插件
裁剪原则:只保留当前场景所需,减少启动开销与攻击面
一句话:containerd 的插件清单就是它的"能力清单"——
ctr plugins ls能看全部,config 里可按需开关。
3. shim v2 与自定义运行时
3.1 shim v2 的角色
shim v2(Runtime v2):
- 每个容器一个 shim 进程,作为 containerd 与运行时的桥梁
- 负责容器进程的回收、IO、信号转发
- 运行时实现 Task Service(start/exec/exit 等)即可接入
代表实现:
- io.containerd.runc.v2(默认)
- io.containerd.runsc.v1(gVisor)
- io.containerd.runwasi.v1.wasmtime(Wasm)
3.2 配置自定义运行时
# config.toml 声明一个自定义运行时(如 gVisor)
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
runtime_type = "io.containerd.runsc.v1"
# 或通过 CRI 的 runtime handler 在 Pod 里选择
# K8s 里:spec.runtimeClassName: runsc
# K8s RuntimeClass 定义
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata: { name: runsc }
handler: runsc
3.3 编写自定义 shim 的要点
实现 TaskService 接口:
Create / Start / Exec / Kill / Wait / Stats / Pause / Resume
关键注意:
- 复用 containerd 的 event/io 通道,不要自造
- 处理容器退出后的 shim 退出时机(防止僵尸进程)
- 测试先用 ctr 手动创建容器,再接入 K8s
一句话:shim v2 是"每个容器一个适配器"——只要实现 Task 服务,任何进程/沙箱模型都能以 OCI 容器形态跑起来。
4. 自定义 snapshotter
4.1 snapshotter 的职责
snapshotter 负责"层 → 快照":
- Mount(挂载点构建,OverlayFS 上下层叠加)
- Commit(把可写层固化为只读层)
- 不同后端:overlayfs / native / fuse(Nydus、Stargz)
接口核心:
Prepare / Mount / Commit / Remove / Stat
4.2 Nydus/Stargz 懒加载 snapshotter
懒加载 snapshotter(如 Nydus):
- 不一次性拉全部分块,启动时只取元数据 + 需要的分块
- 通过 FUSE 挂载,按需从远端取数据
- 适合大镜像、弱网、冷启动敏感场景
对比 OverlayFS:OverlayFS 必须完整落盘后才能挂载
# 启用 nydus snapshotter
# config.toml
[plugins."io.containerd.snapshotter.v1.nydus"]
root_path = "/var/lib/containerd-nydus"
# 拉取时指定 snapshotter
ctr images pull --snapshotter nydus registry.example.com/app:nydus-v1
4.3 自定义 snapshotter 注意事项
□ 复用内容存储:快照数据与 content store 解耦或复用,勿重复存储
□ 保证 prepare/mount 的幂等与并发安全(containerd 会并发调用)
□ 挂载点清理:容器退出后正确卸载,防止挂载泄漏
□ 用 ctr snapshotter 子命令验证再上线
一句话:snapshotter 决定了"镜像层如何变成文件系统"——OverlayFS 求简单、Nydus 求懒加载,自定义时接口稳定、边界要干净。
5. CNI 网络插件
5.1 containerd 的 CNI 集成
containerd 的 CRI 插件负责调用 CNI:
创建 Sandbox → 调用 CNI ADD → 配置 Pod 网络
CNI 配置目录与插件二进制由 kubelet 侧管理
网络栈:
- bridge(默认,宿主 NAT)
- macvlan/vlan(性能敏感、直连)
- Calico/Cilium(overlay,K8s 集群常用,见网络篇)
# 查看当前 CNI 配置
crictl info | jq '.config.cni'
# 常见目录
ls /etc/cni/net.d/ /opt/cni/bin/
5.2 自定义 CNI 插件
自定义 CNI 的两个层次:
1. 编排现有插件:写 net.d 配置(按顺序链式调用)
2. 实现 CNI 协议:编写 CNI 二进制(标准输入/输出 JSON)
实现要点:
- CNI 插件是独立二进制,通过环境变量(CNI_COMMAND/CNI_CONTAINERID)入参
- 命令:ADD / DEL / CHECK / VERSION
- 返回标准 JSON(cniVersion + interfaces + ips)
5.3 CNI 排障
常见问题:
- CNI 配置缺失/插件二进制不在 → Pod 一直 ContainerCreating
- 插件 ADD 返回错误 → 查看 kubelet 日志中的 cni 错误
- IP 冲突/路由未下发 → 检查数据面(路由表/iptables)
一句话:containerd 不管具体网络,只按标准调用 CNI 插件——网络能力完全由 net.d 配置与插件二进制决定。
6. 与 Docker / Kubernetes 集成
6.1 Docker 如何使用 containerd
dockerd → containerd(管理)→ containerd-shim → runc
Docker 的 containerd 集成对用户不可见(docker 命令屏蔽细节)
生产直接使用:K8s + containerd CRI(不经 dockerd)
6.2 Kubelet 的 CRI 接入
# 检查 kubelet 使用的运行时
kubelet --container-runtime-endpoint=unix:///run/containerd/containerd.sock
# 节点运行时信息
crictl version
crictl info
# 使用 containerd 的镜像加速配置示例(/etc/containerd/config.toml)
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://registry-1.docker.io", "https://mirror.example.com"]
6.3 运行时无缝切换(dockerd → CRI)
迁移考虑:
- 镜像兼容:containerd 支持 Docker 镜像格式,直接可用
- 命令差异:docker ps → crictl ps / ctr c
- 网络:K8s 场景由 CNI 接管,不再用 Docker 网络
- 日志/存储路径变化:/var/lib/docker → /var/lib/containerd
一句话:Docker 与 K8s 都走 containerd 这条底座——区别只是谁在调用、用什么接口(Docker CLI 还是 CRI)。
7. 调试与排查
7.1 ctr / crictl 工具
# containerd 原生工具(不经 CRI)
ctr images pull docker.io/library/alpine:3.20
ctr run --rm docker.io/library/alpine:3.20 test echo hi
ctr content ls && ctr snapshots ls && ctr plugins ls
# CRI 工具(K8s 视角)
crictl ps -a; crictl images
crictl logs <cid>
crictl exec -it <cid> sh
7.2 常见故障与定位
| 症状 | 定位手段 | 常见根因 |
|---|---|---|
| 拉镜像卡住 | ctr content ls 看传输 | 网络/仓库鉴权 |
| 容器启动失败 | ctr run --rm 复现 | 镜像缺层/shim 异常 |
| Pod 一直 Creating | crictl inspectp + kubelet 日志 | CNI 或运行时配置 |
| 快照泄漏 | ctr snapshots ls 对比 | 清理任务未执行 |
| daemon 起不来 | containerd --log-level debug | 插件初始化失败 |
7.3 打开调试
# 前台调试运行 containerd
containerd --log-level debug --root /var/lib/containerd-debug
ctr events # 查看事件流
# pprof 性能剖析(默认 6060)
curl -s "http://127.0.0.1:6060/debug/pprof/goroutine?debug=1" | head -50
一句话:containerd 排查三板斧——
ctr直达底层、crictl站在 K8s 视角、debug 日志定位插件初始化问题。
8. 生产配置最佳实践
8.1 生产 config 要点
□ CRI 只读挂载:ReadOnlyPaths、禁用非必要 capabilities(安全加固)
□ 镜像加速:registry mirrors 回源(见多集群分发篇)
□ 资源:--oom-score-adjust 保护 daemon;合理日志级别
□ GC:image GC 与 container GC 阈值,防磁盘打满
8.2 版本兼容与升级
□ containerd 与 CRI/插件版本强相关:升级前查 compatibility matrix
□ 自定义插件(snapshotter/shim)要跟随主版本 API 变动
□ 灰度:先在测试节点升级,验证 ctr plugins ls 与运行稳定再全量
□ 备份:升级前备份元数据(boltdb)与内容存储
8.3 性能与稳定性清单
□ 大并发拉取:监控 content store 锁竞争,必要时限流
□ 懒加载 snapshotter 与镜像仓库预热配合(避免首次全穿透)
□ 自定义 shim 关注退出清理与信号转发,防僵尸与挂载泄漏
□ 监控指标:grpc 请求延迟、snapshotter 挂载数、GC 执行时间
一句话:生产用 containerd 要"配置加固 + 版本灰度 + 性能观测"三管齐下,插件越自定义,兼容性与清理边界越要盯紧。
9. 总结
| 主题 | 关键结论 | 一句话记忆 |
|---|---|---|
| 插件架构 | 核心薄、插件厚,注册表+依赖注入 | 能力皆可换 |
| 插件类型 | snapshotter/diff/runtime/registry/CNI | 六族各管一段 |
| shim v2 | 每容器一个适配器实现 Task 服务 | 换运行时只换 shim |
| snapshotter | 层→快照,OverlayFS 或 Nydus 懒加载 | 决定层怎么变文件系统 |
| CNI | containerd 只调用不实现 | 网络由插件决定 |
| 集成 | Docker/CRI 都走 containerd | 底座同一层 |
| 排障 | ctr 底层、crictl 上层、debug 日志 | 三板斧定位 |
| 生产 | 配置加固 + 版本灰度 + 性能观测 | 自定义越深越要盯 |
containerd 的插件机制让"容器运行时底座"从一个固定实现变成一套可组合、可替换、可扩展的框架。落地要点:先画清自己要扩展哪一环(存储用 Nydus、运行用 gVisor/Wasm、网络用自定义 CNI),走好 shim v2 与 snapshotter 的标准接口,用 ctr 逐层验证再接入 CRI/K8s;自定义越深,越要把兼容性矩阵、清理边界与观测指标一并规划。当底层能力都变成可插拔,containerd 才能真正贴合从数据中心到边缘的每一种工作负载。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。