☰
K8s容器运行时之争:从Docker到containerd的迁移实践
2026/10/16 7:21:49 网站建设 项目流程

这几年如果你维护过K8s集群,大概率被同事抓着问过这几个问题:Docker不是被K8s踢出去了吗,为什么我在服务器上还能用docker命令看到容器?容器运行时到底该选containerd还是CRI-O?明明在本地docker run好好的,一上K8s就各种报错?

这个“容器运行时之争”,从K8s宣布弃用dockershim那天起,就一直是社区里讨论度最高的话题之一。干了好多年K8s的团队,也经常在选型、迁移、排障时被折腾得够呛。今天就把这个话题作为k8s系列的第一篇,掰开揉碎讲清楚:这些运行时到底在争什么、为什么Docker会被“替换”、实际生产环境该怎么选、以及如何从Docker平滑切到containerd。

这篇东西,适合刚入门的K8s新手,也适合已经摸过一段时间集群、却始终对运行时细节一知半解的运维和开发。看完之后,你不仅能看懂各种“运行时之争”的文章,还能自己动手把一套集群安全迁到containerd上。

1. 容器运行时到底在争什么

1.1 先搞清楚“运行时”是什么

很多人把“容器运行时”等同于Docker,这个理解其实差得很远。

容器运行时是真正负责拉起容器、管理容器生命周期的那一层程序。它要做的事情大致包括:解压镜像、组装rootfs、创建命名空间、配置Cgroup、启动进程、最后再把运行状态反馈给上层调用者。

打个比方。镜像可以理解成一张已经刻好的光盘,里面是完整的软件环境和启动命令。运行时就是那个光盘播放器,它把光盘里的内容读出来,在一个独立小房间里放着给你看。这个“小房间”,就是操作系统层面的隔离空间,隔离能力来自Linux内核的namespaces和cgroups。

那Docker在里面扮演什么角色?Docker其实是一个“全家桶”。它提供了docker CLI、docker daemon、镜像构建、网络、卷管理、镜像仓库上传下载等一系列能力。真正负责把容器拉起来的那一步,早期是Docker里内嵌的libcontainer,后来被拆分成了独立的runC项目,也就是现在大部分运行时底层都在用的那个“容器启动器”。

runC本身只是一个很小的命令行工具,它按照OCI规范读取一个bundle目录,然后直接创建并启动容器进程。但它只能启动单个容器,没有网络、没有镜像管理、没有远程接口。于是在runC之上,出现了containerd、CRI-O这类完整的容器运行时服务,它们管理镜像、管理容器状态、暴露API接口,形成一个可以被上层系统调用的“运行时平台”。

所以,当你把“Docker”和“containerd”放在一起比较时,要意识到两者根本不在同一个层面对话。Docker像是一个完整的自动化生产车间,containerd更像是车间里那条最核心的生产线。K8s要对接的不是整个车间,而是那条生产线。

1.2 K8s介入:CRI就是把运行时变成插件

K8s的节点组件Kubelet,需要为Pod创建容器、删除容器、查看状态、拉取镜像。在早期,Kubelet是直接调用Docker暴露出来的API。因为那时候Docker是唯一的容器方案,K8s也只能先绑在Docker身上。

但它一直有这个顾虑:跟着Docker走没问题,但如果哪天Docker不再是主流了,或者Docker发布了不兼容的版本,整个K8s都会被卡住。所以从K8s 1.5开始,社区就在酝酿一套标准接口,把“运行时”从Kubelet里解耦出来,于是就有了CRI,全称Container Runtime Interface。

CRI是一套基于gRPC的协议,定义了镜像管理和容器管理两组接口,比如拉镜像、创建容器、启动容器、停止容器、获取日志等等,全都规范好了。任何程序只要实现了这套接口,就能被Kubelet使用。换句话说,Kubelet不再关心你底层是runC还是别的什么,它只认CRI接口。

