当你在服务器上敲下 docker run 时,真正拉起容器进程的并不是 Docker 守护进程本身,而是藏在它背后的 containerd——一个工业级的容器运行时(container runtime)。理解 containerd,是写好 Dockerfile 之后通往生产级容器管理的下一站:它既是 Docker 的底层引擎,也是 Kubernetes 通过 CRI 调用的事实标准运行时。本文从原理到命令,带你把 containerd 用在生产环境。
一、containerd 是什么:为什么 Docker 背后是它
容器生态里长期存在”运行时”的分层。早年的 Docker 引擎大而全,包含了镜像构建、网络、编排等大量功能;而 containerd 是从 Docker 中抽离出来的核心运行时,专注于”管好镜像、管好容器生命周期”这一件事。2017 年 Docker 将 containerd 捐献给 CNCF,如今它已成为云原生领域最基础的组件之一。
用一句话区分三者关系:Docker CLI 调用 dockerd,dockerd 调用 containerd,containerd 再通过 runc 这样的底层运行时真正创建容器进程。在 Kubernetes 世界里,kubelet 则通过 CRI(Container Runtime Interface)插件直接对接 containerd,完全绕开 dockerd。这也是为什么很多生产集群选择”只装 containerd 不装 Docker”。
二、安装与版本对齐
containerd 的版本必须和 Kubernetes 版本匹配,错配会导致 CRI 接口不兼容。下面以 Ubuntu 22.04 为例,使用官方二进制方式安装,避免被发行版旧包坑到。
# 下载并解压 containerd 二进制(以 v1.7.x 为例)
wget https://github.com/containerd/containerd/releases/download/v1.7.20/containerd-1.7.20-linux-amd64.tar.gz
tar -C /usr/local -xzf containerd-1.7.20-linux-amd64.tar.gz
# 安装 runc(底层 OCI 运行时)
wget https://github.com/opencontainers/runc/releases/download/v1.1.12/runc.amd64
install -m 755 runc.amd64 /usr/local/sbin/runc
# 生成默认配置并启动
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
systemctl enable --now containerd
systemctl status containerd --no-pager
如果你更习惯用 Docker Compose 一键编排多服务,可以保留 Docker 不动——Docker 24+ 默认底层就是 containerd,两者并不冲突。而在 k3s 轻量级 Kubernetes 集群 中,containerd 已是默认内置运行时。
三、核心概念:五个你必须分清的对象
操作 containerd 前,先理清它的对象模型,否则很容易把”镜像”和”容器”搞混:
| 对象 | 作用 | 类比 |
|---|---|---|
| Namespace | 资源隔离边界,默认 default,K8s 用 k8s.io | 租户/环境 |
| Image | 拉取后存放的只读镜像 | 安装包 |
| Container | 被创建的容器元数据(尚未运行) | 已配置的实例 |
| Task | 真正运行的进程(容器跑起来后才有) | 运行中的进程 |
| Snapshot | 容器可写层/镜像只读层的存储 | 文件系统层 |
关键区别:Container 只是”定义”,Task 才是”运行”。一个 Container 同一时刻只能有一个 Task,停掉 Task 后 Container 还在,可以再次 start 拉起新 Task。这一点和 Docker 的 docker create / docker start 一一对应。
四、日常管理:ctr 与 nerdctl
containerd 自带命令行 ctr,但它偏底层、不带镜像加速等友好特性,更适合排障。日常推荐用 nerdctl(兼容 Docker CLI 语法的 containerd 客户端)。
# ctr:指定命名空间查看镜像(注意必须带 -n 命名空间)
ctr -n k8s.io images ls
ctr images pull docker.io/library/nginx:1.27
ctr containers create docker.io/library/nginx:1.27 nginx-demo
ctr tasks start -d nginx-demo
ctr tasks ls
# nerdctl:语法几乎和 docker 一致,体验更顺手
nerdctl pull nginx:1.27
nerdctl run -d --name nginx-demo -p 8080:80 nginx:1.27
nerdctl ps
nerdctl images
五、配置镜像加速与私有仓库
国内拉取 Docker Hub 经常超时,需要在 /etc/containerd/config.toml 中配置镜像加速。注意 containerd 1.7 的配置结构和旧版差异较大,下面是最常用的写法:
# /etc/containerd/config.toml 关键片段
version = 2
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://registry.cn-hangzhou.aliyuncs.com", "https://docker.m.daocloud.io"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.example.com"]
endpoint = ["https://registry.example.com"]
# 私有仓库若用自签证书,需在此声明 ca 与明文(insecure)跳过
[plugins."io.containerd.grpc.v1.cri".registry.configs."registry.example.com".tls]
insecure_skip_verify = true
# 改完务必重启
systemctl restart containerd
| 场景 | 配置位置 | 常见坑 |
|---|---|---|
| Docker Hub 加速 | registry.mirrors.”docker.io” | 只改了 endpoint 没重启 |
| 私有仓库 | registry.configs.”域名”.tls | 自签证书未信任导致拉取 401/x509 |
| K8s 镜像 | 默认走 k8s.io 命名空间 | 用 ctr 忘加 -n k8s.io 看不到 |
六、与 Kubernetes 集成:CRI 与 runtimeClass
在 Kubernetes 的 Pod、Deployment、Service 体系中,kubelet 通过 CRI 调用 containerd。只需在 kubelet 启动参数里指向它即可:
# kubelet 关键参数(kubeadm 通常已自动配好)
--container-runtime=remote
--container-runtime-endpoint=unix:///run/containerd/containerd.sock
# 用 runtimeClass 让特定 Pod 走不同运行时(例如 runsc/gVisor)
kubectl get runtimeclasses
# 若集群同时装了 runc 与 runsc,可在 Pod spec 指定
# spec.runtimeClassName: runsc
当节点上 containerd 异常时,Cilium 基于 eBPF 的网络策略 往往仍在工作,但新 Pod 会卡在 ContainerCreating——这时第一时间 systemctl status containerd 和 ctr -n k8s.io tasks ls 排障最快。
七、生产级调优
默认配置在单机跑跑demo足够,但生产环境建议调以下几处,避免镜像多、并发高时把节点拖垮:
| 调优项 | 建议值 | 原因 |
|---|---|---|
| io.concurrent 下载并发 | 10→20 | 加速大镜像拉取 |
| snapshotter | overlayfs(默认) | 比 native 更快,需内核支持 |
| registry 并发限制 | 按需放开 | 避免瞬间打满带宽 |
| 日志大小限制 | max-size=100m, max-file=5 | 防止容器日志吃满磁盘 |
# 日志轮转配置片段(config.toml)
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "runc"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true # 配合 systemd 的 cgroup v2
# 容器日志上限(关键!否则 /var/log 会被撑爆)
[plugins."io.containerd.grpc.v1.cri".containerd]
max_container_log_line_size = 16384
配合 Prometheus + Grafana 监控面板,把 containerd 的 container_blkio、任务数、镜像数等指标纳入看板,能在磁盘或内存异常前提前告警。
八、常见误区与排障清单
| 现象 | 根因 | 解法 |
|---|---|---|
| ctr images ls 看不到镜像 | 忘了 -n k8s.io | 加命名空间参数 |
| 拉取私有仓库报 x509 | 自签证书未配置 | configs.”域名”.tls 信任或 insecure |
| Pod 卡 ContainerCreating | containerd 未就绪 | systemctl restart containerd + 查 sock |
| 磁盘被镜像占满 | 未做垃圾回收 | ctr images prune 或 crictl rmi |
清理悬空镜像与快照是运维日常:ctr -n k8s.io images prune 可释放大量空间;若节点磁盘告警,优先排查 /var/lib/containerd 下的 overlay 快照而非盲目重启。
九、生产落地清单
| 检查项 | 是否完成 |
|---|---|
| containerd + runc 版本与 K8s 对齐 | ☐ |
| 配置国内镜像加速 | ☐ |
| 私有仓库证书/ insecure 已声明 | ☐ |
| SystemdCgroup = true(cgroup v2) | ☐ |
| 容器日志 max-size 限制 | ☐ |
| 接入监控 + 磁盘告警 | ☐ |
十、containerd 与 Docker:生产环境如何取舍
一个常见误区是”上了 containerd 就必须卸载 Docker”,事实并非如此。Docker 提供了完整的开发者体验:镜像构建、Compose 编排、友好的 CLI,这些 containerd 并不擅长。真正的变化发生在 Kubernetes 侧——自 1.24 版本起,K8s 移除了内置的 dockershim,kubelet 不再能直接驱动 Docker,节点运行时因此普遍转向 containerd 或 CRI-O。换句话说,开发机用 Docker 编写镜像、生产节点用 containerd 运行镜像,两者通过 OCI 标准镜像无缝衔接,才是当前主流的最佳实践。
| 维度 | Docker | containerd |
|---|---|---|
| 定位 | 开发者全功能平台 | 生产级运行时 |
| 镜像构建 | 原生支持 build | 需配合 buildkit / nerdctl |
| CLI 友好度 | 极高 | ctr 偏底层,nerdctl 接近 |
| K8s 集成 | 经 dockershim(已弃用) | 原生 CRI 对接 |
| 资源占用 | 较重 | 轻量 |
如果你的团队还没有全面拥抱 Kubernetes,或者重度依赖 Docker Compose 一键编排,保留 Docker 完全没问题;只要把”镜像制作”和”运行时”的职责分开,就能同时享受 Docker 的易用与 containerd 的高效。另外别忘了,镜像拉取下来不清理会持续蚕食磁盘,建议在 config.toml 中配置 content 与 snapshot 的垃圾回收阈值,或写一条定时任务执行 ctr -n k8s.io images prune,把节点磁盘健康交给自动化而不是人工巡检。
小结:containerd 把”容器运行时”这件大事做得足够纯粹——它不抢 Docker 的编排饭碗,也不越界做 K8s 的调度,而是稳稳站在两者之下。掌握它的命名空间模型、镜像配置与 CRI 集成方式,你就能在 Nginx 反向代理 与 GitHub Actions CI/CD 流水线 之外,把容器底座真正握在自己手里。当你下次遇到 Pod 起不来,第一反应不再是”重启 Docker”,而是直奔 containerd 看 Task 与快照——这就是生产级运维的差别。




