引言
在 Kubernetes 集群中,真正负责创建和运行容器进程的并不是 kubelet 本身,而是容器运行时。随着 Kubernetes 从 v1.20 开始逐步弃用 dockershim,**containerd** 已经成为社区推荐的默认容器运行时。它从 Docker 项目中独立出来,由 CNCF 孵化并毕业,以轻量、稳定、模块化的特性支撑着无数生产集群。
然而,许多从 Docker 迁移过来的开发者对 containerd 仍感到陌生:它与 Docker 到底有哪些不同?它的进程模型是怎样的?如何配置和调试?本文将围绕这些问题,从 CRI 接口标准出发,深入解析 containerd 的架构分层,并通过实际操作在 Ubuntu 22.04 上完成部署、镜像拉取、容器创建以及进程分析,最后对比 containerd 与 Docker 的本质差异,帮助读者建立起对容器运行时的清晰认知。
一、容器运行时在 Kubernetes 中的定位
Kubernetes 调度的是容器化应用,而容器运行时就是负责执行这些容器的软件层。为了避免 kubelet 与特定的运行时绑定,K8s 定义了 **CRI(Container Runtime Interface)**------一套标准的 gRPC 接口,使得任何实现了 CRI 的运行时都可以无缝接入。
kubelet 通过 CRI 向运行时发出如下指令:
- 创建和管理 Pod 沙箱(pause 容器 + 网络命名空间)
- 创建、启动、停止和删除业务容器
- 拉取、列表、删除镜像
这种可插拔的架构意味着,只要替换 CRI 实现,我们就可以在 containerd、CRI-O 甚至其他定制运行时之间自由切换,而不必修改 kubelet 自身。调用流程可以简化为:
|-------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text kubelet → CRI gRPC Client → CRI gRPC Server (containerd) ├── RuntimeService: 管理 Pod 和容器生命周期 └── ImageService: 拉取、查询、删除镜像 |
关键操作对应到 CRI 方法:RunPodSandbox 创建沙箱、CreateContainer 在沙箱内创建容器、StartContainer 启动进程、PullImage 拉取镜像等。
二、containerd 的演进与架构分层
containerd 最初是 Docker 引擎的底层容器管理器,2016 年从 Docker 项目分离并捐赠给 CNCF,2019 年成为毕业项目。如今它定位为行业标准的容器运行时,原生支持 CRI 插件,架构高度模块化。
其内部可以划分为五个核心层次:
|-------|-------------|--------------------------------------------------|
| 层级 | 组件 | 功能 |
| API 层 | GRPC API | 对外暴露容器与镜像管理接口,包括 CRI 和原生 API |
| 核心层 | Core | 管理对象元数据、命名空间、事件流、任务生命周期 |
| 快照层 | Snapshotter | 管理联合文件系统(overlayfs、device-mapper 等),为容器提供 rootfs |
| 运行时层 | Runtime | 通过 shim 调用 runc 或其他 OCI 运行时创建容器 |
| 存储层 | Storage | 基于内容寻址存储(content store)管理镜像层和 manifests |
所有层级通过清晰的 API 边界解耦,开发者可以针对快照、运行时等单独扩展,例如替换成 katacontainers 或 gVisor 等安全运行时。
三、进程模型:containerd-shim 的关键角色
在 containerd 的进程树中,最值得关注的组件是 **containerd-shim-runc-v2**。其设计目的是让 runc 在完成容器创建后可以退出,而容器进程继续运行,避免 containerd 自身直接成为所有容器进程的父进程。
运行时调用链为:
|-------------------------------------------------------|
| Plain Text containerd → containerd-shim → runc → 容器进程 |
我们可以用 pstree 观察实际的进程关系:
|------------------------------------|
| Bash pstree -p $(pidof containerd) |
输出示例:
|---------------------------------------------------------------------------------|
| Plain Text containerd─┬─containerd-shim─┬─nginx───nginx ... └─{containerd-shim} |
shim 的作用可以归纳为三点:
- 作为容器的父进程,持续收集退出码和状态;
- 保持容器的标准输入输出流连接;
- 当容器退出时向 containerd 报告,自身随后也退出。
这种设计使得 containerd 守护进程的重启不会影响到已运行的容器,整体稳定性大幅提升。
四、containerd 与 Docker 的渊源与区别
传统 Docker 引擎的架构可以看作:
|--------------------------------------------------------------------------------|
| Plain Text docker CLI → dockerd REST API → containerd → containerd-shim → runc |
Docker 是一个"全家桶"式的平台,除了 containerd 还包含了 dockerd 守护进程、命令行工具、镜像构建(BuildKit 集成)、Docker Compose 等大量功能。
而 containerd 独立运行时,去掉了 dockerd 这一层,调用路径变得极为简洁:
|--------------------------------------------------------------------------------|
| Plain Text crictl / ctr / nerdctl → containerd (gRPC) → containerd-shim → runc |
两者的关键区别可以总结如下:
|--------|-----------------------------------------|-----------------------------|
| 维度 | containerd | Docker |
| 定位 | 聚焦容器运行时与镜像管理 | 完整开发到部署平台 |
| 进程模型 | 单一守护进程 + shim | dockerd + containerd + shim |
| CRI 支持 | 内置原生 CRI 插件 | 曾依赖外部 dockershim(已移除) |
| 镜像构建 | 无原生 docker build,需配合 BuildKit 或 nerdctl | 内置 build 命令 |
| 默认 CLI | ctr(调试用,功能基础) | docker(用户友好,生态丰富) |
| 资源占用 | 较低,仅运行时核心 | 较高,包含完整平台组件 |
从 Kubernetes 的视角看,推荐 containerd 的原因非常明确:去除不必要的中介层,减少故障面,提升整体稳定性。Docker 虽然功能强大,但在 K8s 集群中,构建镜像等功能早已被 CI/CD 流水线接管,节点上仅需纯粹的容器运行时即可。
五、实训:在 Ubuntu 22.04 上部署 containerd
接下来我们在真实环境中完成 containerd 的安装与配置。
环境准备:一台 Ubuntu 22.04 节点,拥有 root 权限,可访问外网(如已安装 Docker 可保留用于后续对比)。
- 安装 containerd
|----------------------------------------------------------|
| Bash sudo apt update && sudo apt install -y containerd |
- 生成并修改默认配置
默认配置文件位于 /etc/containerd/config.toml。生成一份标准配置并启用 systemd cgroup 驱动:
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Bash sudo sed 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml | sudo tee /etc/containerd/config.toml.tmp > /dev/null sudo mv /etc/containerd/config.toml.tmp /etc/containerd/config.toml |
为何一定要启用 SystemdCgroup = true?Kubernetes 推荐使用 systemd 作为节点的 cgroup 管理器,如果 containerd 仍使用 cgroupfs,就会在同一节点上存在两层 cgroup 管理器,极易引起资源竞争和冲突。该选项位于配置文件中的:
|-------------------------------------------------------------------------------------------------------|
| TOML [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options] SystemdCgroup = true |


