资讯动态

Docker架构演变全解析:从单体daemon到containerd+runc

发布时间:2026/10/17 5:28:39 来源:尧图企业网站定制
最近几年每次聊容器总有人冒出一句Docker 已经不是从前的 Docker 了。这话对了一半也错了一半。对的是底层架构确实换了一遍血错的是从使用者角度看docker run、docker build、docker compose依然是千万开发者最熟练的那套肌肉记忆。这篇文章要做的就是把这套已经不太“Docker 原教旨主义”的架构完整剖开看看从单体守护进程到 CLI dockerd containerd shim runc 这条职责链Docker 到底经历了什么以及那个“从前的 Docker”究竟是没了还是换了种方式活了下来。如果你用的是近两年的 Docker 版本而且曾经好奇过为什么docker version的 Server 输出里多了 containerd 和 runc 两行那么这篇内容正好能把你脑子里零散的知识点串起来。1. Docker 1.x 时代一个守护进程把所有脏活累活包圆了1.1 当时的设计CLI、REST API 与万能 daemonDocker 刚火起来那阵子架构简单到可以用一句话讲完你敲docker rundocker 客户端把请求通过 unix socket 发给宿主机上的 docker daemondaemon 负责从镜像仓库拉镜像、创建容器、配置网络、挂载数据卷然后把容器进程拉起来。这个 daemon 不叫“容器运行时”它更像一个全栈操作系统。镜像管理是它镜像构建是它容器执行是它网络端口映射是它日志收集也是它。用一个生活化的比方Docker daemon 就像一家餐厅的老板前台接待、后厨炒菜、传菜上桌、结账收银全是同一个人。好处显而易见心智模型简单用户只需要认识一个docker.sock就能操作一切。对 2013 年前后的开发者来说这种“一条命令得到一个干净隔离环境”的体验是颠覆性的。虚拟机太重配置环境太累Docker 一句话就解决了。那时候的 docker daemon 确实配得上“万能”这个形容词但“万能”同时也意味着“什么都要管”一旦某一环出问题整条链路都会跟着遭殃。1.2 libcontainer甩掉 LXC 的那一次关键转身早期 Docker 并不是直接调用内核接口的。它最初基于 LXC 这套现成的容器工具通过封装 LXC 的能力来创建容器。现在回顾这段历史很多人会忽略一个关键点LXC 的目标是隔离出一个完整的“系统环境”它会初始化一大堆系统级的东西而 Docker 真正要的是“在隔离环境里跑一个指定的进程”。目标是错位的。目标错位带来的直接问题就是不可控。LXC 依赖宿主机里安装的 lxc-start 这类系统工具版本由系统包管理器控制Docker 很难保证在不同机器上得到完全一致的行为。于是 Docker 团队在 2014 年推出了 libcontainer用 Go 直接调用 Linux 内核的 Namespace 和 Cgroups 来创建容器彻底摆脱了对 LXC 的外部依赖。这一步的分量怎么强调都不过分。libcontainer 让 Docker 从“一个调用别人工具的协调者”变成了“直接跟内核打交道的容器引擎”也为此后 runc 的诞生埋下了伏笔。后面所有架构拆分源头都在这里。1.3 单体 daemon 的隐患后来全变成了拆分的理由单体架构在用户量小的时候没问题但当 Docker 进入生产环境问题就开始暴露了。我踩过的、也见过别人踩的最典型的坑有这么几个第一个是单点风险。daemon 进程一旦异常退出所有容器就失去了“管理者”。容器进程本身未必挂但你没法再执行 stop、logs、exec 这些操作想去捞现场日志都无从下手。第二个是升级代价。在单体内核时代升级 Docker daemon 基本上意味着重启 daemon而 daemon 重启会连带影响它管理的所有容器。生产环境里想升级一下 Docker 版本得先把业务容器全部停掉这在生产环境是不可接受的。第三个是权限边界太宽。docker daemon 长期以 root 身份运行API 面又覆盖了镜像、网络、存储、进程管理这么多领域。一旦 /var/run/docker.sock 被暴露出去攻击者拿到 docker socket 基本等于拿到宿主机 root 权限。这个攻击面随着功能膨胀只会越来越大。还有一点是生态问题。当时的处境是任何想接入容器生态的系统都必须跟 Docker 打交道。调度器、云平台、监控系统都想在 Docker 之上做集成但 Docker 的 daemon 是一个封闭的实现别人想绕开它很难想替换它的某一部分更难。这些隐患叠加在一起拆分已经是必然结局只是时间问题。2. 拆分元年containerd、shim、runc 是怎么一步步长出来的2.1 从 libcontainer 到 OCI 标准Docker 团队很聪明的一点是他们在拆自己代码之前先拆了一个“标准”出来。2015 年Docker 牵头成立了 OCIOpen Container Initiative开放容器组织核心产物有两个runtime-spec 定义容器运行时的行为规范image-spec 定义镜像格式规范。为什么 Docker 愿意把自己定义的东西交出去变成一个中立标准原因很务实那时候 Docker 已经接近事实标准但“事实标准”和“开放标准”的区别在于前者是别人合作时要看你脸色后者是大家一起遵守同一套契约。Kubernetes 这类调度系统如果只能对接 Docker那 Docker 就变成了平台税会阻碍生态壮大。反过来如果把运行时接口和镜像格式标准化Docker 反而能借助整个生态的力量巩固自己在开发体验层的地位。libcontainer 随后脱胎成了 runc。runc 是一个独立的命令行工具是按照 runtime-spec 实现的参考运行时负责真正跟内核打交道创建 Namespace、设置 Cgroups、挂载 rootfs然后在隔离环境里启动容器进程。2.2 containerd 诞生把“管容器生命周期”这件事独立出去Docker 1.11 是架构史上的关键版本。从这一版开始Docker daemon 不再是容器的直接父进程中间插了一层 containerd。containerd 最初是 Docker 内部的组件负责镜像传输、通过 runc 管理容器生命周期、收集容器状态等。dockerd 不再直接 fork 容器进程而是把请求发给 containerd由 containerd 调 runc 去真正创建容器。这个过程用户几乎是感知不到的。docker run敲下去体验跟以前一模一样但背后的进程链已经变了。从架构角度看dockerd 终于在“业务功能”和“运行时功能”之间划了一条线镜像构建、网络管理、数据卷这些偏上层的东西留在 dockerd容器生命周期这种纯底层能力下沉到 containerd。后来 containerd 从 Docker 肚子里独立出来不再依赖 dockerd直接对接 Kubernetes。这个决定对整个云原生生态的走向影响极大后文再展开讲。2.3 shim 存在的意义很多人在拆 Docker 架构时容易忽略 containerd-shim 这个角色但它恰恰是这套设计里最精妙的一环。runc 是一个短生命周期的 CLI 工具。runc create执行完runc 进程就退出了容器主进程被创建出来但它需要有人继续照顾。谁来接住容器主进程的退出状态谁来保持 stdin/stdout/stderr 的通道不被关闭谁在 containerd 或 dockerd 需要重新启动的时候继续照看已经运行中的容器答案就是 containerd-shim。shim 是每个容器一个的常驻守护小进程它夹在 containerd 和容器主进程之间。runc 完成创建之后退出shim 接管容器进程的 stdio、上报状态、响应 containerd 的查询和操作请求。这个设计带来的一个直接好处是现在你升级或重启 dockerd甚至重启 containerd运行中的容器都不会因此失去“监护人”。这在单体 daemon 时代是完全做不到的。shim 就像每个工位上的值班组长换经理不影响车间正常运转。2.4 这条链路现在怎么跑把整条链路串起来看一次docker run大概是这样流转的docker CLI 把命令发给 dockerd。dockerd 解析配置、准备网络和卷然后调用 containerd 的 gRPC 接口。containerd 拿到请求后准备 rootfs、生成 OCI 运行时配置找一个空闲的 shim 启动作业。shim 调 runc 执行最终创建runc 与内核协商创建 Namespace 和 Cgroups在隔离环境中启动容器进程随后 runc 退出。shim 留在原地接管容器进程并持续向 containerd 汇报状态。注意这里面每一层都是独立的进程都有自己的职责边界。dockerd 不直接管理容器containerd 不直接创建进程shim 不参与业务决策runc 更像那个按一下开关就退场的执行器。这套分层逻辑到现在仍然是云原生容器运行时的主流范式。3. 当前 Docker 架构解剖你手里的 docker 命令到底在跟谁说话3.1 docker CLI薄薄的一层干的事比你想的多先说用户每天面对最多的 docker 命令。它是一个独立的二进制本身并不包含容器运行能力。它的工作是把用户的命令行参数翻译成对 dockerd 的 REST API 请求。但注意现代 docker CLI 不再只是“翻译官”。如果你用近几年的版本跑过docker build会发现构建过程走的是 BuildKit。BuildKit 是一套独立的镜像构建引擎有自己的 daemon 进程 buildkitd。docker CLI 会把构建任务交给构建器插件而不是让旧式 daemon 一步步处理 Dockerfile 指令。这是很多人版本的架构理解里缺失的一块镜像构建这件事已经从 dockerd 的核心能力中切分了出去。即便你用 docker CLI 操作远程 Docker 主机体验也完全一致。正因为 CLI 足够薄它成了跨环境最稳定的一层“用户接口”。3.2 dockerd从“万能管家”变成“业务调度”现在 dockerd 的职责比早期少了很多但依然不是可有可无的。它保留的是这些事作为 docker CLI 与容器生态之间的 API 入口、镜像构建的调度入口、网络管理bridge、overlay 这些 Docker 自己的网络模型、数据卷管理、Compose 和 Swarm 这类编排层面的支持。dockerd 自己已经不再“会”创建容器了。它把容器生命周期管理的活全部下发给 containerd。dockerd 此时更像一个业务中台接收用户的业务意图翻译成对下层的具体调用。这让 dockerd 的复杂度降了一大截也让独立替换底层运行时成为可能。如果你在服务器上只看进程dockerd 依然是最显眼的那一个。但现在它的存在更像一个“产品层”真正的运行时能力在它下游。3.3 containerd被拉出来单干后反而活得更好containerd 从 Docker 里独立出来以后路越走越宽。它现在是一个中立的容器运行时管理组件不仅 Docker 在用Kubernetes 节点也在用。很多生产环境的 K8s 集群甚至完全没有 dockerd只跑 containerd 和 runc照样完成全部的容器调度。containerd 自己也有命令行工具 ctr有 gRPC API有快照器管理容器的 rootfs有内容存储管理镜像层。它的功能边界大致是镜像传输与存储、容器生命周期管理、容器状态事件、运行时的插件化接入。比 dockerd 更聚焦比 runc 更完整正好处在“调度系统”和“内核接口”之间的黄金位置。对开发者来说从“Docker 是容器的代名词”到“Docker 只是容器入口之一”这个认知转变到今天已经不是观念问题而是基础设施事实。3.4 Docker Desktop 里的隐藏 VM再往上层看如果你在 macOS 或 Windows 上使用 Docker实际还有一个容易被忽略的结构Docker Desktop 并不是原生跑容器的它会在本机启动一个轻量级 Linux 虚拟机macOS 上可以是 Hypervisor.framework 方案Windows 上通常基于 WSL2 或 Hyper-V真正的 dockerd、containerd、buildkitd 都运行在那个 Linux VM 内部。Docker CLI 通过 Docker Desktop 暴露的 socket 跟 VM 里的 daemon 通信同时 Docker Desktop 负责文件系统挂载和端口转发让容器里的文件和网络在宿主机上看起来像本地的一样。所以如果你在 macOS 上ps aux | grep dockerd是看不到 dockerd 的它藏在 VM 里。很多初学者在这一层犯迷糊花了不少时间试图在 macOS 本机进程里找 containerd 和 runc结果一无所获。4. Moby 大重构和商业转身Docker 把江山分出去了4.1 Moby一个项目拆出两个开源名分2017 年是 Docker 项目史上一个绕不开的年份。这一年 Docker 把开源仓库重组命名为 Moby Project。Moby 是一套用独立组件拼装容器平台的工具箱里面包含 containerd、runc、buildkit、swarmkit 等多个组件任何人可以用 Moby 的组件自己拼一套容器系统。与此同时Docker 的商业产品 Docker CE 和 Docker EE 是基于 Moby 构建的但不再等于 Moby 本身。一句话理解Moby 是开放的拼图原料Docker 是拼好的官方成品。这个命名上的拆分本质上是把“工程社区”和“产品品牌”做了切割为后续的商业化铺路。4.2 containerd、runc 捐给中立组织带来的连锁反应同一时期Docker 做了一个影响深远但当时很多人没意识到后果的决定把 containerd 捐给 CNCF把 runc 的治理放在 OCI 框架下共同维护。这意味着什么意味着这两个云原生时代最底层的组件不再由 Docker 一家说了算。Docker 放弃了底层运行时的控制权换来的是整个生态的认可。这件事在当时看像是“割肉”回头看其实是“脱钩”Docker 不再跟调度器争夺平台地位而是死守开发者体验层。连锁反应很快出现。Kubernetes 生态开始把 containerd 作为首选运行时之一K8s 在 1.24 版本正式移除 dockershim 之后节点上跑没跑 dockerd 已经不影响容器调度了。今天的生产环境里大量 Kubernetes 集群只有 containerd 和 runcDocker 产品的名字连配置文件里都不出现。4.3 Swarm 与 Kubernetes 的插曲另一个值得回顾的事件是 Docker Swarm 与 Kubernetes 的路线之争。Swarm 是 Docker 官方的集群和编排方案一度被 Docker 视为原生杀手锏跟 Docker API 无缝集成学习曲线友好。但 Kubernetes 因为背靠更强大的调度模型、声明式 API、庞大的社区生态在生产集群编排之战中逐步占据上风。2017 年 Docker 宣布在 Docker 企业版中支持 Kubernetes 编排这个决定实际上已经默认了 Swarm 在集群编排市场中的地位下降。现在你打开 docker 命令docker swarm init依然可用会在一些轻量场景里看到它的影子但主流生产环境几乎已经没人用它做集群管理了。这段历史对理解“Docker 还是不是从前的 Docker”非常关键Docker 曾经想做完整的容器平台从开发到编排全覆盖但后来它把平台层放弃掉了专心做开发工具链和体验闭环。4.4 Docker 丢掉的和拿到的东西把账算一算Docker 丢掉的是唯一底层运行时地位、集群编排主导权、对 containerd 和 runc 的单方面控制权。拿到的是开发者心智的绝对入口、Dockerfile 事实标准、Compose 多容器编排标准、Buildx 多架构构建入口以及一个更聚焦的商业产品定位。这个取舍不一定是最好的但方向的清晰度在 2017 年之后越来越高。现在你再听到“Docker 已死”这类说法多半是把“Docker 作为底层运行时”和“Docker 作为开发生态”混为一谈了。Docker 在运行时层面确实不再独占但在开发者工作流层面它依然是事实标准。5. 实操视角用 docker version 和 ps 命令直接验证这套架构5.1 docker version 输出里藏着整个技术栈架构说得再多不如自己看一眼。在一台装了新版 Docker 的 Linux 服务器上执行docker version你会看到 Client 和 Server 两个大块。关键在 Server 部分$ docker version Client: Version: 26.x.x ... Server: Engine: Version: 26.x.x API version: 1.4x ... containerd: Version: 1.7.x runc: Version: 1.1.x注意 Server 下面除了 Engine还有 containerd 和 runc 两行。这就是最直观的架构证据Docker 这个产品已经从“一个 daemon”变成了“多个组件协同”的集合体。如果你手头是某个老版本Server 部分可能还看不到 containerd 的字段这本身就是 Docker 架构演进的一个小小注脚。5.2 docker info 里的运行时信息再看docker info。输出里有一堆字段其中这些值得格外关注Runtime 字段默认值通常是 runc这表示当前容器运行时使用的底层实现。Cgroup Driver一般是 systemd 或 cgroupfs它决定容器资源限制跟宿主机 cgroup 管理方式的对接方式。Security Options通常能看到 apparmor、seccomp 这些表示容器的内核安全能力是经过受限策略约束的。这些信息在你排查“为什么容器起不来”“为什么 cgroup 配置不生效”这类问题时非常有用。比如有一个常见现象宿主机用的是 systemd 管理 cgroup但 Docker 的 cgroup driver 还是 cgroupfs混用会导致资源限制出现奇怪的报错。在 K8s 节点上这个问题尤其常见kubelet 要求节点上的 cgroup driver 与容器运行时保持一致。5.3 从进程树看分层结构技术最扎实的验证方式是在宿主机上直接看进程树。找到任意一个正在运行的容器然后在主机上执行ps -ef | grep -E dockerd|containerd|shim|runc|容器进程名你会看到一个类似这样的层级dockerd 下面挂着 containerdcontainerd 下面挂着多个 containerd-shim每个 shim 下面是一个容器主进程。runc 通常看不到因为它是短生命周期进程创建完容器就退出了只在极端情况下以临时进程的形式出现。也就是说平时你以为“docker 启动了一个容器”的那个感觉在进程层面是两层导演加一个现场执行的结果。如果你在 K8s 节点上跑同样的命令大概率找不到 dockerd但 containerd 和 containerd-shim 的进程结构完全一样。这就是 Docker 当年把 containerd 捐出去的最强佐证离开了 Docker这套运行时链路照样运转如常。6. 一点个人体会我自己用了不少年 Docker从最早期那个“daemon 挂了全家停电”的时代一路用过来最大的感受是Docker 并没有变成另一个东西它只是把“能力”和“身份”分开了。能力还在而且更强了——镜像、构建、多架构支持这些依然是容器开发的核心入口身份却变了它从一个平台垄断者变成了生态连接者。如果有一天你在 K8s 节点上发现连 dockerd 都没有也不用惊讶。那不是 Docker 消失了而是 Docker 当年种下的组件长出了新的枝干。反过来只要你还写 Dockerfile、还敲 docker compose upDocker 就还是那个 Docker——只是它学会把重担分给不同的人扛。这篇文章先聊到这架构和组分的部分基本齐了。下一步我会把目光转向镜像构建的演变谈谈 Dockerfile 从 v1 到 BuildKit 再到 buildx 多架构构建这条暗线。到时候见。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