containerd 插件机制与扩展:snapshotter、shim 与 CNI 的深度定制

深入讲解 containerd 的插件机制与扩展开发:containerd 分层插件架构(核心 vs 插件)、snapshotter/diff/runtime/registry/CNI 等插件类型、shim v2 与自定义运行时接入(runsc/Wasm)、自定义 snapshotter 与懒加载(Nydus/Stargz)、CNI 网络插件集成、与 Docker/Kubernetes 的集成方式,以及 ctr/crictl 调试与生产配置。

前置阅读:建议先阅读 容器运行时深度解析: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镜像仓库交互自定义鉴权/镜像格式
CNIPod 网络自定义网络插件
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 一直 Creatingcrictl 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 懒加载决定层怎么变文件系统
CNIcontainerd 只调用不实现网络由插件决定
集成Docker/CRI 都走 containerd底座同一层
排障ctr 底层、crictl 上层、debug 日志三板斧定位
生产配置加固 + 版本灰度 + 性能观测自定义越深越要盯

containerd 的插件机制让"容器运行时底座"从一个固定实现变成一套可组合、可替换、可扩展的框架。落地要点:先画清自己要扩展哪一环(存储用 Nydus、运行用 gVisor/Wasm、网络用自定义 CNI),走好 shim v2 与 snapshotter 的标准接口,用 ctr 逐层验证再接入 CRI/K8s;自定义越深,越要把兼容性矩阵、清理边界与观测指标一并规划。当底层能力都变成可插拔,containerd 才能真正贴合从数据中心到边缘的每一种工作负载。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「docker」更多文章

  1. 镜像安全扫描与 SBOM:Trivy、Grype、Clair 与 CI 安全门禁
  2. 边缘容器与轻量运行时:Wasm、gVisor 与 K3s 的资源受限实践
  3. 多集群多区域镜像同步与分发:从复制拓扑到 P2P 拉取加速