Docker 时代架构:Docker Engine 详解与历史背景

在 Kubernetes 早期生态中,Docker 是唯一被广泛认可的容器运行时。理解 Docker Engine 的内部架构,是掌握容器运行时控制面演进的起点。本章深入剖析 Docker 时代的核心组件:dockerd、containerd-shim、runc,并分析 Docker 作为 Kubernetes 默认运行时的历史背景与局限性。

2.1 Docker Engine 架构详解

Docker Engine 在 1.11 版本之后经历了重大重构,从单体架构演变为由多个独立组件协作的模块化架构。其核心组件包括:

  • dockerd:Docker 守护进程,负责镜像管理、容器生命周期编排、网络与存储卷管理,对外暴露 REST API。
  • containerd:容器运行时管理守护进程,负责容器执行、镜像分发、存储管理,是 dockerd 与底层运行时的中间层。
  • containerd-shim:每个容器对应一个 shim 进程,作为 containerd 与 runc 之间的桥梁,确保容器退出后父进程仍能收集退出状态。
  • runc:OCI 运行时规范的标准实现,负责根据 OCI 规范创建和运行容器。
关键认知:Docker Engine 并非单一二进制,而是一组协同工作的组件。dockerd 是用户交互的入口,containerd 是核心管理面,runc 是实际执行者。这种分层设计为后续 CRI 接口的诞生奠定了基础。

2.1.1 组件交互流程

当用户执行 docker run 命令时,组件间的调用链如下:

  1. docker CLI 通过 REST API 向 dockerd 发送创建容器请求。
  2. dockerd 解析请求,调用 containerd 的 gRPC 接口进行容器创建。
  3. containerd 准备镜像、创建容器配置,并启动 containerd-shim 进程。
  4. containerd-shim 调用 runc 的二进制接口,基于 OCI 规范创建容器进程。
  5. runc 利用 Linux 内核特性(cgroups、namespaces)完成容器隔离与资源限制,启动容器内进程。
Docker Engine 组件交互流程 Docker Engine 组件交互流程 docker CLI dockerd containerd containerd-shim runc 组件职责说明 • docker CLI:用户命令行工具 • dockerd:REST API 守护进程 • containerd:容器管理中间层 • containerd-shim:容器生命周期桥接 • runc:OCI 运行时执行器

2.1.2 核心组件详解

组件 进程类型 核心职责 通信方式
dockerd 常驻守护进程 镜像管理、网络配置、存储卷、REST API 服务 REST API (HTTP)
containerd 常驻守护进程 容器生命周期管理、镜像分发、存储管理 gRPC
containerd-shim 每个容器一个进程 桥接 containerd 与 runc,收集退出状态,保持容器标准流 Unix Socket / 进程间通信
runc 短暂执行进程 基于 OCI 规范创建容器、设置 cgroups/namespaces 命令行调用
架构亮点:containerd-shim 的设计是 Docker 架构的关键创新。它使得容器可以在 dockerd 或 containerd 重启后继续运行,因为 shim 进程独立于父进程存在。这一设计后来被 CRI 接口继承,成为容器运行时高可用的基础。

2.2 Docker 作为 Kubernetes 默认运行时的历史背景

Kubernetes 项目于 2014 年由 Google 开源,当时容器生态中 Docker 占据主导地位。Kubernetes 选择 Docker 作为默认运行时,主要基于以下历史原因:

  • 生态成熟度:Docker 提供了完整的容器生命周期管理工具链,包括镜像构建、分发、运行,开发者社区庞大。
  • 用户习惯:早期 Kubernetes 用户几乎都是 Docker 用户,复用 Docker 工具链降低了学习成本。
  • 快速验证:Kubernetes 团队需要快速验证编排系统设计,直接对接 Docker API 是最短路径。
  • 网络与存储集成:Docker 提供了成熟的网络(libnetwork)和存储(Volume Driver)插件机制,Kubernetes 可直接复用。

Kubernetes 早期通过 kubelet 内置的 DockerManager 直接调用 Docker API 来管理容器。这种方式虽然简单直接,但存在明显的架构耦合问题。

Kubernetes 早期直接调用 Docker API 架构 Kubernetes 早期直接调用 Docker API 架构 kubelet DockerManager dockerd REST API containerd + runc 架构耦合问题 • kubelet 直接依赖 Docker API,无法替换其他运行时 • 每次 Docker 版本升级都可能影响 Kubernetes 集群稳定性 • 容器运行时逻辑与编排系统逻辑高度耦合,不利于扩展

2.3 Docker 作为运行时的局限性

随着 Kubernetes 生态的快速发展,Docker 作为默认运行时的局限性逐渐暴露:

局限性 具体表现 影响
API 耦合 kubelet 直接调用 Docker API,无法接入其他运行时 生态封闭,无法支持 Kata Containers、gVisor 等安全容器
版本依赖 Docker 版本升级可能破坏 kubelet 兼容性 集群升级风险高,运维复杂度增加
性能开销 Docker 的完整工具链带来额外内存和 CPU 消耗 大规模集群资源利用率下降
功能冗余 Kubernetes 不需要 Docker 的镜像构建、docker-compose 等功能 不必要的功能增加了攻击面和维护成本
标准化缺失 Docker API 并非行业标准,不同运行时实现各异 阻碍容器运行时生态的多元化发展
关键转折点:2016 年,Kubernetes 社区意识到直接依赖 Docker API 的架构不可持续。同年,Kubernetes 推出了 CRI(Container Runtime Interface),旨在定义一套标准化的容器运行时接口,彻底解耦 kubelet 与具体运行时实现。这一决策直接推动了 containerd、CRI-O 等轻量级运行时的诞生,也标志着 Docker 作为 Kubernetes 唯一运行时的时代走向终结。

2.3.1 从 Docker 到 CRI 的演进路径

Kubernetes 社区为解决上述局限性,采取了以下演进路径:

  1. CRI 接口定义:在 kubelet 与容器运行时之间定义标准 gRPC 接口,包括 RuntimeService(容器生命周期)和 ImageService(镜像管理)。
  2. Docker 适配层(dockershim):在 kubelet 内部实现 CRI 到 Docker API 的转换,作为过渡方案。
  3. 原生 CRI 运行时:containerd 和 CRI-O 等运行时原生实现 CRI 接口,无需适配层。
  4. dockershim 移除:Kubernetes 1.24 版本正式移除 dockershim,标志着 Docker 作为默认运行时的时代彻底结束。
历史启示:Docker 时代架构的核心价值在于验证了容器编排的可行性,但其架构耦合问题也推动了标准化接口 CRI 的诞生。理解这段历史,有助于我们把握容器运行时架构设计的核心原则:解耦、标准化、可替换。