news 2026/10/21 5:25:07

深入理解Containerd:容器运行时架构、实操与Kubernetes实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解Containerd:容器运行时架构、实操与Kubernetes实践

1. 为什么Containerd值得单独拎出来讲

很多刚接触云原生的人,一上来就被Kubernetes、Docker、Containerd、CRI-O这一堆名词绕晕。我见过不少项目里的同事,明明宿主机上装着Docker,结果发现Kubernetes集群用的运行时根本不是它,而是底下的一层Containerd。这时候要是概念不清,排查问题会非常痛苦。

先说结论:Containerd是一个工业级的容器运行时,它负责容器的生命周期管理、镜像传输、存储挂载等核心能力。你可以把它理解为"专门做容器脏活累活"的底层引擎。Docker早期确实是直接干这些活的,但后来Docker把运行时的职责拆分了出去,Containerd就成了那个被剥离出来的核心。2017年,CNCF接纳Containerd成为孵化项目,之后它逐渐成为Kubernetes最主流的CRI运行时之一。

为什么现在非得搞懂Containerd?最直接的原因是:Kubernetes从1.24版本开始就彻底移除了对Docker Shim的直接支持,虽然你还能用Docker构建镜像,但集群真正运行容器的引擎几乎都是Containerd或CRI-O。而Containerd在主流云厂商的托管集群里占比极高,很多生产环境默认就是它。再加上很多运维排查链路,比如查看日志、进入容器、调试镜像,命令都和Docker时代不太一样,如果只会docker exec、docker ps,切到Containerd后会感觉寸步难行。

这篇文章不打算堆砌无聊的概念定义,我尽量按照实际使用的维度来讲:它和Docker的关系、内部是怎么设计的、平时怎么操作、接入Kubernetes时有哪些必须注意的细节、以及我在生产环境里踩过的那些坑。整个过程尽量像拉家常一样,让你读完就能上手干活。

1.1 Containerd、Docker、CRI-O三者的边界

先解决一个最基本的困惑:这三个东西到底谁是谁的替代品?

  • Docker:面向开发者的完整容器平台。它包含命令行工具、镜像构建、Compose编排、网络、卷管理,以及底层的容器执行能力。对用户来说体验很好,但组件多、耦合重,对Kubernetes这种编排平台来说显得有点臃肿。
  • Containerd:一个精简的容器运行时。它不提供开发友好的CLI(不过有ctr和nerdctl可以用),不关心你怎么构建镜像,它只管把镜像拉到本地、把容器跑起来、维护容器生命周期。Kubernetes需要的核心能力它都有,而且性能更好、占用更低。
  • CRI-O:专门为Kubernetes CRI接口实现的另一个轻量运行时,直接从OCI镜像规范出发,跳过Docker那一套。它的定位和Containerd类似,但目前生产环境占有率远不及Containerd。

用一个不太严谨但好记的类比:Docker是一台"傻瓜相机",什么都是集成好的,按快门就能用;Containerd是"专业机身",你必须自己配镜头、设置参数,但上限更高;CRI-O算是另一个品牌的专业机身,功能差不多,但用的人少些。在Kubernetes场景里,你不需要Docker那些花哨功能,只需要一个稳定、高效、可编程的引擎,Containerd就是最优解。

1.2 Containerd的组件全景和它解决的核心问题

Containerd从代码层面来看由几个核心组件构成:

  • containerd守护进程:运行在宿主机上,通过gRPC对外提供服务。所有对容器和镜像的请求最终都打到这个守护进程。
  • containerd-shim:这是它的精髓之一。每个容器都有一个独立的shim进程,它负责把容器的标准输入输出、退出状态、信号转发等从Containerd主进程中隔离开。好处是:即便Containerd守护进程重启,容器也不会挂掉。这一点在生产环境至关重要——升级Containerd时不至于把上面的业务全部打断。
  • snapshotter:管理容器镜像在宿主机上的存储层,类似Docker的storage driver。它的作用是把镜像的各个层挂载成可写的容器视图。常见的快照器有overlayfs、btrfs、native等。
  • content store:负责存储镜像本身的blob数据,也就是那些解压前的层文件和索引。
  • metadata db:基于BoltDB,保存所有容器、镜像、快照的元数据。