- 启动并验证服务
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Bash # 根目录确保 rw sudo mount -o remount,rw / sudo systemctl daemon-reload sudo systemctl restart containerd systemctl status containerd ls -l /run/containerd/containerd.sock |
确认 containerd 服务已正常运行,并且监听了 Unix Socket。

六、使用 ctr 操作 containerd 原生接口
ctr 是 containerd 自带的命令行工具,直接与原生 gRPC API 交互,不经过 CRI。它适合底层调试。
常用操作示例:
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Bash # 查看所有命名空间(默认使用 'k8s.io') sudo ctr ns ls # 列出k8s命名空间下所有容器 sudo ctr -n k8s.io c ls # 列出镜像 sudo ctr -n k8s.io i ls # 修复 DNS sudo chattr -i /etc/resolv.conf echo "nameserver 223.5.5.5" | sudo tee /etc/resolv.conf echo "nameserver 8.8.8.8" | sudo tee -a /etc/resolv.conf sudo systemctl restart systemd-resolved # 拉取 nginx 镜像(使用毫秒镜像) sudo ctr -n k8s.io image pull docker.1ms.run/library/nginx:alpine # 列出本地镜像 sudo ctr -n k8s.io image ls # 运行一个一次性容器(--rm 结束后自动删除) 方案 A:后台运行(推荐,可访问 80 端口) sudo ctr -n k8s.io run -d --net-host docker.1ms.run/library/nginx:alpine test-nginx 方案 B:前台测试(用完自动删除) sudo ctr -n k8s.io run --rm --net-host docker.1ms.run/library/nginx:alpine test-nginx # 查看正在运行的任务(task 代表进程实例) sudo ctr -n k8s.io task ls # 查看运行中的容器 sudo ctr -n k8s.io container ls |

在 containerd 的概念模型中,**container** 是一个静态的配置对象,而 task 代表实际运行中的进程实例。同一个 container 可以对应多个 task(例如容器重启后会产生新的 task)。
七、使用 crictl 通过 CRI 接口调试
crictl 是 Kubernetes 社区提供的 CRI 调试工具,它沿着与 kubelet 完全相同的 gRPC 路径与运行时交互,这对于验证 CRI 实现是否正常工作非常有帮助。
- 安装 crictl
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Bash VERSION="v1.30.0" wget https://github.com/kubernetes-sigs/cri-tools/releases/download/$VERSION/crictl-$VERSION-linux-amd64.tar.gz sudo tar zxvf crictl-$VERSION-linux-amd64.tar.gz -C /usr/local/bin |
- 创建连接配置 /etc/crictl.yaml
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Bash cat <<EOF | sudo tee /etc/crictl.yaml runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false EOF |
- 拉取镜像并创建 Pod 沙箱
|------------------------------------------------------------------------------|
| Bash sudo crictl pull docker.1ms.run/library/nginx:alpine sudo crictl images |