在这个设计里,谁实现了CRI接口,谁就是K8s认可的容器运行时。后来containerd专门做了一个CRI插件,把自己包装成CRI服务;CRI-O则从一开始就盯着CRI去实现;而Docker并不原生支持CRI,于是K8s社区在Kubelet内部内置了一个叫dockershim的“垫片”,把CRI请求翻译成Docker API请求,再去调用Docker。就这样,Docker才勉强能被K8s继续使用。

这套机制解释了一个关键点:所谓“容器运行时之争”,争的其实不是谁更能打,而是“在CRI这个接口标准下,谁来扮演向Kubelet提供服务的那个人”。

1.3 争论的本质:兼容、锁定与资源开销

明白了分层关系,回头再去看网上各种争论,就清晰多了。整个“之争”的核心,其实集中在几个点上。

第一是兼容性。过去所有K8s用户构建镜像默认都用docker build,如果换一个运行时,现有镜像还能不能跑?其实是可以的,因为镜像格式早已被OCI标准化了,只要打出来的是OCI镜像,任何遵循OCI的运行时都能运行。这个问题的本质,是用户担心工作流要重写,担心本地开发和生产环境行为不一致。

第二是供应商锁定。K8s社区不想让整个生态依赖某一家公司的实现,所以它要定义CRI,把运行时做成一个可插拔的组件。Docker越强大、越全家桶,K8s就越难把它当作一个纯粹的“运行时”来对待,因为Docker附加了太多K8s不需要的东西。

第三是资源与稳定性。Kubelet调用Docker要先经过dockershim转一层,再经过Docker daemon的API层,Docker内部再调containerd,最终才到runC。链路很长,每多一跳,就多一份故障概率。Docker daemon一旦出问题,这个节点上所有容器都跟着遭殃。这对大规模集群来说是不能接受的。

理解了这些底层诉求,再看后文的历史变迁,就顺理成章了。

2. dockershim移除:一个被误解最深的变更

2.1 v1.24到底做了什么

K8s在1.20版本宣布弃用dockershim,然后在1.24版本正式把Kubelet里的dockershim代码移除掉。这个事件被很多媒体简化成了“K8s不用Docker了”,然后一传十十传百,最后变成了“K8s不支持docker镜像了”。

这里必须把事实说清楚。

dockershim是Kubelet内置的一段代码,作用是翻译CRI请求给Docker API。移除掉它,意味着Kubelet不再帮你做这个翻译了。如果你还想让K8s走Docker的链路,就得自己在外围再起一个翻译服务,把CRI请求转换成Docker调用,这可能是一个叫cri-dockerd的独立组件。但注意,Docker本身依然能正常工作,依然能构建镜像、运行容器,只是K8s不主动依赖它了。

在Docker这条旧链路里,kubelet先找dockershim,dockershim找Docker daemon,Docker daemon再找它内嵌的containerd,containerd最后找runC。也就是说,即便是在旧架构里,真正干活的底层其实也是containerd和runC,Docker只是包在最外层的那套管理壳。

移除dockershim之后,Kubelet直接通过CRI协议访问containerd插件,containerd的无事发生之间完成老链路的工作。让我们捋直了看,是不是少了好几层。这个架构调整,本质上是把Docker从K8s的链路里拆了出去,但容器镜像、容器格式、OCI规范这些都没变。

2.2 对普通用户的实际影响

很多人最关心的问题是:我用docker build打的镜像,还能不能推到K8s里用?

答案是可以。镜像构建用的是docker build,产出的是OCI标准格式,包含分层、元数据、manifest。只要镜像能推到一个K8s能访问到的镜像仓库,containerd或CRI-O就能原样拉下来运行。日常工作流里最常见的那种“本地docker run能跑、推到K8s不能跑”的报错,问题多半出在环境变量、挂载、端口、资源限制等手段,而不是运行时换了。

真正受影响的,是集群侧的命令和配置。你以前用docker ps查看节点上的容器,切换后看不到东西了,因为docker daemon管理的容器和containerd管理的容器不在同一个数据目录,更不在同一个状态存储里。以后要改看crictl ps。

