搭建DevOps企业级仿真实验环境:012容器运行时 containerd 详解

引言

在 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 可保留用于后续对比)。

  1. 安装 containerd

|----------------------------------------------------------|
| Bash sudo apt update && sudo apt install -y containerd |

  1. 生成并修改默认配置

默认配置文件位于 /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 |

  1. 启动并验证服务

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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 实现是否正常工作非常有帮助。

  1. 安装 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 |

  1. 创建连接配置 /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 |

  1. 拉取镜像并创建 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 请求的角度把整个容器启动流程串起来:

  1. crictl runp 发送 RunPodSandbox 请求 → containerd API 层接收。
  1. Core 创建 sandbox 元数据,Snapshotter 为 pause 容器准备 rootfs(基于 overlayfs 快照)。
  1. Runtime 层创建 containerd-shim 进程,shim 调用 runc 启动 pause 容器。
  1. 若有 CNI 配置,此时会配置 Pod 的网络命名空间。
  1. crictl create 发送 CreateContainer,在指定 sandbox 内创建容器元数据,Snapshotter 为业务容器准备 rootfs。
  1. 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 爱好者,在评论区留言交流探讨,互相学习。

相关推荐
MXsoft6181 小时前
**运维标准化建设:让杂乱无章的工作变成可复制****流程**
运维
云游牧者1 小时前
K8S安全框架深度解析-从认证到RBAC实战完全指南
安全·容器·kubernetes·rbac·kubeconfig·rolebinding
maosheng11461 小时前
第二次作业(RHCE(https+http))
运维
古城小栈2 小时前
K8s 核心知识 讲解
docker·容器·kubernetes
杨云龙UP2 小时前
MySQL主库高峰期备份引发504故障:从库手动切换接管 + 主从恢复同步 + Docker版DB2重启实战_2026-05-17
linux·运维·数据库·mysql·docker·容器·centos
曾帅1682 小时前
linux ubuntu 挂载硬盘
linux·运维·ubuntu
Yjiokm2 小时前
proot-distro 安装指定版本 ubuntu
linux·运维·ubuntu
lifewange2 小时前
ls -ltr
linux·运维·服务器
say_fall2 小时前
Git完全入门指南-从概念到实战掌握版本控制的核心
linux·运维·服务器·git·学习