概述
Kubernetes v1.24 正式移除了 dockershim,这意味着 Docker Engine 不再作为 K8s 的容器运行时。取而代之的是符合 CRI(Container Runtime Interface)规范的运行时,其中 containerd 是事实上的标准选择。
很多运维人员对 containerd 的认知停留在"Docker 的替代品",但实际上 containerd 的架构、镜像管理、命令行工具都与 Docker 有显著差异。本文深入理解 CRI 规范、containerd 架构、日常运维和从 Docker 迁移的实践。
本文基于 containerd v2.0 和 Kubernetes v1.30。参考 containerd 官方文档
CRI 规范
什么是 CRI
CRI(Container Runtime Interface)是 K8s 定义的容器运行时接口规范。K8s 不再直接调用容器运行时,而是通过 gRPC 调用符合 CRI 规范的运行时:
kubelet → CRI gRPC → 容器运行时(containerd/CRI-O)→ 容器
CRI 接口定义
CRI 定义了两个核心服务:
| 服务 | 功能 | 关键方法 |
|---|---|---|
RuntimeService | 容器和 Pod 生命周期 | RunPodSandbox, CreateContainer, StartContainer, StopContainer |
ImageService | 镜像管理 | ListImages, PullImage, RemoveImage, ImageStatus |
// CRI 接口简化定义
service RuntimeService {
rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse);
rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse);
rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse);
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse);
rpc StartContainer(StartContainerRequest) returns (StartContainerResponse);
rpc StopContainer(StopContainerRequest) returns (StopContainerResponse);
rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse);
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse);
rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse);
rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse);
}
service ImageService {
rpc ListImages(ListImagesRequest) returns (ListImagesResponse);
rpc PullImage(PullImageRequest) returns (PullImageResponse);
rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse);
rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse);
}
为什么移除 dockershim
Docker Engine 不原生支持 CRI,K8s 通过 dockershim 适配层调用 Docker。这带来几个问题:
| 问题 | 说明 |
|---|---|
| 额外适配层 | dockershim 增加了一层调用,降低效率 |
| 维护负担 | K8s 维护 dockershim 代码,但 Docker 更新频繁 |
| 功能限制 | dockershim 无法利用 CRI 的高级特性 |
| 额外进程 | Docker Engine 包含 dockerd、containerd、docker-proxy 等多个进程 |
移除 dockershim 后,直接使用 containerd 去掉了中间层:
# 旧架构(Docker)
kubelet → dockershim → dockerd → containerd → containerd-shim → runc → 容器
# 新架构(containerd)
kubelet → CRI Plugin → containerd → containerd-shim → runc → 容器
Docker vs containerd vs CRI-O
三大运行时对比
| 特性 | Docker Engine | containerd | CRI-O |
|---|---|---|---|
| CRI 支持 | 需 dockershim | 原生支持 | 原生支持 |
| 架构 | 多进程(dockerd + containerd) | 单进程 | 单进程 |
| 镜像格式 | Docker Image | OCI Image | OCI Image |
| CLI | docker | nerdctl / ctr | crictl |
| 镜像存储 | 独立 | 独立 | 独立 |
| 网络管理 | docker-proxy | CNI | CNI |
| 构建能力 | docker build | nerdctl build / buildkit | buildah |
| 适用场景 | 开发环境 | K8s 生产 | OpenShift |
| 资源开销 | 高 | 低 | 低 |
架构对比
# Docker Engine 架构
dockerd(HTTP API + 镜像构建 + 卷管理)
└── containerd(容器生命周期)
└── containerd-shim(容器进程管理)
└── runc(OCI 运行时)
# containerd 架构
containerd(CRI + 容器生命周期 + 镜像管理)
└── containerd-shim(容器进程管理)
└── runc(OCI 运行时)
# CRI-O 架构
cri-o(CRI + 容器生命周期 + 镜像管理)
└── conmon(容器进程监控)
└── runc(OCI 运行时)
资源开销对比
| 指标 | Docker | containerd | CRI-O |
|---|---|---|---|
| 常驻内存 | ~150MB | ~50MB | ~40MB |
| 磁盘占用 | ~200MB | ~80MB | ~70MB |
| 启动时间 | ~2s | ~0.5s | ~0.5s |
| 镜像拉取速度 | 基准 | 快 10-15% | 快 10-15% |
数据为大致参考值,实际取决于节点配置和负载。
containerd 架构
核心组件
containerd
├── Containerd API (gRPC API,供外部调用)
├── Containerd Runtime
│ ├── Tasks Manager (容器任务管理)
│ ├── Runtime Shim (OCI 运行时 shim)
│ └── runc (OCI 低级运行时)
├── Containerd Images
│ ├── Pull / Push (镜像拉取/推送)
│ ├── Image Store (镜像存储)
│ └── Content Store (镜像内容存储)
├── Containerd Snapshots
│ └── Snapshotter (快照管理,overlayfs/btrfs)
├── Containerd Metadata
│ └── BoltDB (元数据存储)
└── Containerd Events (事件系统)
containerd 配置文件
# /etc/containerd/config.toml
version = 2
root = "/var/lib/containerd"
state = "/run/containerd"
oom_score = 0
[grpc]
address = "/run/containerd/containerd.sock"
uid = 0
gid = 0
[metrics]
address = "127.0.0.1:8080"
grpc_histogram = false
[debug]
address = ""
uid = 0
gid = 0
level = "info"
# CRI 插件配置
[plugins."io.containerd.grpc.v1.cri"]
# sandbox_image = "registry.k8s.io/pause:3.9"
sandbox_image = "registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.9"
# 容器运行时
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "runc"
snapshotter = "overlayfs"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true # 使用 systemd cgroup
# 镜像拉取配置
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d" # 镜像仓库配置目录
# 镜像 GC 策略
[plugins."io.containerd.grpc.v1.cri".image_decryption]
key_model = "node"
# 日志配置
[plugins."io.containerd.grpc.v1.cri".containerd.log]
max_size = "100MB"
max_files = 3
systemd cgroup 驱动
K8s v1.26+ 要求容器运行时使用 systemd cgroup 驱动:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
如果不配置,kubelet 和 containerd 的 cgroup 驱动不一致会导致不稳定。
镜像管理
镜像拉取加速配置
# 方式一:config.toml 中配置镜像仓库(旧方式,已废弃)
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://mirror.ccs.tencentyun.com", "https://registry-1.docker.io"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.k8s.io"]
endpoint = ["https://registry.cn-hangzhou.aliyuncs.com/google_containers"]
# 方式二:hosts.toml 方式(containerd 1.7+,推荐)
mkdir -p /etc/containerd/certs.d/docker.io
cat > /etc/containerd/certs.d/docker.io/hosts.toml << 'EOF'
server = "https://registry-1.docker.io"
[host."https://mirror.ccs.tencentyun.com"]
capabilities = ["pull", "resolve"]
[host."https://docker.1panel.live"]
capabilities = ["pull", "resolve"]
EOF
# k8s.io 镜像
mkdir -p /etc/containerd/certs.d/registry.k8s.io
cat > /etc/containerd/certs.d/registry.k8s.io/hosts.toml << 'EOF'
server = "https://registry.k8s.io"
[host."https://registry.cn-hangzhou.aliyuncs.com/google_containers"]
capabilities = ["pull", "resolve"]
EOF
# 私有仓库认证
mkdir -p /etc/containerd/certs.d/harbor.example.com
cat > /etc/containerd/certs.d/harbor.example.com/hosts.toml << 'EOF'
server = "https://harbor.example.com"
[host."https://harbor.example.com"]
capabilities = ["pull", "resolve"]
ca = "/etc/containerd/certs.d/harbor.example.com/ca.crt"
# 或跳过 TLS 验证(不推荐生产使用)
# skip_verify = true
EOF
# config.toml 中指向 certs.d 目录
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
私有仓库认证
# 方式一:K8s Secret(Pod 拉取镜像时使用)
kubectl create secret docker-registry harbor-secret \
--docker-server=harbor.example.com \
--docker-username=admin \
--docker-password=Harbor12345 \
--docker-email=admin@example.com \
-n production
# Pod 中引用
# imagePullSecrets:
# - name: harbor-secret
# 方式二:节点级认证(所有 Pod 共享)
# 创建认证目录
mkdir -p /etc/containerd/certs.d/harbor.example.com
# 登录生成认证文件
crictl login harbor.example.com -u admin -p Harbor12345
# 或手动写入
cat > /var/lib/kubelet/config.json << 'EOF'
{
"auths": {
"harbor.example.com": {
"auth": "YWRtaW46SGFyYm9yMTIzNDU="
}
}
}
EOF
命名空间
containerd 命名空间
containerd 使用命名空间隔离不同客户端的容器和镜像:
| 命名空间 | 使用者 | 说明 |
|---|---|---|
k8s.io | kubelet | K8s 管理的容器和镜像 |
moby | Docker | Docker Engine 管理的容器(如果安装了 Docker) |
default | nerdctl / ctr | 默认命名空间 |
buildkit | buildkitd | 构建过程中使用的镜像 |
# 查看所有命名空间
ctr namespaces list
# 在指定命名空间操作
ctr -n k8s.io images list
ctr -n k8s.io containers list
重要:K8s 管理的容器和镜像在
k8s.io命名空间下。用ctr操作时要加-n k8s.io,否则看不到 K8s 的资源。
CLI 工具
ctr:containerd 原生 CLI
ctr 是 containerd 自带的命令行工具,功能最全但不够友好:
# 镜像操作
ctr -n k8s.io images list # 列出镜像
ctr -n k8s.io images pull docker.io/library/nginx:1.25-alpine # 拉取镜像
ctr -n k8s.io images push harbor.example.com/myapp:v1 # 推送镜像
ctr -n k8s.io images delete myapp:v1 # 删除镜像
ctr -n k8s.io images export app.tar myapp:v1 # 导出镜像
ctr -n k8s.io images import app.tar # 导入镜像
# 容器操作
ctr -n k8s.io containers list # 列出容器
ctr -n k8s.io tasks list # 列出运行中的任务
crictl:K8s 容器调试工具
crictl 是 K8s 社区维护的 CRI 调试工具,与 K8s 概念对齐:
# 查看 Pod
crictl pods
# 查看容器
crictl ps
crictl ps -a # 包括已停止的
crictl ps --name nginx # 按名称过滤
# 查看镜像
crictl images
crictl images --digests # 显示摘要
# 查看容器日志
crictl logs <container-id>
crictl logs -f <container-id> # 跟踪日志
# 在容器中执行命令
crictl exec -it <container-id> sh
# 查看容器状态
crictl inspect <container-id>
crictl stats
# 拉取镜像
crictl pull nginx:1.25-alpine
# /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 10
debug: false
nerdctl:Docker 兼容 CLI
nerdctl 是 containerd 社区开发的 Docker 兼容 CLI,推荐作为 Docker CLI 的替代:
# 基本与 docker 命令兼容
nerdctl ps # 列出容器
nerdctl images # 列出镜像
nerdctl pull nginx:1.25-alpine # 拉取镜像
nerdctl run -d --name nginx -p 80:80 nginx:1.25-alpine # 运行容器
nerdctl exec -it nginx sh # 进入容器
nerdctl logs -f nginx # 查看日志
nerdctl stop nginx # 停止容器
nerdctl rm nginx # 删除容器
nerdctl rmi nginx:1.25-alpine # 删除镜像
# 构建镜像(需要 buildkitd)
nerdctl build -t myapp:v1 .
nerdctl build -t myapp:v1 --platform linux/amd64,linux/arm64 .
# compose 支持
nerdctl compose up -d
nerdctl compose down
# 指定命名空间
nerdctl --namespace=k8s.io ps
nerdctl --namespace=default ps
工具对比
| 功能 | ctr | crictl | nerdctl |
|---|---|---|---|
| 镜像管理 | 支持 | 支持 | 支持 |
| 容器管理 | 支持 | 只读 | 支持 |
| 容器创建 | 支持 | 不支持 | 支持 |
| 镜像构建 | 不支持 | 不支持 | 支持 |
| compose | 不支持 | 不支持 | 支持 |
| 日志查看 | 不支持 | 支持 | 支持 |
| exec | 不支持 | 支持 | 支持 |
| K8s 对齐 | 否 | 是 | 否 |
| Docker 兼容 | 否 | 否 | 是 |
推荐:日常运维用 crictl(与 K8s 概念一致),替代 Docker CLI 用 nerdctl,底层调试用 ctr。
snapshotter
快照器的作用
snapshotter 负责管理容器文件系统的层叠。每拉取一个镜像层,snapshotter 创建一个快照;运行容器时,在最上层创建可读写层。
支持的 snapshotter
| snapshotter | 说明 | 适用场景 |
|---|---|---|
overlayfs | 默认,基于 OverlayFS | 通用(推荐) |
btrfs | 基于 Btrfs 文件系统 | 需要 Btrfs 支持 |
zfs | 基于 ZFS 文件系统 | 需要 ZFS 支持 |
native | 简单的文件拷贝 | 测试环境 |
stargz | 懒加载镜像 | 大镜像加速启动 |
nydus | 镜像加速(蚂蚁开源) | 大镜像加速启动 |
overlayfs 配置
[plugins."io.containerd.grpc.v1.cri".containerd]
snapshotter = "overlayfs"
# 查看 overlayfs 挂载
mount | grep overlay
# 查看镜像层
ctr -n k8s.io snapshots list
Stargz / Nydus 懒加载
传统镜像拉取是全量的,大镜像(GB 级)拉取慢。Stargz 和 Nydus 通过懒加载解决:
# 启用 stargz snapshotter
[plugins."io.containerd.snapshotter.v1.stargz"]
root_path = "/var/lib/containerd-stargz-grpc"
# 传统镜像拉取:全量下载 → 启动容器(慢)
# 懒加载镜像:元数据下载 → 启动容器 → 按需下载(快)
性能对比
镜像拉取性能
# 测试条件:1GB 镜像,百兆内网
Docker: ~85s (包含 dockerd 调度开销)
containerd: ~72s (直接 CRI 调用)
CRI-O: ~73s (直接 CRI 调用)
containerd + stargz: ~15s(懒加载,按需拉取)
容器启动性能
# 测试条件:nginx:alpine,100 个并发容器
Docker: ~8s
containerd: ~5s
CRI-O: ~5s
内存占用
# 空载状态下
Docker(dockerd + containerd): ~150MB
containerd: ~50MB
CRI-O: ~40MB
从 Docker 迁移到 containerd
迁移评估
| 检查项 | 说明 |
|---|---|
| 镜像构建 | Docker Build 替换为 BuildKit / nerdctl build |
| CLI 工具 | docker 命令替换为 crictl / nerdctl |
| 镜像仓库 | 认证配置方式不同 |
| 监控脚本 | 依赖 docker CLI 的脚本需更新 |
| 日志驱动 | containerd 不支持 Docker 的日志驱动 |
| docker.sock | 依赖 Docker socket 的应用需改造 |
迁移步骤
# 1. 安装 containerd
apt-get update && apt-get install -y containerd
# 2. 生成默认配置
containerd config default > /etc/containerd/config.toml
# 3. 修改配置(SystemdCgroup + 镜像加速)
# 编辑 /etc/containerd/config.toml,确保:
# SystemdCgroup = true
# config_path = "/etc/containerd/certs.d"
# 4. 配置镜像加速
mkdir -p /etc/containerd/certs.d/docker.io
cat > /etc/containerd/certs.d/docker.io/hosts.toml << 'EOF'
server = "https://registry-1.docker.io"
[host."https://mirror.ccs.tencentyun.com"]
capabilities = ["pull", "resolve"]
EOF
# 5. 重启 containerd
systemctl restart containerd
systemctl enable containerd
# 6. 修改 kubelet 配置
# /var/lib/kubelet/kubeadm-flags.env
KUBELET_KUBEADM_ARGS="--container-runtime=remote --container-runtime-endpoint=unix:///run/containerd/containerd.sock"
# 7. 重启 kubelet
systemctl restart kubelet
# 8. 验证
kubectl get nodes -o wide
# 验证 CONTAINER-RUNTIME 列显示 containerd://
镜像迁移
# 导出 Docker 镜像
docker save -o myapp.tar myapp:v1
# 导入到 containerd
ctr -n k8s.io images import myapp.tar
# 或用 nerdctl
nerdctl -n k8s.io load -i myapp.tar
脚本迁移
# Docker 命令 → containerd 等价命令对照
# 列出容器
docker ps → crictl ps
# 列出镜像
docker images → crictl images
# 拉取镜像
docker pull nginx → crictl pull nginx
# 查看日志
docker logs <id> → crictl logs <id>
# 进入容器
docker exec -it <id> sh → crictl exec -it <id> sh
# 查看容器详情
docker inspect <id> → crictl inspect <id>
# 查看容器资源
docker stats → crictl stats
# 构建镜像
docker build -t app . → nerdctl build -t app .
# 运行容器
docker run -d nginx → nerdctl run -d nginx
# 删除镜像
docker rmi nginx → crictl rmi nginx
日常运维
镜像 GC 配置
# K8s 镜像 GC 通过 kubelet 管理,不在 containerd 配置
# /var/lib/kubelet/config.yaml
imageGCHighThresholdPercent: 85 # 磁盘使用率超过85%触发GC
imageGCLowThresholdPercent: 80 # GC到80%停止
imageMinimumGCAge: 2m # 镜像至少存在2分钟才可被GC
日志管理
containerd 不像 Docker 有丰富的日志驱动。容器日志通过 kubelet 管理,输出到 /var/log/pods/:
# 查看容器日志
crictl logs <container-id>
# K8s Pod 日志路径
ls /var/log/pods/<namespace>_<pod-name>_<pod-uid>/
# 日志轮转配置(kubelet)
# /var/lib/kubelet/config.yaml
containerLogMaxSize: "100Mi"
containerLogMaxFiles: 5
性能调优
# containerd 性能调优
version = 2
# 提高 gRPC 并发
[grpc]
max_concurrent_streams = 1000
# 调整镜像拉取并发
[plugins."io.containerd.grpc.v1.cri"]
max_concurrent_downloads = 5 # 镜像并发下载数(默认3)
# overlayfs 性能
[plugins."io.containerd.snapshotter.v1.overlayfs"]
upperdir_capability = true
故障排查
# 查看 containerd 状态
systemctl status containerd
# 查看 containerd 日志
journalctl -u containerd -f
# 查看 CRI 状态
crictl info
# 查看 containerd metrics
curl -s http://localhost:8080/metrics | grep containerd
# 常见问题
# 1. Pod 创建失败 - ImagePullBackOff
crictl images | grep <image> # 检查镜像是否存在
ctr -n k8s.io images pull <image> # 手动拉取测试
# 2. 容器启动失败
crictl inspect <container-id> # 查看容器详情
journalctl -u containerd | grep <container-id> # 查看日志
# 3. 镜像拉取慢
cat /etc/containerd/certs.d/*/hosts.toml # 检查镜像加速配置
总结
containerd 已成为 K8s 的事实标准容器运行时,核心要点:
- CRI 是规范,containerd 是实现:理解 CRI 规范有助于理解 K8s 与运行时的交互方式。
- 三个 CLI 各有定位:
crictl用于 K8s 运维(只读操作为主),nerdctl作为 Docker CLI 替代,ctr用于底层调试。 - 镜像加速必须配:国内环境必须配置镜像加速,使用
certs.d目录方式(hosts.toml),旧版配置方式已废弃。 - SystemdCgroup 必须开:K8s 要求 cgroup 驱动一致,不配置会导致不稳定。
- 命名空间隔离:K8s 的容器和镜像在
k8s.io命名空间,操作时要加-n k8s.io。 - 迁移要有计划:从 Docker 迁移到 containerd 需要评估依赖 Docker socket 的应用、监控脚本等,不能直接切换。
- 大镜像用懒加载:Stargz / Nydus 可以显著加速大镜像的启动,适合 CI/CD 场景。
containerd 的设计比 Docker 更简洁高效,但也意味着一些 Docker 的便利功能(如 docker build、丰富的日志驱动)需要用替代方案。理解这些差异是平滑迁移的关键。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- containerd 官方文档 — Containerd,参考了containerd 官方文档相关内容