配置层面的变化更直接。之前Kubelet命令行参数里可能写了--container-runtime=docker,或者通过kubeadm初始化时生成的配置里指定了/var/run/dockershim.sock。新版本K8s要用containerd,就得把Kubelet的启动参数改成:

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

这里有一个很多人忽略的坑:镜像不互通。Docker daemon把镜像存在/var/lib/docker,containerd把镜像存在/var/lib/containerd。切换运行时后,Kubelet去containerd里找不到本地的缓存镜像,只能重新拉取。这种细节在迁移时最容易让人措手不及,后面实操章节再细说。

2.3 为什么说“移除”反而是好事

很多老运维对Docker有感情,觉得Docker好用、成熟、社区大。但站在K8s集群管理的角度,dockershim被移除真不是坏事。

最直观的好处是链路短了。K8s不经过Docker daemon,意味着一个节点上少了一个常年驻扎的、容易出幺蛾子的守护进程。可能有人没意识到,Docker daemon本身是进程管理能力很强的程序,但一旦它自己的状态目录损坏、日志文件被误删、磁盘写满,整个节点上的容器都会受到牵连。我知道的就有团队碰到过,因为节点磁盘满,docker daemon卡在某个I/O上,导致Kubelet心跳超时,整个节点直接NotReady。那种问题排查起来非常痛苦。

其次是资源占用更小。Docker daemon要维护一系列镜像、网络、卷、日志的功能,这都需要内存和CPU。换成containerd之后,节点上少了一个重服务,大规模集群攒下来节省的资源挺可观。

再往后看,是标准化带来的可选择性。CRI接口立住了,Kata、gVisor这类安全容器才能顺利接入K8s生态。如果没有这层抽象,安全容器想接入K8s,成本要高得多。

所以我对dockershim移除的态度是:这不是“K8s背叛了Docker”,而是K8s长大了,决定不再依赖某个具体厂商的实现,改走标准接口。你可以继续用Docker做开发、构建,但在生产集群里,让更轻量、更直接的运行时上台,是合理的取舍。

3. 主流容器运行时选型对比

3.1 先按技术路线分个类

要选型,先把候选者分个类。现在市面上接入K8s的主流运行时,大致可以分成三条技术路线。

第一条是“通用CRI实现”路线,以containerd和CRI-O为代表。它们直接实现CRI接口,不依赖Docker,职责边界清晰,是生产环境的优先选择。

第二条是“兼容Docker API”的过渡路线,代表是cri-dockerd。它本质上还是让K8s走Docker daemon,只适合特殊情况。

第三条是“增强隔离”的安全容器路线,代表是gVisor和Kata Containers。它们能提供比普通容器更强的安全隔离,适合运行不可信工作负载或多租户边界要求高的场景。

下面逐个拆解。

3.2 containerd:当前事实标准

containerd是现阶段K8s生态里事实上的默认运行时。它最早是Docker生态的一部分,后来被独立出来捐给了开源基金会,再后来有了自己的CRI插件。从一开始的“Docker底层组件”,变成了一个完全独立、轻量、面向K8s的运行时平台。

为什么它是默认选择?首先是轻量。containerd只需要一个守护进程,监听在/run/containerd/containerd.sock,不提供那些K8s用不到的build、网络、卷能力,这些东西K8s分别交给了其他组件。其次是内置了CRI插件,装上就能用,不需要额外的shim翻译。再次是生态成熟,crictl、nerdctl这些工具都围绕它做得很顺手。

containerd有一个值得了解的概念:命名空间。它在内部区分了k8s.io和default两个常见命名空间。Kubelet创建的Pod容器都在k8s.io这个命名空间里,而普通本机调试容器默认归default。你用ctr命令查看镜像时,如果不加-n参数,看不到K8s的镜像;用crictl则默认就锁定在k8s.io上。这个设计一开始容易绕晕人,记住了就能少踩坑。

日常运维中,containerd配合nerdctl可以做到接近Docker的体验。nerdctl支持docker build、docker run这类命令,还支持docker-compose风格。如果你习惯了Docker CLI,切到containerd后,用nerdctl过渡是最平滑的。

