三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Kubernetes弃用Docker:从CRI标准到containerd迁移的深度解析

Kubernetes弃用Docker:从CRI标准到containerd迁移的深度解析

1. 项目概述:一次技术栈的“和平分手”

如果你在2020年底之后才开始接触Kubernetes,可能会觉得“K8s放弃Docker”是个老生常谈的话题。但对于很多从Docker Swarm时代一路走来的老运维,或者习惯了docker rundocker-compose up这套工作流的开发者来说,这无疑是一次认知上的地震。当时社区里充满了困惑和不解:“我Docker用得好好的,K8s凭什么说不用就不用了?”“是不是以后就不能用Docker镜像了?”“我的CI/CD流水线是不是要重写了?”

事实上,这并非一次突如其来的“决裂”,而是一场酝酿已久、水到渠成的技术架构演进。K8s(Kubernetes)并没有“放弃”容器本身,它放弃的只是Docker作为一个容器运行时的默认集成。理解这场变革,不仅能帮你厘清K8s与Docker如今的关系,更能让你深入理解云原生基础设施的底层设计哲学。简单来说,K8s需要的是一个更专注、更标准化的“发动机”(容器运行时),而Docker是一个功能丰富的“整车厂”。当K8s这个“智能驾驶平台”成熟后,它更希望直接对接标准化的“发动机”,而不是整合一个自带方向盘、座椅和音响的“整车厂”。接下来,我们就从容器生态的演变、接口标准化的必然以及实际运维的视角,彻底拆解这次“分手”背后的深层逻辑。

2. 核心需求解析:K8s到底需要什么?

要理解为什么“分手”,首先要明白K8s和Docker各自的核心诉求是什么。这绝非简单的“谁更好”,而是角色定位的根本不同。

2.1 Docker的定位:开发者友好的全能工具箱

Docker的成功,在于它极大地降低了容器技术的使用门槛。它提供了一站式解决方案:

  • Docker Daemon:一个常驻后台的守护进程,负责管理容器生命周期、镜像构建与分发。
  • Docker CLI:我们熟悉的docker命令,是与Daemon交互的客户端工具。
  • Docker Registry:镜像仓库服务。
  • Docker Compose:用于定义和运行多容器应用的工具。
  • Docker Desktop:为Mac/Windows用户提供完整的桌面端体验。

Docker把容器运行时、镜像构建、网络、存储、集群编排(早期Swarm)等众多功能打包在一起,形成了一个强大的“全家桶”。对于开发者个人或小团队,这个全家桶非常方便,开箱即用。

2.2 K8s的定位:生产级编排系统的标准化诉求

K8s的目标是成为数据中心的操作系统,管理成千上万个容器化应用。它的核心诉求是:

  1. 稳定与可靠:作为基础设施,必须极其稳定,任何组件的崩溃都不应导致集群大面积故障。
  2. 标准化与可插拔:希望底层组件(网络、存储、运行时)能通过标准接口接入,避免被单一厂商绑定,促进生态繁荣。
  3. 轻量与高效:每个组件职责单一,减少不必要的开销和攻击面。
  4. 安全的生命周期管理:对容器的创建、运行、监控需要有更精细、更安全的控制能力。

问题就出在这里。Docker Daemon作为一个大而全的单体守护进程,与K8s的诉求产生了根本性冲突。

3. 技术架构冲突与CRI的诞生

矛盾的核心在于架构。在早期,K8s是通过一个叫dockershim的组件来调用Docker的。

3.1 “垫片”架构的固有缺陷

dockershim的本质是一个适配器,它把K8s定义的容器操作指令,翻译成Docker Daemon能理解的API(Docker Engine API)。这个架构带来了几个致命问题:

  1. 单点故障与稳定性风险:Docker Daemon是一个独立的、有状态的守护进程。如果它崩溃了,所有通过它创建的容器都会失去管理(尽管容器本身可能还在运行)。这对于K8s控制平面来说是不可接受的。K8s希望运行时是轻量的、无状态的,即使运行时重启,也不应影响现有容器的状态。
  2. 额外的抽象层与性能损耗:调用链变成了:kubelet->dockershim->Docker Daemon->containerd->runc。每多一层,就意味着更多的序列化/反序列化、进程间通信开销和潜在的故障点。
  3. 功能冗余与维护负担:Docker Daemon提供了很多K8s根本不需要的功能(比如内置的镜像构建、Docker Swarm集群管理等)。K8s只需要它最核心的容器生命周期管理能力。维护dockershim这个“胶水代码”成了K8s社区一个沉重的负担,尤其是当Docker Engine API发生变化时。
  4. 安全边界模糊:Docker Daemon通常以root权限运行,拥有巨大的权限。dockershim的存在使得攻击面增大。