创建沙箱配置文件 pod-config.json:
|----------------------------------------------------------------------------------------------|
| JSON { "metadata": { "name": "nginx-sandbox", "uid": "test-uid" }, "log_directory": "/tmp" } |
运行沙箱(即 pause 容器)并获取其 ID:
|-------------------------------------------------|
| Bash POD_ID=$(sudo crictl runp pod-config.json) |
创建业务容器配置 container-config.json:
|--------------------------------------------------------------------------------|
| JSON { "metadata": { "name": "nginx" }, "image": { "image": "nginx:alpine" } } |
创建并启动容器:
|-----------------------------------------------------------------------------------------------------------------------|
| Bash CONTAINER_ID=(sudo crictl create POD_ID container-config.json pod-config.json) sudo crictl start $CONTAINER_ID |
验证容器已运行:
|----------------------------------------|
| Bash sudo crictl ps -a | grep Running |

通过 crictl,我们可以看到容器已经以 Pod 为单位组织:每个 Pod 先有一个 pause 容器(沙箱),然后业务容器加入该沙箱共享网络和 IPC 命名空间。这与 kubelet 创建 Pod 的过程完全一致。
ctr 与 crictl 的使用场景对比:
- ctr:直接操作 containerd 原生接口,无 Pod 概念,适合运行时底层调试;
- crictl:遵循 CRI 标准,有 Pod 概念,适合模拟 kubelet 行为、验证 K8s 节点运行时健康度。
八、containerd 与 Docker 进程模型对比实战
如果我们手头有一个同时安装了 Docker 的节点,可以直观对比两者的进程树。
Docker 环境:运行一个 nginx 容器
|---------------------------------------------------------------------------------------------------|
| Bash docker run -d --name nginx-test nginx:alpine pstree -p | grep -E "docker|containerd|runc" |
典型输出会看到 dockerd 调用 containerd,再调用 containerd-shim 和 nginx。
纯净 containerd 环境:如前述,只有
|-------------------------------------------------|
| Plain Text containerd─┬─containerd-shim─┬─nginx |
显然 containerd 路径更短,没有 dockerd 这个额外进程。这意味着少了一个守护进程的内存开销,也减少了因 dockerd 自身故障导致容器不可用的风险。
此外,镜像构建是 containerd 不能原生覆盖的场景。containerd 没有 docker build 命令,但可以配合 BuildKit 或 **nerdctl**(一个兼容 Docker CLI 风格的工具)来构建镜像,形成完整的容器工作流。
九、containerd 完整工作流程与日志分析
让我们从 CRI 请求的角度把整个容器启动流程串起来:
- crictl runp 发送 RunPodSandbox 请求 → containerd API 层接收。
- Core 创建 sandbox 元数据,Snapshotter 为 pause 容器准备 rootfs(基于 overlayfs 快照)。
- Runtime 层创建 containerd-shim 进程,shim 调用 runc 启动 pause 容器。
- 若有 CNI 配置,此时会配置 Pod 的网络命名空间。
- crictl create 发送 CreateContainer,在指定 sandbox 内创建容器元数据,Snapshotter 为业务容器准备 rootfs。
- crictl start 发送 StartContainer,shim 调用 runc 启动业务进程,并把它加入到 pause 的网络命名空间中。
我们可以通过日志验证这一流程:
|------------------------------------------------|
| Bash journalctl -u containerd -n 50 --no-pager |
关键日志行示例:
- PullImage "nginx:alpine" ------ 镜像拉取开始
- content digest: sha256:... ------ 镜像清单校验通过
- CreateContainer within sandbox ... ------ 容器创建
- StartContainer for ... returns sandbox ID ... ------ 容器启动
- shim disconnected ------ 容器退出时 shim 断开连接
错误日志通常以 level=error 开头,包含运行时、快照或插件故障的详细信息,是日常排障的重要入口。
容器运行时是 Kubernetes 的"发动机",掌握它是成为可靠集群管理员的必经之路。希望本文能让你向着透明化、可控化的运维目标再近一步。
本文为"搭建DevOps企业级仿真实验环境"系列的一部分,所有内容均基于实际硬件环境(32核64线程 / 128G内存 / 6T硬盘)编写,力求贴近真实企业部署场景。
欢迎各位 DevOps、SRE 爱好者,在评论区留言交流探讨,互相学习。