3.3 CRI-O:为K8s而生的“最小实现”

CRI-O定位非常纯粹:只做一件事,让Kubelet通过CRI接口运行OCI容器。它不像containerd那样带着一套通用容器平台的包袱,也不含任何Docker兼容层。它直接对接runC,镜像管理交给容器/镜像存储库,网络交给CNI,存储交给CSI。

CRI-O的优点是“少即是多”。它专门针对K8s的行为进行调整,K8s升级后,CRI-O对接口的适配往往跟得很快。很多OpenShift用户和RHEL系生态的团队天然倾向CRI-O,因为它在某些场景下与系统组件配合得更紧密。

它的缺点同样明显:社区规模比containerd小,能找到的运维经验和第三方工具少一些;如果你习惯用docker CLI管理本机容器,CRI-O不具备那种体验,得靠podman或crictl来补。对于大多数团队来说,containerd已经够用,CRI-O更多是特定技术栈里的选择。

3.4 cri-dockerd:过渡期的保底方案

cri-dockerd是dockershim被移除后,那个“翻译层”独立出来的产物。它从Kubelet里被搬出来,变成了一个单独的进程,继续做CRI到Docker API的转换。如果你有存量集群,实在不想动底层运行时,可以用它续命。

我见过一些团队因为内部工具深度依赖Docker daemon,比如审计系统要从/var/run/docker.sock读取容器元数据,或者运维脚本里全是docker inspect,所以短期内坚持用cri-dockerd。这种方案在技术上是合法的,社区维护者也表示会继续支持一段时间。

但它只能是过渡。你想想,Kubelet请求先到cri-dockerd,再转给Docker daemon,Docker daemon内部再走containerd到runC,整个链路上多了两个中转。资源消耗、故障面、排障难度全都会上升。而且随着K8s版本不断往上走,cri-dockerd的兼容性只会越来越吃力。我的建议是:除非有强理由,否则新集群不要选它,存量集群也要排期迁移。

3.5 安全容器:gVisor与Kata Containers

普通容器共享宿主内核,隔离主要靠namespaces和cgroups。这对于大部分业务足够,但在面向不可信代码或多租户强隔离的场景里,风险依然存在。一旦内核漏洞被利用,容器和宿主机之间的边界可能被突破。

gVisor的思路是“用户态内核”。它在宿主机和容器应用之间插入一个用户态内核,把容器内的系统调用先拦截下来,再由这个用户态内核去处理并代理到宿主机。这样即使容器内发起恶意调用,攻击的也是这个用户态内核,很难直接打到真实宿主机内核。代价是系统调用性能损耗比较明显,对I/O密集型的应用不太友好。

Kata Containers的思路更“物理”:每个容器直接跑在一个轻量虚拟机上,有自己的内核,虚拟化层自带强隔离。它依然遵循OCI规范,所以可以无缝接入K8s。代价是启动速度慢一点、内存开销大一些,但在金融、政务这类合规要求高的环境,这种“虚拟机级别的隔离”意味着直接满足安全基线。

选型时不要把这两者当作默认生产选项,它们都有明确的适用场景,只有当你真的需要一个额外隔离层时,才考虑它们。

3.6 一张表看明白选型

下面这张表是我整理选型笔记时常用的版本,比较直观。

运行时技术路线隔离级别典型场景运维成本常用CLI
containerd通用CRI实现内核共享大多数生产集群低crictl、nerdctl、ctr
CRI-O通用CRI实现内核共享OpenShift及RHEL系生态低crictl、podman
cri-dockerdDocker API兼容内核共享存量Docker集群过渡中docker、crictl
gVisor用户态内核系统调用级隔离不可信代码沙箱中crictl
Kata Containers轻量虚拟机虚拟机级隔离强多租户隔离、合规要求中高crictl

大部分团队如果没特殊要求,直接在containerd上落地就行。不必为了“多一个选择”去增加复杂度。

4. 从Docker平滑切到containerd:完整迁移实操