3.2 CRI:容器运行时的“普通话”标准

为了解决这些问题,Kubernetes社区提出了容器运行时接口(Container Runtime Interface, CRI)。你可以把CRI想象成容器运行时的“普通话”标准。

  • 目标:定义一套K8s(具体是kubelet)与任何容器运行时之间通信的通用API协议(基于gRPC)。
  • 好处:任何实现了CRI的容器运行时,都可以无缝接入K8s。K8s无需关心底层运行时是Docker、containerd还是CRI-O,它只需要用“普通话”(CRI)发号施令即可。

CRI的诞生,标志着K8s在基础设施标准化上迈出了关键一步。它希望底层运行时是一个专注、高效、稳定的“引擎”,而不是一个“整车厂”。

3.3 Docker与CRI的“兼容性”问题

那么,Docker本身支持CRI吗?不支持。Docker Daemon暴露的是自己的Docker Engine API,而不是CRI。这就是最根本的“语言不通”。

为了让Docker能在K8s 1.23版本之前继续工作,社区不得不一直维护着dockershim这个“翻译官”。但随着CRI的成熟和替代方案(如containerd)的稳定,继续维护这个多余的、有问题的翻译层就显得越来越不划算。

最终,K8s社区做出了一个合乎逻辑的决定:废弃dockershim,直接拥抱实现了CRI的标准化运行时。这被很多人解读为“K8s放弃Docker”,准确地说,是“K8s放弃通过非标准的、间接的方式(dockershim)来调用Docker所包含的容器运行时功能”。

4. 替代方案:containerd的崛起与实操迁移

那么,Docker“离开”后,谁接替了它的位置?答案是:containerd。事实上,它一直都在。

4.1 containerd:从幕后到台前

很多人不知道的是,从Docker 1.11版本开始,Docker Daemon的底层容器运行时功能就已经被拆分成独立的containerd项目。Docker Daemon本身变成了一个更上层的管理工具,它通过API调用containerd来实际创建和管理容器。

containerd是一个专注于容器核心功能的工业级运行时:镜像传输、容器执行、存储管理、网络命名空间管理。它比Docker Daemon更轻量、更专注,并且原生实现了CRI接口(通过一个叫cri-containerd的插件)。

所以,当K8s移除dockershim后,它并不是找了一个新朋友,而是选择了直接和老朋友containerd“牵手”。架构从:kubelet->dockershim->Docker Daemon->containerd简化为了:kubelet->CRI->containerd

少了两层,更简洁、更高效、更稳定。

4.2 对用户的实际影响:镜像、命令与工作流

这是大家最关心的问题:改变之后,对我们有什么影响?

  1. 镜像兼容性:完全不受影响

    • Docker镜像遵循OCI(开放容器倡议)标准格式。containerd和所有其他主流容器运行时都完全支持OCI标准。你之前所有docker pull下来的镜像,都可以继续在K8s中使用,无需任何转换。镜像仓库(如Docker Hub、Harbor)也完全通用。
  2. 构建工具:不受影响

    • 你仍然可以使用docker build来构建镜像。Docker作为一个强大的镜像构建工具和开发者桌面体验工具,其地位并未改变。构建好的镜像,推送到仓库,K8s集群中的containerd会拉取并运行它。你的CI/CD流水线中关于镜像构建的部分通常无需改动。
  3. 节点运维命令:需要改变习惯

    • 这是主要的变化点。以前在K8s节点上排查问题,我们习惯用docker psdocker logsdocker exec等命令。现在,需要改用containerd提供的命令行工具crictl(CRI兼容的工具)或ctr(containerd原生工具)。
    • 重要提示crictl的命令设计刻意模仿了dockerCLI的使用习惯,以降低迁移成本。例如:
      • docker ps->crictl ps
      • docker logs <container-id>->crictl logs <container-id>
      • docker exec -it <container-id> sh->crictl exec -it <container-id> sh
      • docker images->crictl images
    • ctr命令更底层,功能更强,但语法与docker差异较大,一般用于更高级的调试。
  4. Docker Desktop等工具:不受影响

    • Docker Desktop for Mac/Windows 在“启用Kubernetes”时,内部早已使用containerd作为K8s的运行时。对于开发者本地环境,一切照旧。

4.3 迁移实操指南与注意事项

如果你的集群是在K8s 1.24版本之前搭建的,且仍在使用dockershim,那么升级到1.24+时需要迁移。主流K8s安装工具(如kubeadm、k3s、RKE2)的新版本默认都已使用containerd

