Containerd容器运行时:从核心架构到Kubernetes生产实践
1. 容器运行时演进与Containerd的定位
如果你在运维Kubernetes集群,或者在生产环境中与Docker打过交道,那么“containerd”这个名字你一定不陌生。但很多时候,它就像一个默默无闻的幕后英雄,被Docker或Kubelet的光环所掩盖。简单来说,containerd是一个行业标准的容器运行时,它负责管理容器的完整生命周期——从镜像的拉取、解压,到容器的创建、启动、停止和删除,再到底层存储和网络命名空间的隔离。它不是一个直接面向最终用户的工具,而是一个被设计为嵌入到更大系统中的核心引擎。
为什么我们需要专门了解它?因为在现代云原生架构中,容器运行时的角色正在被清晰地解耦和标准化。早期,Docker Engine是一个“大而全”的解决方案,集成了运行时、构建、镜像管理、API和CLI。这种捆绑带来了便利,但也引入了复杂性、臃肿和潜在的维护负担。随着Kubernetes成为容器编排的事实标准,社区需要一个更轻量、更专注、更稳定的底层运行时。于是,containerd从Docker项目中孵化并捐赠给了云原生计算基金会(CNCF),成为了一个独立的顶级项目。今天,无论是Docker Desktop还是Kubernetes(通过CRI插件),其底层默认的运行时引擎都是containerd。理解containerd,就是理解现代容器技术的基石,它能帮助你在排查问题、优化性能、甚至构建自己的容器平台时,拥有更清晰的视野和更直接的控制力。
2. Containerd核心架构深度解析
要驾驭containerd,不能只停留在命令层面,必须对其内部架构有一个清晰的认知。它的设计遵循了“单一职责”和“模块化”原则,各个组件通过清晰的接口进行通信。
2.1 分层架构与核心组件
Containerd采用客户端-服务器架构,主要由以下几个核心层和组件构成:
客户端接口层:这是与containerd交互的入口。它提供了多种客户端协议,最常用的是gRPC API。Docker Engine、Kubernetes的kubelet(通过
containerd-shim和CRI插件)都是通过这套gRPC API与containerd守护进程通信的。此外,它也提供了一个名为ctr的命令行工具,虽然功能不如dockerCLI丰富,但它是直接与daemon对话的“手术刀”,非常适合调试和深入操作。核心服务层:这是containerd的大脑,运行在一个常驻守护进程(
containerd)中。它内部又包含了多个关键服务:- 内容服务:管理所有不可变的内容,主要是镜像的层(Blobs)。它负责从镜像仓库拉取内容,并存储在本地的内容可寻址存储(CAS)中。
- 镜像服务:管理镜像的元数据。它将镜像视为一个清单文件,该文件指向内容服务中的多个层,并包含配置信息。镜像服务不存储实际数据,只存储索引关系。
- 容器服务:管理容器的元数据和生命周期。当创建一个容器时,容器服务会记录其配置(如要使用的镜像、启动命令、环境变量等),但此时并不运行任何进程。
- 任务服务:这是真正让容器“动起来”的服务。它负责根据容器配置创建实际的进程(任务)。一个容器可以关联多个任务(例如,
docker exec就会创建新任务),但主进程任务只有一个。
运行时层:任务服务在需要执行容器进程时,会调用底层的运行时。Containerd支持通过
shim架构来适配不同的低级运行时。containerd-shim:这是一个关键的设计。每个容器进程都由一个独立的shim进程管理。Shim作为容器进程的父进程,主要有几个作用:第一,它允许containerd daemon在启动容器后退出或重启,而不影响正在运行的容器(实现了daemon与容器的生命周期解耦);第二,它将容器的标准输入输出(stdio)转发到日志驱动(如json-file);第三,它负责收集容器退出后的状态并汇报给containerd。我们常用的runc就是通过containerd-shim-runc-v2来调用的。- 运行时:最常用的是
runc,它是一个符合OCI(开放容器倡议)运行时标准的轻量级工具,直接利用Linux内核的cgroups和namespaces来创建隔离的容器环境。Containerd也支持其他运行时,如gVisor(安全沙箱)、Kata Containers(轻量级虚拟机)等,通过不同的shim来接入。
存储与快照:Containerd使用快照器来管理容器的根文件系统。当你拉取一个镜像时,它的每一层都会被作为快照存储起来。创建容器时,containerd会基于镜像的顶层快照,创建一个新的、可写的“容器层”快照(通常使用
overlayfs驱动)。这个设计非常高效,因为镜像层是只读共享的,而每个容器的写入操作都发生在自己独立的可写层中。
2.2 与Docker Engine的关系辨析
很多人会混淆Docker和Containerd。你可以这样理解:Docker Engine = Containerd + 一系列增值服务(如构建工具docker build、镜像打包格式docker image、用户友好的CLIdocker、网络和卷管理的高级抽象等)。从Docker 1.11版本开始,Docker Engine的架构就演变为:dockerd(Docker Daemon) ->containerd->runc。dockerd通过gRPC调用containerd,而containerd再去调用runc。因此,当你安装Docker时,其实也安装了Containerd。在Kubernetes场景下,为了追求更简洁和稳定的运行时,社区推荐直接使用Containerd,绕过Docker Engine这一层。
3. 从零开始:Containerd的安装与基础配置
理论清晰后,我们动手实践。这里以最常见的Linux发行版(如Ubuntu 20.04/22.04或CentOS 7/8)为例,介绍两种主流的安装方式。
3.1 安装方式选型:包管理器与二进制部署
对于生产环境,我强烈建议使用操作系统厂商或容器项目官方提供的包管理器(如apt或yum)进行安装。这能确保containerd与系统更好地集成,方便接收安全更新和系统维护。
通过APT安装(Debian/Ubuntu):
# 1. 安装必要的依赖 sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg lsb-release # 2. 添加Docker官方GPG密钥(Containerd的包也在Docker仓库中) sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 3. 设置稳定版仓库 echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 4. 更新包索引并安装containerd.io sudo apt-get update sudo apt-get install -y containerd.io通过YUM安装(RHEL/CentOS/Rocky Linux):
# 1. 安装yum-utils工具集 sudo yum install -y yum-utils # 2. 添加Docker官方仓库 sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # 3. 安装containerd.io sudo yum install -y containerd.io安装完成后,containerd的systemd服务单元会自动创建,但默认的配置文件可能不存在,我们需要生成一个。
3.2 生成与解读默认配置文件
Containerd的主配置文件默认位于/etc/containerd/config.toml。如果该文件不存在,我们可以使用containerd命令生成一个默认配置。
# 停止containerd服务(如果正在运行) sudo systemctl stop containerd # 备份可能存在的旧配置(如果有) sudo mv /etc/containerd/config.toml /etc/containerd/config.toml.bak 2>/dev/null || true # 生成默认配置 sudo containerd config default | sudo tee /etc/containerd/config.toml现在,让我们打开这个配置文件,看看几个最关键的配置节:
# /etc/containerd/config.toml 关键部分解读 version = 2 # 根目录,存放containerd的持久化数据 root = "/var/lib/containerd" # 状态目录,存放运行时状态信息 state = "/run/containerd" # grpc配置,定义API服务的监听地址 [grpc] address = "/run/containerd/containerd.sock" # 注意:默认是Unix Socket,Kubelet需要通过这个Socket与containerd通信 # 配置使用Systemd作为cgroup驱动,这对与Kubernetes集成至关重要 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] ... [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true # 镜像仓库镜像配置,用于加速或替换默认仓库 [plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://registry-1.docker.io"]注意:对于要接入Kubernetes的节点,将
SystemdCgroup设置为true是必须的,这需要与kubelet的cgroup驱动配置保持一致,否则Kubernetes将无法正确管理容器的资源。
3.3 应用配置并启动服务
生成并修改配置后,需要重启服务使其生效。
# 重新加载systemd配置 sudo systemctl daemon-reload # 启动containerd服务并设置开机自启 sudo systemctl enable --now containerd # 检查服务状态 sudo systemctl status containerd如果状态显示为active (running),恭喜你,containerd已经成功运行。你可以使用自带的ctr工具进行验证:
# 查看containerd版本信息 sudo ctr version4. 掌握核心操作:镜像与容器生命周期管理
虽然ctr命令不如docker命令直观,但它是理解containerd工作模型的绝佳工具。我们通过它来演练核心工作流。
4.1 使用ctr管理镜像
ctr命令默认需要root权限,因为它直接操作/run/containerd/containerd.sock。
拉取镜像:
# 拉取一个nginx镜像 sudo ctr image pull docker.io/library/nginx:alpine这里需要完整的镜像地址。docker.io/library/是官方镜像的命名空间。
列出镜像:
sudo ctr image list你会看到镜像的名称、标签、大小和摘要等信息。
打标签和推送镜像:
# 为镜像打上一个新标签(例如,推送到私有仓库前) sudo ctr image tag docker.io/library/nginx:alpine myregistry.local:5000/nginx:my-tag # 推送镜像到私有仓库(需要提前登录,使用`ctr i pull`的认证方式配置) # sudo ctr image push myregistry.local:5000/nginx:my-tag --user <username>:<password>删除镜像:
sudo ctr image remove docker.io/library/nginx:alpine4.2 使用ctr管理容器与任务
在containerd中,“容器”和“任务”是两个阶段的概念,这比Docker的抽象更底层。
创建容器:
# 创建一个名为`mynginx`的容器,但此时它只是一个静态配置,没有运行进程。 sudo ctr container create docker.io/library/nginx:alpine mynginx这个命令会在/var/lib/containerd/io.containerd.runtime.v2.task/default/下创建容器的运行时目录结构。
启动任务(运行容器):
# 为容器`mynginx`创建一个主任务(即启动容器进程) sudo ctr task start mynginx现在,一个nginx进程就在容器中运行起来了。你可以用ctr task ls查看运行中的任务。
与任务交互:
# 在运行的任务中执行一个命令(类似于 docker exec) sudo ctr task exec --exec-id myexec1 mynginx sh # 暂停任务(发送SIGSTOP) sudo ctr task pause mynginx # 恢复任务 sudo ctr task resume mynginx停止和删除:
# 停止任务(发送SIGTERM,默认等待10秒后发送SIGKILL) sudo ctr task kill mynginx # 或者先暂停再删除任务 sudo ctr task pause mynginx && sudo ctr task rm mynginx # 最后删除容器定义 sudo ctr container rm mynginx实操心得:
ctr命令的参数顺序比较固定,通常是ctr [命名空间] [对象类型] [命令] [标识]。默认的命名空间是default。如果你操作失败,先检查对象(镜像、容器、任务)是否存在正确的命名空间下。对于生产环境,我们很少直接使用ctr,但它是排障的利器,比如当kubelet无法拉取镜像时,你可以用ctr image pull手动测试仓库连通性。
5. 生产环境集成:Containerd与Kubernetes
Containerd在Kubernetes生态中扮演着容器运行时接口(CRI)的实现者角色。kubelet通过一个名为containerd-shim的插件与containerd通信。
5.1 配置Kubelet使用Containerd
当你使用kubeadm初始化集群时,可以通过配置文件指定CRI运行时。
首先,创建一个kubeadm配置文件,例如kubeadm-config.yaml:
apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration nodeRegistration: criSocket: unix:///run/containerd/containerd.sock # 关键配置,指向containerd的socket --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.0然后使用此配置文件初始化集群:
sudo kubeadm init --config=kubeadm-config.yaml对于已有的节点,你需要修改kubelet的配置。编辑/var/lib/kubelet/kubeadm-flags.env文件,确保--container-runtime-endpoint参数指向containerd的socket:
KUBELET_KUBEADM_ARGS="--container-runtime=remote --container-runtime-endpoint=unix:///run/containerd/containerd.sock ..."修改后重启kubelet:
sudo systemctl restart kubelet5.2 关键配置:CRI插件与cgroup驱动
确保containerd的CRI插件已启用且配置正确。前面生成的默认配置已经包含了CRI插件。你需要重点关注的是cgroup驱动。在config.toml中确认:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true同时,kubelet也需要配置相同的cgroup驱动。通常可以通过在kubelet的启动参数中添加--cgroup-driver=systemd,或者由kubeadm自动检测并设置。
5.3 排查Kubernetes与Containerd集成问题
集成后最常见的问题集中在镜像拉取和容器创建上。
查看容器日志:当Pod状态异常时,除了kubectl logs和kubectl describe pod,你可以直接通过containerd查看更底层的日志。首先用crictl(Kubernetes的CRI调试工具)找到容器ID:
# 安装crictl # 列出所有Pod sudo crictl pods # 列出所有容器 sudo crictl ps -a # 查看特定容器的日志 sudo crictl logs <container-id>crictl是比ctr更贴近Kubernetes视角的调试工具。
直接通过containerd查看:每个容器的日志默认存储在/var/log/pods/和/var/log/containers/下,但也可以通过containerd的插件配置重定向。更直接的方式是查看shim的日志,但shim日志默认不持久化。你可以调整containerd的日志级别为debug(临时用于排障),然后通过journalctl查看:
sudo journalctl -u containerd -f6. 高级特性与生产调优
掌握了基础操作和集成后,我们来看看containerd的一些高级特性和生产环境调优点。
6.1 镜像优化:快照与存储驱动
Containerd支持多种快照器,最常用的是overlayfs。你可以通过配置选择不同的存储驱动。对于某些旧内核或特定发行版,可能需要使用devicemapper或aufs,但overlayfs是性能最好、最推荐的选择。
在config.toml中配置:
[plugins."io.containerd.grpc.v1.cri".containerd] snapshotter = "overlayfs" disable_snapshot_annotations = false注意事项:切换快照器是一个危险操作,因为它涉及底层存储结构的变更。通常应在初次安装时确定,后期切换可能需要迁移现有镜像和容器,操作复杂且易丢失数据。
6.2 资源限制与隔离配置
虽然资源限制主要由Kubernetes通过CRI指定,但containerd底层通过runc实现。你可以在config.toml中为runc运行时配置默认的cgroup路径、root路径等。更常见的做法是在Kubernetes的Pod Spec中定义resources.limits和resources.requests,containerd会忠实地通过runc应用这些cgroup限制。
6.3 网络集成模式
Containerd本身不管理网络,容器的网络命名空间由它创建,但网络配置(如IP分配、网卡创建)由外部的CNI(容器网络接口)插件负责。当containerd通过CRI创建容器时,它会调用配置好的CNI插件来配置网络。因此,网络问题的排查通常需要结合CNI插件(如Calico、Flannel)的日志和状态。
6.4 日志管理策略
默认情况下,容器日志通过containerd-shim被捕获并写入到stdout/stderr,然后由kubelet收集。你可以配置containerd使用不同的日志驱动,例如json-file(默认)或journald。在生产环境中,为了集中管理日志,通常会部署如Fluentd、Filebeat等边车容器或DaemonSet,从/var/log/containers/目录收集日志并发送到Elasticsearch等后端。
在config.toml中可以调整CRI插件的日志配置:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] ... [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] ... # 限制单个容器日志文件大小和数量,防止磁盘被撑爆 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] ... # 这些是runc的选项,但日志限制通常由kubelet或上层配置更关键的日志限制是在Kubernetes层面,通过kubelet参数--container-log-max-size和--container-log-max-files来控制。
7. 故障诊断与日常运维命令手册
即使配置无误,在生产环境中也会遇到各种问题。这里整理一份实用的诊断清单和命令。
7.1 常见问题与排查路径
问题一:Pod状态一直为ContainerCreating或Pending。
- 排查思路:
- 检查节点资源:
kubectl describe node <node-name>,看是否资源不足。 - 检查镜像拉取:
kubectl describe pod <pod-name>,查看Events部分。如果提示镜像拉取失败,到对应节点上手动用ctr image pull测试。 - 检查容器运行时:在节点上执行
sudo crictl ps -a,看容器是否被创建。执行sudo systemctl status containerd和sudo journalctl -u containerd -n 50 --no-pager查看containerd服务状态和近期日志。 - 检查CNI网络:如果Events中有网络相关错误,检查CNI插件状态,如
sudo journalctl -u kubelet | grep -i cni。
- 检查节点资源:
问题二:容器运行后立即退出(CrashLoopBackOff)。
- 排查思路:
- 查看应用日志:
kubectl logs <pod-name> --previous(查看前一个容器的日志)。 - 检查容器启动命令和参数:
kubectl describe pod <pod-name>,确认command和args是否正确。 - 检查容器内进程:如果可能,在Pod Spec中为容器添加一个
sleep infinity的sidecar或使用kubectl debug进入容器排查。 - 检查运行时配置:确认containerd的
runc运行时配置无误,特别是当使用非默认运行时(如Kata)时。
- 查看应用日志:
问题三:磁盘空间不足。
- 排查思路:
- 清理未使用的镜像:
sudo ctr images ls查看,使用sudo ctr images rm <ref>删除。 - 清理containerd内部数据:containerd提供了
ctr content和ctr snapshot命令来管理内容和快照,但清理需谨慎。更安全的方式是使用crictl:sudo crictl rmi --prune。 - 调整日志轮转策略:如前所述,确保kubelet的容器日志大小限制已配置。
- 清理未使用的镜像:
7.2 实用运维命令速查表
| 场景 | 命令 | 说明 |
|---|---|---|
| 服务管理 | sudo systemctl status/restart/stop containerd | 管理containerd守护进程 |
| 查看版本 | sudo ctr version | 查看containerd和runc版本 |
| 镜像操作 | sudo ctr image pull/push/list/tag/rm | 拉取、推送、列表、打标签、删除镜像 |
| 容器操作 | sudo ctr container create/ls/info/rm | 创建、列表、查看信息、删除容器 |
| 任务操作 | sudo ctr task start/ls/exec/pause/resume/kill/rm | 启动、列表、执行命令、暂停、恢复、停止、删除任务 |
| 命名空间 | sudo ctr ns ls | 列出所有命名空间(默认是default,k8s用的是k8s.io) |
| K8s视角调试 | sudo crictl ps/pods/info/stats/logs | 通过CRI接口查看容器、Pod、信息、状态、日志 |
| 内容与快照 | sudo ctr content lssudo ctr snapshot ls | 查看拉取的镜像层内容、查看快照(谨慎操作) |
| 日志查看 | sudo journalctl -u containerd -f -n 100 | 实时查看containerd服务日志 |
7.3 性能监控与健康检查
Containerd暴露了metrics接口,可以与Prometheus集成进行监控。默认情况下,metrics端点未开启。你可以在config.toml中启用:
[metrics] address = "0.0.0.0:1338" # 设置一个监听地址和端口 grpc_histogram = false启用后,你可以通过http://<node-ip>:1338/metrics获取监控指标,如容器创建耗时、镜像拉取计数、运行时操作错误等,这对于构建生产监控大盘至关重要。
最后,关于containerd的升级,我个人的经验是:在测试环境充分验证,关注版本变更日志中关于CRI插件和配置结构的改动。升级时,先滚动升级工作节点,确保业务Pod能平滑迁移,最后再处理控制平面节点。每次变更配置后,养成用sudo containerd config validate(如果版本支持)或至少用sudo systemctl restart containerd后观察日志的好习惯,将问题扼杀在启动阶段。