我刚接触Containerd时,头一件事就是理解它"没有什么是主动推送的"。所有操作都是被动的:Kubelet发送CreateContainer请求,Containerd去拉镜像、创建快照、启动容器进程。它不关心网络CNI插件是谁,也不关心镜像怎么构建,它是一个非常纯粹的执行者。

2. Containerd核心架构拆解:shim、snapshotter和那套gRPC接口

很多文章喜欢把架构画成一张方框图,但光看图其实很难理解为什么这么设计。这一节我会一个个组件拆开讲,重点说明它们解决了哪些实际问题,顺便指出你在排查问题时会如何用到这些认知。

2.1 containerd-shim为什么是"救命稻草"

先讲shim,因为它是理解Containerd容错能力的钥匙。我们用Docker时,如果dockerd守护进程挂了,容器会怎样?在Docker老版本里,容器会跟着遭殃。但Containerd的设计从源头规避了这个问题。

它的工作模式是:containerd主进程收到请求后,为每个容器启动一个独立的shim进程,shim再通过运行时(默认是runC)启动真正的容器进程。这样一来,容器进程的父进程是shim,而不是containerd。就算containerd重启或故障,shim和容器的关系始终稳定。容器里的进程照常运行,外部连接也不会中断。

这个机制在我一次实际运维中帮了大忙。当时集群里某个节点的containerd需要升级,配合上滚动操作,节点上跑了十几个关键服务。我预想中最坏的情况是进程断掉需要重启容器,结果升级完毕后容器全部健在,只有kubelet重新连上containerd的socket完成状态同步。那块悬着的石头才落地。

我们排查问题时也经常接触shim。比如说某个容器一直删不掉,或者状态异常,但containerd的API看起来正常。这时候大概率是shim出了问题,直接去查shim的日志、看进程是否存在,往往能拿到比containerd主日志更详细的线索。像pstree一看,就能看到containerd -> containerd-shim -> 容器进程的三层关系,非常清晰。

2.2 snapshotter存在的原因,以及overlayfs为什么是默认选择

镜像是一层一层叠加的,容器运行时要能在这些只读层之上创建一个可写层,并让进程看到合并后的完整视图。这个合并和挂载的工作,就是snapshotter做的。

Containerd里的snapshotter默认是overlayfs(在Linux上)。overlayfs把镜像层和可写层拼成一个联合视图,写操作只在最上层做,读操作依次往下层查找。这样既节省磁盘,又让镜像共享变得极其高效。多个容器基于同一个镜像启动时,镜像层可以共用,每个容器只占一份可写层的空间。