以使用kubeadm的集群为例,迁移的核心步骤:

  1. 准备工作

    • 备份所有重要数据和应用配置。
    • 逐节点操作,确保应用有高可用或可在其他节点重建。
    • 清空节点:使用kubectl drain <node-name> --ignore-daemonsets安全驱逐节点上的Pod。
  2. 卸载Docker Engine

    # 停止Docker服务 sudo systemctl stop docker # 卸载Docker引擎包 (以Ubuntu为例) sudo apt-get purge -y docker-ce docker-ce-cli # 清理残留文件(谨慎操作,确保不需要旧镜像/容器数据) sudo rm -rf /var/lib/docker
  3. 安装containerd

    # 安装containerd.io包 (具体版本需匹配K8s要求) sudo apt-get update sudo apt-get install -y containerd.io # 生成默认配置文件 sudo mkdir -p /etc/containerd sudo containerd config default | sudo tee /etc/containerd/config.toml # 修改配置,启用Systemd Cgroup驱动(与kubelet保持一致) sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml # 重启containerd sudo systemctl restart containerd sudo systemctl enable containerd
  4. 配置kubelet使用containerd

    • 修改kubelet参数,通常位于/var/lib/kubelet/kubeadm-flags.env/etc/default/kubelet
    • 确保--container-runtime参数为remote,且--container-runtime-endpoint指向containerd的CRI socket(默认是unix:///run/containerd/containerd.sock)。
    # 例如,在kubeadm环境中,编辑配置文件后重启kubelet sudo systemctl restart kubelet
  5. 验证与恢复

    • 使用crictl ps查看容器是否正常运行。
    • 使用kubectl get nodes查看节点状态是否恢复为Ready
    • 取消节点保护:kubectl uncordon <node-name>

实操心得:在生产环境迁移时,强烈建议先在测试环境完整走通流程。重点关注自定义容器运行时配置(如私有镜像仓库的认证、日志驱动等)如何从Docker迁移到containerd。containerd的配置文件是/etc/containerd/config.toml,其结构与Docker的daemon.json不同,需要重新学习。

5. 深入对比:containerd vs Docker Daemon

理解两者的区别,能更好地把握这次架构变更的精髓。

特性维度Docker Daemoncontainerd
定位完整的容器平台,面向开发者专注的容器运行时,面向基础设施
架构单体守护进程,集成度高模块化设计,通过插件扩展
APIDocker Engine API (REST)CRI (gRPC) 为主,低级API为辅
功能范围容器生命周期、镜像构建、网络、存储、集群(Swarm)等核心的容器生命周期、镜像管理、存储管理
资源占用相对较高更轻量,内存和CPU占用更少
启动速度较慢更快
稳定性组件耦合,一损俱损风险稍高职责单一,更稳定,故障影响面小
安全性庞大的root进程,攻击面大更小的攻击面,支持rootless模式

简单来说,Docker Daemon是一个“瑞士军刀”,而containerd是一把锋利的“主厨刀”。对于K8s这个需要高效、稳定、标准化后厨的“大酒店”来说,一把专注的好刀比一个多功能工具更合适。

6. 常见问题与排查技巧实录

迁移到containerd后,运维习惯需要改变。以下是一些常见问题和处理技巧。

6.1 命令转换速查与技巧

场景:需要进入容器排查问题。

  • 旧习惯docker exec -it <container-id> bash
  • 新方法crictl exec -it <container-id> bash
  • 技巧:如果容器内没有bash,可以尝试sh。获取容器ID最快捷的方式是结合kubectlkubectl get pods -n <namespace> <pod-name> -o jsonpath='{.status.containerStatuses[0].containerID}' | cut -d'/' -f3。这个命令能直接输出容器ID供crictl使用。

场景:查看容器日志。

  • 旧习惯docker logs -f <container-id>
  • 新方法crictl logs -f <container-id>
  • 技巧crictl logs默认输出所有日志。对于K8s Pod,日志通常也被收集到/var/log/pods//var/log/containers/目录下,你可以直接使用tail -f查看这些文件,这在crictl不可用时是备选方案。

场景:检查容器内进程。

  • 旧习惯docker top <container-id>
  • 新方法:首先用crictl inspect <container-id>获取容器的PID,然后使用nsenter命令进入容器的命名空间查看,或者直接用ps -ef | grep <pid>查看进程树。更简单的方法是使用crictl exec <container-id> ps aux

6.2 镜像拉取失败问题排查

这是迁移后最常见的问题之一,尤其是使用私有镜像仓库时。

  1. 症状:Pod状态为ImagePullBackOff,事件显示Failed to pull image
  2. 排查思路
    • 第一步:确认镜像地址和标签。使用kubectl describe pod <pod-name>查看事件详情。
    • 第二步:在节点上手动拉取测试。使用crictl pull命令模拟拉取,这能绕过K8s,直接测试containerd的配置。
      sudo crictl pull myprivateregistry.com/myapp:v1
    • 第三步:检查containerd的私有仓库配置。Docker的认证配置在~/.docker/config.json,而containerd需要在其配置文件/etc/containerd/config.toml中配置[plugins."io.containerd.grpc.v1.cri".registry.mirrors][plugins."io.containerd.grpc.v1.cri".registry.configs]部分。这是与Docker最大的配置差异点
    • 第四步:重启containerd。修改配置后,务必sudo systemctl restart containerd
    • 第五步:检查K8s的Secret。如果使用imagePullSecrets,确保Secret在正确的命名空间,且内容正确。

避坑技巧:对于私有仓库的HTTPS证书问题,如果使用自签名证书,需要在containerd配置中指定ca文件,或者直接配置insecure_skip_verify = true(仅限测试环境)。配置格式示例:

[plugins."io.containerd.grpc.v1.cri".registry.configs."myregistry:5000".tls] insecure_skip_verify = true

6.3 容器日志与存储路径变更

Docker的默认工作目录是/var/lib/docker,而containerd的默认目录是/var/lib/containerd。这带来两个变化:

  1. 日志路径:容器标准输出日志不再位于/var/lib/docker/containers/.../*.log。K8s环境下,容器日志被kubelet通过CRI接口获取后,默认写入节点文件的/var/log/pods/<namespace_pod_uid>/<container-name>/目录下,按序号分文件存储。同时,在/var/log/containers/目录下有指向这些日志文件的符号链接,方便查找。
  2. 镜像存储:镜像和容器层数据现在存储在/var/lib/containerd/io.containerd.content.v1.content/等子目录下,结构更为复杂。一般不需要直接操作这些文件。

磁盘空间清理:以前用docker system prune,现在可以用:

  • sudo crictl rmi --prune:删除未被任何容器引用的镜像。
  • 清理容器:sudo crictl rm删除已停止的容器。
  • 注意:containerd没有一键清理所有缓存数据的命令,需要手动结合crictlctr命令,或定期清理/var/lib/containerd目录(风险高,需谨慎)。

6.4 crictl与ctr命令的选择

  • crictl:这是K8s项目维护的、兼容CRI的调试工具。它的命令和输出格式针对K8s环境做了优化,能更好地显示与Pod、容器沙箱(pause容器)相关的信息。日常节点运维和问题排查,首选crictl
  • ctr:这是containerd原生的命令行客户端,功能更强大,可以操作所有containerd管理的命名空间(而crictl只操作k8s.io命名空间)。它可以管理镜像、容器、命名空间、快照等。当你需要进行底层操作,或者crictl无法满足需求时(如导入导出镜像、管理非K8s容器),才使用ctr

例如,导入一个离线镜像包:

# 使用ctr导入 sudo ctr -n=k8s.io images import /path/to/image.tar # 使用crictl查看是否导入成功 sudo crictl images

7. 总结与展望:生态的必然选择

回顾整个过程,K8s放弃对Docker的默认集成,不是一个针对Docker的“惩罚”,而是云原生生态走向成熟和标准化的必然结果。CRI标准的确立,就像为容器运行时定义了USB接口,让K8s这个“主机”可以连接任何符合标准的“外设”(运行时),无论是containerd、CRI-O还是其他任何实现。

对于用户而言,这次变化带来的短期阵痛(主要是运维命令的改变)远小于其带来的长期收益:更稳定的集群、更高效的资源利用、更清晰的架构边界。Docker本身作为容器技术的布道者和卓越的开发者工具,其历史地位不可撼动,它只是在一个更专业化的生产环境编排领域,将核心的运行时职责交给了更专注的组件。

现在,当我们再部署一个K8s集群时,标准栈已经变成了:Kubernetes + containerd + runc。这是一个更清晰、更健壮、更面向未来的架构。理解这次变迁,意味着你不仅跟上了技术的步伐,更深入理解了云原生基础设施演进的底层逻辑——标准化、解耦和专注。下次再有人问起“K8s和Docker是什么关系”,你可以清晰地告诉他:它们是曾经亲密的合作伙伴,如今在标准化道路上,各自扮演着更专业、更高效的角色。Docker负责制造优秀的“集装箱”(镜像)和提供友好的“码头”(开发体验),而K8s和containerd则负责在庞大的“物流中心”(数据中心)里,高效、自动化地调度和管理这些集装箱的运输与存放。

← 返回列表