4.1 迁移前的盘点该做什么

从Docker切到containerd,最怕的不是操作难,而是想当然。整个迁移最耗时间的,往往是把节点上那些“只有Docker能处理”的依赖都找出来。

所以先别急着动手,先把这些信息盘清楚。

第一,确认集群版本和运行时现状。执行:

kubectl get nodes -o wide

看RUNTIME-VERSION列,确认现在是不是Docker。再查看一下Kubelet的启动参数,确认当前的container-runtime-endpoint指向哪里。

第二,盘点业务对Docker的特殊依赖。有没有CronJob会在Pod里挂载docker.sock去构建镜像?有没有日志采集组件通过Docker socket拿容器日志?有没有监控组件通过Docker API获取容器指标?这些是最容易被忽略的。任何挂载了/var/run/docker.sock的工作负载,在切换后都会失灵,迁移前必须把它们改造或替换掉。

第三,确认镜像都能从镜像仓库正常拉取。刚才说过,切换后containerd的本地镜像是空的,Kubelet会重新拉取所有镜像。如果集群里有一些“只存在于节点本地、没有推到仓库”的镜像,你要么提前推到仓库,要么提前用nerdctl或ctr把镜像导入到containerd里。

第四,选好迁移窗口。节点迁移意味着一个批次内的Pod会被重建,虽然理论上服务不受影响,但如果有单副本且在节点上有本地数据,就有风险。老规矩,先选一个最不重要的节点练手。

4.2 kubeadm集群一颗一颗节点换

下面以最常见的kubeadm部署集群为例,完整走一遍单节点切换流程。

先把containerd装好并写好配置。

apt install containerd mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml

这里要改两个关键参数。

第一个是SystemdCgroup,我每次都要强调。找到containerd配置里CgroupDriver这段,改成:

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] base_runtime_spec = "" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true

为什么一定要改?因为K8s和节点上的systemd在cgroup管理上是有约定的。如果Kubelet用systemd驱动来管理cgroup,而containerd还在用cgroupfs,两边账本对不上,Pod创建时就会冲突,具体表现为Pod一直ContainerCreating,报runc创建沙箱失败。这个问题在迁移后相当常见,先在这里堵死。

第二个是sandbox_image。默认的pause镜像地址在K8s官方仓库,国内环境经常拉不动。把配置里的sandbox_image改成一个可用的镜像加速地址,或者内网能访问的地址。比如改成:

sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.9"

然后启动并检查containerd。

systemctl enable --now containerd crictl info

看到输出里的runtimeName是containerd,就算就位了。

接下来改Kubelet的启动配置,让Kubelet知道去连接containerd。不同发行版的配置文件位置不太一样,常见的是在/etc/sysconfig/kubelet或/etc/systemd/system/kubelet.service.d/10-kubeadm.conf里。核心是加上:

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

如果你的K8s版本较老,可能还需要--container-runtime=remote,新版本一般只需要endpoint了。

然后执行节点排空、停止组件、清理容器。

kubectl drain node-01 --ignore-daemonsets --delete-emptydir-data systemctl stop kubelet systemctl stop docker docker rm -f $(docker ps -aq)

这段操作要重点说明:为什么要把旧容器删掉?因为Kubelet停止后,节点上的Pod容器还在被Docker管理着。下次Kubelet启动时,它要去containerd里找这些容器,但containerd根本不知道它们的存在。那条dockershim时代的容器记录会一直残留,占用磁盘和资源。直接删掉,让Kubelet在节点重新可用后,用containerd重建所有Pod,是最干净的做法。

然后启动新运行时链。

systemctl enable --now containerd systemctl start kubelet

观察一下节点状态。

kubectl get nodes node-01

等它回到Ready,跑一个测试Deployment,确认新Pod调度到这个节点且日志正常。

kubectl uncordon node-01 journalctl -u kubelet -f

单个节点迁移成功后,再逐一对其他节点重复同样的流程。整个集群全部切换完成后,再决定要不要卸载Docker。如果确实没有其他进程依赖Docker,卸载即可;如果有开发或审计需求,也可以把它当作普通工具保留,只是K8s不再使用它。