如果你还要自己维护镜像仓库或磁盘占用,得学会看/var/lib/containerd/下的目录结构。其中有一个snapshots目录,下面会按照snapshotter的格式存各层的挂载信息。磁盘沾满时,先不要瞎删,用du -sh /var/lib/containerd/*找大块头,一般重点就是snapshots目录。维护过几百个节点的集群后你会发现,掌握这部分存储模型几乎是排查空间问题的基础功。

2.3 gRPC API和事件机制:Containerd不是命令行玩具

很多人以为有命令就行,实际上Containerd是一套可编程的平台。它对外暴露gRPC接口,任何语言都能通过客户端库去调用。Kubelet就是通过CRI插件包装后的gRPC接口与它交互的。

事件机制也值得注意。Containerd会主动推送容器生命周期变化事件,比如创建、启动、退出、删除。如果你在做一个自定义的运维平台或审计系统,可以订阅这些事件流来感知集群状态,而不是靠轮询。这一点比Docker时代省心得多,也给了我们很大的自定义空间。

我做过一个小工具,就是监听containerd的事件流,把容器重启信息实时同步到告警系统。这在排障和统计容器稳定性的场景里非常好用。它既能统计崩溃次数,也能辅助判断某个节点是否出现异常重启潮。如果只靠Kubernetes的event来查,很多节点层的信息其实看不到。

3. 上手实操:ctr、nerdctl与配置文件的日常用法

理论讲完要落地。这一部分我按实际工作的路径来写:装好Containerd之后,你第一个面对的往往不是Kubernetes,而是如何手动操作这个引擎。

3.1 用ctr完成最基础的镜像和容器操作

ctr是Containerd自带的CLI,功能基础但五脏俱全。默认它需要通过socket访问containerd,通常是非root用户连不上。常用命令:

# 查看命名空间(Kubernetes一般用k8s.io) ctr namespace ls # 拉镜像 ctr image pull docker.io/library/nginx:1.27-alpine # 查看镜像列表 ctr image ls # 生成容器并运行(run会直接创建并启动) ctr run --rm docker.io/library/nginx:1.27-alpine nginx-test # 查看容器列表 ctr container ls ctr task ls

你要留意一个差异:Docker的镜像名写起来只需要nginx:1.27,而ctr里最好带上完整地址,否则它会默认到docker.io。--rm参数用来运行完立即清理。ctr里"容器"和"任务"是两回事:容器是元数据对象,任务是真正的运行中进程。排查问题时,如果ctr container ls看到了,但ctr task ls里没有,说明容器处于已创建未启动或已停止状态。这个思维模型很重要,避免对着列表空转。

不过ctr用起来比较费劲,尤其当你习惯了docker run -d -p 8080:80这种人性化参数后。所以社区推出了nerdctl,它是一个类Docker的CLI,底层调用containerd。强烈建议装一个。

3.2 nerdctl:为什么说它是Containerd世界的Docker

nerdctl是一个能让你无缝迁移命令行习惯的工具。它支持docker run、docker compose、docker build等绝大部分指令,差别只是操作对象换成了containerd。遇到紧急问题,你完全可以用nerdctl来替代日常的容器管理。

# 类似docker run nerdctl run -d --name web -p 8080:80 nginx:1.27-alpine # 类似docker ps nerdctl ps # 类似docker logs nerdctl logs -f web # 类似docker exec nerdctl exec -it web sh # 进入命名空间 nerdctl --namespace k8s.io ps

有一点必须注意:Kubernetes创建的容器都在k8s.io命名空间,而你手动用nerdctl不带--namespace参数操作时,默认是default命名空间。如果你Kubernetes里容器出了问题,想手动查看和操作,一定记得加--namespace k8s.io,不然看到的列表是空的,很容易误判成"Containerd里没容器"。

3.3 配置文件里那些关键参数

Containerd的配置默认在/etc/containerd/config.toml。我们不需要改动所有内容,但至少要知道几个关键项:

version = 2 # 存储容器根目录和数据目录 state = "/run/containerd" root = "/var/lib/containerd" [grpc] address = "/run/containerd/containerd.sock" [plugins] [plugins."io.containerd.grpc.v1.cri"] sandbox_image = "registry.k8s.io/pause:3.9" [plugins."io.containerd.grpc.v1.cri".containerd] default_runtime_name = "runc"

生产环境最常见的一个改动是配置私有镜像仓库。一般会加上[plugins."io.containerd.grpc.v1.cri".registry]段,设置config_path指向一个目录,然后在目录下为每个仓库建一个hosts.toml,用来配置认证和镜像地址重写。这个比Docker的daemon.json里的registry-mirrors灵活很多,也有清晰的优先级策略。

另外如果你用GPU机器跑推理任务,配置文件里还要增加nvidia-container-runtime的配置。这里不展开,但记住套路:先安装nvidia-container-toolkit,然后改默认运行时,再重启containerd。改动完记得用containerd config dump > /etc/containerd/config.toml这样的方式备份原始配置,或先手动备份原文件。

4. 对接Kubernetes的CRI细节:从Docker切到Containerd的配置要点

集群层面用Containerd和单机层面又有不同。这一节进入我最常被问到的领域:Kubelet到底怎么和Containerd配合?为什么切换后经常出现"连接不上运行时"的错误?以及沙箱pause镜像是什么鬼?

4.1 CRI插件的角色与sandbox镜像的重要性

Kubernetes需要每个Pod有一个"沙箱容器",它就是pause容器。pause承担着初始化网络命名空间和IPC命名空间的职责,业务容器启动时会加入pause所在的命名空间。这样Pod里的多个容器共享网络栈,也保证了nginx和业务代码能通过localhost通信。

Containerd的CRI插件启动时要求能拉取pause镜像,默认指向registry.k8s.io/pause:3.9。如果你所在环境拉不到这个镜像,节点状态可能直接变成NotReady,而且你查看kubelet日志时会看到一堆关于sandbox image拉取失败的记录。这大概是切Containerd后最容易栽跟头的地方。

解决方案很简单:在配置文件的sandbox_image里改成一个内网可达的镜像地址。特别是国内环境或者离线环境,直接把默认地址换掉能避免很多麻烦。很多云厂商交付的集群镜像也包含pause,但宿主机的containerd并不会自动去读那些镜像,需要你明确指定。

4.2 Kubelet如何发现Containerd

Kubelet启动时通过--container-runtime-endpoint参数指定运行时地址,对应Containerd就是:

--container-runtime-endpoint=unix:///run/containerd/containerd.sock

注意不要漏掉unix://前缀。如果你发现kubelet一直报failed to connect to containerd,先检查这个参数对不对,再看socket文件是否存在。可以用crictl info快速验证运行时连接是否正常。

crictl是Kubernetes社区提供的CRI调试工具,生产环境排查容器问题比ctr更顺手,因为它完全按照CRI的语义来展示信息:

crictl ps crictl images crictl logs <container-id> crictl exec -it <container-id> sh crictl inspect <container-id>

优先用crictl,因为它能识别Kubernetes创建的容器,无需关心命名空间。ctr往往作为最后手段,用于检查底层镜像存储或重置容器状态。

4.3 切换时的镜像和存储迁移思路

如果要把集群从Docker运行时平滑切换到Containerd,最麻烦的是镜像怎么搬。Docker镜像在/var/lib/docker,Containerd在/var/lib/containerd里,直接拷贝是没用的。稳妥的方案是:

  1. 先部署一套新的Containerd节点,不拆老节点。
  2. 让镜像仓库作为中转,老节点按需导出镜像到仓库,新节点从仓库拉取(可以写个脚本批量导入导出)。
  3. 或者利用nerdctl的--load/save能力,把镜像打成tar包搬过去,但注意带平台参数和OCI格式。

不要试图硬拷目录数据,因为它们的存储格式和元数据组织完全不同,硬拷大概率导致无法识别甚至损坏索引。我们团队生态里最早踩这个坑的兄弟,最后不得不重建节点。从那以后,我一律推荐"用镜像仓库做中转"或者"优先在基建侧更新节点镜像"。

5. 实战中反复踩到的坑和我的应对习惯

技术部分说完,这一节专门用来吐槽和总结。Containerd给生产环境带来的稳定性的确好,但有些坑几乎人人都会碰到。我把它们列出来,希望能帮你省下一夜的排查时间。

5.1 "镜像删不掉"和磁盘空间诡异占用

Containerd有内容存储层和快照层。有时候你用ctr images rm删了一个镜像,但df -h一看磁盘没释放多少。原因在于:镜像的层blob还在content store里,并没有被彻底回收,或者还有容器在引用该镜像。和Docker不一样,Containerd的垃圾回收需要等引用计数归零,别指望删一个镜像立刻释放全部空间。

平平无奇的一天,我们一个节点的磁盘报警,但看不出哪个日志在增长。我用了很久才排查出来,是contains所有历史版本的镜像层还躺在磁盘上,删除pod后没有触发自动清理。后来我在运维流程里加了一步:用ctr content ls查看blob列表,再通过ctr content gc --containerd做回收。如果需要全量清理,可以用crictl rmi --prune配合使用。这里要注意,手动gc有风险,如果集群正在运行,可能会影响正在使用的镜像层。所以我会先确认节点负载和安全窗口再做操作。

5.2 容器日志缺失背后的--log-opt问题

Kubernetes里容器的标准输出默认由containerd写入到/var/log/pods目录,kubelet负责做日志轮转和收集。有时你会遇到一个问题:某个容器明明有日志,但kubectl logs只能看到很少的输出,或者完全为空。排查发现,原因是镜像内的进程没有把日志写到标准输出,而是写入了容器内的文件。这在Docker时代也一样,但因为DaemonSet侧收集器(比如某个根目录采集组件)依赖标准输出,导致日志链路断裂。

解决这个问题没有银弹。我们一般从镜像侧规范化日志输出,强制要求业务进程把访问日志打到stdout/stderr。实在不行就用sidecar模式,在主容器旁再放一个日志转发容器读取文件并输出到stdout。这个思路无论用什么容器运行时都适用,Containerd并不会帮你做更多额外处理。

5.3 升级containerd时保活并没有想象中那么省心

前面夸了shim的设计,但升级流程其实依然有讲究。官方推荐逐个节点升级,并配合kubelet的节点维护。单纯在机器上替换二进制和重启containerd,虽然容器进程可能不受影响,但kubelet与containerd的gRPC连接会中断,可能出现状态同步延迟和误报。

我的做法是:先把节点置为不可调度并驱逐pod,让业务容器全部迁移走,然后再升级containerd。即使shim能保活,从集群管理的可控性和业务稳定性来看,迁移后升级一定是最稳的。有同事图省事直接原地升级,结果某个pod因为和containerd关联的半死状态卡了很久,人工干预花费更多时间,得不偿失。

5.4 镜像仓库认证多仓库并存时最常见的误用

在containerd里配置多个私有仓库认证,最容易犯的错是把所有认证都写在了一个hosts.toml里。官方设计是每个仓库单独一个目录对应文件,例如:

/etc/containerd/certs.d/myregistry.example.com/hosts.toml

里面写server地址、用户名密码、ca证书路径。如果多个仓库混在一个toml里,pull时大概率出现凭证错乱。养成按仓库名拆开配置的习惯,后期加仓库或换凭证也方便。

此外要注意hosts.toml里有两个容易混淆的字段:host和capabilities。capabilities是拉取或推送到该仓库时才会解析的,不是每个路径都默认启用。如果配了认证还是401,先检查这个字段。这类问题经常因为别人给的参考文档不够精确而折腾半天,其实关键就在capabilities上。

6. 一点补充:想进阶该看什么,以及学习时的实验环境搭建

如果看到这里觉得基本概念通了,下一步我建议你自己动手搭一个小实验环境,把每个操作都过一遍。别只在生产环境上看在线文档,很多细节不实际操作记不住。

6.1 单机实验环境的快速搭建

最省事的路径是用一台2C4G的Linux虚机,装好Docker其实也行(因为Docker内置了Containerd),但为了方便实操,我倾向于直接装原生的Containerd套件。以常见的Debian系为例:

apt-get update && apt-get install -y containerd systemctl enable --now containerd

装完后不用急着配Kubernetes,先用ctr玩一玩,再用nerdctl跑一个nginx。接着手动改一下配置里的sandbox_image,引入自建镜像仓库,模拟一次完整的离线镜像拉取。这一套走下来,比看10篇文档都有用。

6.2 建议自己研究的几个方向

如果还想深入,我会建议分三条线:

  1. 存储驱动:尝试把overlayfs换成btrfs或native,对比一下性能差异和适合场景,搞清楚默认配置为什么是overlayfs。
  2. 运行时扩展:研究一下runC之外的运行时,比如在安全敏感场景下能提供更强隔离的方案。Grpc入口在哪里、如何注册一个自定义运行时,这对做安全加固很有用。
  3. CRI和Kubelet交互:在节点上抓取containerd的gRPC请求,你会看到Kubelet调用CRI接口的完整生命周期。这能让你对Pod启动过程有精确理解。

6.3 最后几条经验补充

  • 生产环境改配置前,永远先备份:cp /etc/containerd/config.toml /etc/containerd/config.toml.bak
  • 遇到问题先看journalctl -u containerd -f,很多根因在守护进程日志里已经写得很明确,别上来就重启。
  • 不要随意删/var/lib/containerd,除非你确定节点上没有任何需要的容器。
  • 把crictl、nerdctl这两个命令装进所有节点的运维工具集,它们比ctr的友好度高太多。

我自己的体会是,Containerd的学习曲线不算陡,但和Docker的使用习惯差异确实存在。好在只要你肯花半天时间亲自操作一遍,建立起"命名空间、镜像引用、快照、任务"这几个概念,后续再处理Kubernetes节点问题都会顺畅很多。如果你正在规划把集群从Docker迁移到Containerd,或者刚接手一套新集群并且总遇到奇怪的问题,希望这篇内容能帮你把底层的框架撑起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 5:24:55

A2A协议实战:从最小示例到多Agent协作避坑指南

上篇我们把 A2A 协议的基本定位、AgentCard 的作用、Task 状态机的雏形都过了一遍。今天这篇换一个角度&#xff0c;不再停留在“A2A 是什么、为什么需要它”&#xff0c;而是直接聊“怎么把它跑起来”。我会用一个最小可运行的示例把整个通信链路打通&#xff0c;再拆解任务状…

作者头像 李华
网站建设 2026/10/21 5:24:54

SSM+Vue社团管理系统实战:验证码防刷与部署全流程解析

做这类大学生社团管理系统的人真的太多了&#xff0c;每年课程设计、毕业设计里都能见到。很多人交上来的东西功能列表看着挺全&#xff0c;但一问细节就露馅&#xff0c;尤其是登录页那个验证码——大多数人是直接从别的项目复制过来的&#xff0c;问它怎么生成、怎么校验、为…

作者头像 李华
网站建设 2026/10/21 5:24:42

知识工作插件化架构设计与实现:从转换到编排的完整指南

1. 从“知识工作插件”这个命名说起第一次看到 knowledge-work-plugins 这个项目名&#xff0c;我的直觉是&#xff1a;这大概率不是一个单一工具&#xff0c;而是一套面向知识工作者的能力扩展集合。所谓“知识工作”&#xff0c;说白了就是那些以信息处理为核心的活儿——写文…

作者头像 李华
网站建设 2026/10/21 5:24:13

knowledge-work-plugins:插件化架构如何解决知识工作工具链碎片化

1. 这个插件体系到底在解决什么问题第一次看到 knowledge-work-plugins 这个命名&#xff0c;我脑子里蹦出来的不是某个具体功能&#xff0c;而是一类很典型的工程困境&#xff1a;知识型工作者的工具链太碎了。写文档的、管任务的、查资料的、做归档的、跑分析的&#xff0c;每…

作者头像 李华
网站建设 2026/10/21 5:23:24

C# Access学生管理系统毕业设计源码(WinForms三层架构)

简介&#xff1a;这是一套面向计算机专业本科生的C#毕业设计实战源码&#xff0c;聚焦学生学籍管理这一典型校园信息化场景&#xff0c;帮助初学者掌握Windows桌面应用开发、Access数据库集成与基础软件工程实践。资源包含108个文件&#xff0c;涵盖28个核心C#业务逻辑文件&…

作者头像 李华