4.3 docker命令与crictl命令对照

从Docker切换到containerd后,日常排查命令要换一套。下面这张对照表建议存下来,刚开始一定会反复查。

Docker命令crictl命令说明
docker pscrictl ps查看运行中容器
docker ps -acrictl ps -a查看所有容器
docker imagescrictl images查看镜像列表
docker logs [id]crictl logs [id]查看容器日志
docker exec -it [id] shcrictl exec -it [id] sh进入容器
docker inspect [id]crictl inspect [id]查看容器详情
docker stop [id]crictl stop [id]停止容器
docker rm [id]crictl rm [id]删除容器
docker infocrictl info查看运行时信息

crictl不能构建镜像,也不支持docker run这类交互式启动容器,它定位是“排查和生命周期管理”。如果你需要在节点上手动跑一个临时容器调试,用nerdctl补位:

nerdctl run -it --rm -n k8s.io alpine sh

注意加-n k8s.io,这样容器会创建在K8s的命名空间里,方便和节点上其他容器一起被看到。如果只是在普通环境调试,不加也行。

5. 运行时切换后的常见问题与排障实录

5.1 节点NotReady的几大经典原因

整个迁移过程中,我见过最多的问题,节点状态直接变成NotReady,排査下来一般就这几类原因。

第一类,Kubelet根本没有连上containerd。如果你改了Kubelet参数但没让系统服务重新加载配置,Kubelet可能还在找旧的dockershim socket。报错很典型,journalctl里会出现:

unable to determine runtime API version: rpc error: code = Unimplemented

处理方式是检查Kubelet启动参数,确认--container-runtime-endpoint指向的是/run/containerd/containerd.sock,然后重启kubelet服务。

第二类,cgroup驱动不一致。前面已经强调过SystemdCgroup配置。如果仍然出现Pod无法创建、报错信息里带cgroup字样,就去检查两边的cgroup driver是不是同一个。Kubelet的配置在/var/lib/kubelet/config.yaml里,containerd的配置在/etc/containerd/config.toml里,两边保持一致再重启。

第三类,sandbox_image拉不下来。这种问题节点不一定NotReady,但所有Pod都会卡在ContainerCreating,报错通常是拉取pause镜像超时或没有权限。解决办法是修改sandbox_image指向内网可用的镜像,然后systemctl restart containerd kubelet。

第四类,磁盘或镜像存储目录问题。containerd默认数据目录是/var/lib/containerd,如果这个目录所在分区空间不足,containerd可能无法创建容器。这种问题报错会比较模糊,建议先看df -h和containerd的日志。

5.2 镜像拉取与私有仓库配置

迁移到containerd后,私有仓库的配置方式也和Docker不一样。Docker里在/etc/docker/daemon.json中配置insecure-registries,containerd则要走它自己的配置文件。

假设你的私有仓库地址是registry.example.com,需要在/etc/containerd/config.toml里加这一段:

[plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.example.com"] endpoint = ["https://registry.example.com"]

如果需要认证,还要在config.toml里指定auth配置,或者使用credential store。更简单的办法是提前用ctr或crictl把私有仓库的镜像拉一遍,先确认地址和认证没问题,再动节点。

还有一个细节值得注意:如果你在Docker阶段用docker save导出镜像,想迁移后在containerd里直接load,要用nerdctl而不是ctr。docker save出来的tar包是Docker archive格式,ctr的images import对它的兼容性比较好,而nerdctl load是专门处理这种tar包的。但即使导入了镜像,Kubelet能不能直接用也取决于镜像tag和拉取策略。凡是有镜像仓库的团队,建议切换后直接让Kubelet重新拉,比在几百个节点上分别导包靠谱得多。

5.3 日志、监控与数据路径变化

从Docker切到containerd后,日志和监控,可能看起来不动,实际上会有变化。

Docker内部生成的容器日志,按json-file或journald方式存储。containerd的日志走CRI标准,最终软链到/var/log/containers和/var/log/pods。很多日志采集组件,比如fluentd、filebeat,已经适配了这些路径。切换到containerd后,如果采集器原来通过Docker API读日志,就必须改成读文件系统或通过CRI的标准路径。

监控方面,老版本cadvisor如果通过Docker API获取容器指标,切换后会拿不到数据。解决办法是升级cadvisor版本,或者让它在启动参数里显式指定runtime端点。这里提醒一句,升级前先确认Kubernetes的监控组件对CRI的支持度,否则可能采集出来全是空指标。

5.4 排查工具链:crictl与ctr的配合

排查一个container相关的问题,我一般按照这个顺序来:

先用crictl ps -a看容器状态,确认容器是不是卡在创建阶段,再用crictl logs看应用日志。如果应用日志正常,但Pod起不来,把crictl inspect拿到的容器详情和Kubelet日志对照着看。

crictl默认连的是/run/containerd/containerd.sock,通常不用额外配置。如果你要深入看镜像和底层存储的细节,ctr能提供更多原始信息,但要注意命名空间问题。K8s的容器都在k8s.io命名空间里,查看K8s镜像要用:

ctr -n k8s.io images list

不看命名空间,直接ctr images list,你会以为镜像根本没导入进去。这个坑我见过多次,也有人因此在迁移后白白重拉了好几遍镜像。

再结合Kubelet日志一起看:

journalctl -u kubelet -n 200

排查顺序总结下来就是:先确认Kubelet连上了运行时,再确认sandbox正常,再确认镜像能拉取,最后看应用容器的启动日志。这个四步排查法,能覆盖绝大多数运行时问题的排查路径,特别适合刚切到containerd的团队。

6. 我的选型建议与经验

6.1 新集群怎么选

如果是从零开始建一套K8s,选containerd就行,不需要犹豫。它在社区里的使用量最大、资料最多、适配最广,和你已有的数据面工具兼容性也最好。如果你所在的技术栈和RHEL系绑定很深,团队对红帽系生态更熟,那么CRI-O也可以,但需要团队对它的运维节奏有足够把握。

不要为了“新的就是好”或者“安全就是好”这类理由,在一开始就上gVisor或Kata。Kata适合明确有合规和强隔离要求的金融、政企场景,gVisor适合边界沙箱和不可信代码场景,它们都不是默认项。普通业务跑在共享内核的常用运行时上,才是主流玩法。

6.2 存量集群怎么改

存量集群如果还在用Docker,先别急着焦虑。如果当前版本还在生命周期内,集群稳定运行,可以先做规划,不必当天就切换。但建议尽快把这个事列入技术债排期,毕竟Docker链路在K8s社区已经属于退场状态,越早迁移,越少被动。

迁移时优先选业务低峰窗口,按节点滚动操作,先在测试环境和低负载节点跑通全套命令,再进行生产。迁移完成后,把那些直接依赖Docker socket的组件全部清理掉,否则这些组件以后都是隐患。

6.3 平时怎么练习

如果你现在还不是每天接触K8s,我建议直接在个人电脑上装一套带containerd的K8s环境。学习阶段就把crictl当成习惯,刻意回避docker ps;遇到容器问题,先看crictl能看到什么信息,再去查文档。这个过程会逼你理解“运行时到底负责什么”,而不是把“容器”和“Docker”直接画等号。

另外搭环境时可以用nerdctl来模拟一些日常容器操作,比如本地跑Nginx、拉镜像、进容器调试。你会发现它和docker命令非常接近,学习成本很低。等你在containerd上熟练了之后,再回头去看运行时之争,就不容易被各种说法带偏了。

我自己的习惯是不在生产节点上装Docker,需要构建镜像时选择buildah或独立构建机,运行容器则统一走containerd。这样能减少节点驻留进程,也避免生产环境里出现两个容器管理体系的混乱。踩过几次坑之后,你会慢慢觉得,一个节点只跑一套轻量运行时,其实是最省心的状态。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询