在容器技术发展的十余年间,Docker 的架构经历了数次重大演进。从最初单体化的 dockerd 守护进程,到如今 containerd 与 runC 的清晰分层,这一转变不仅是代码结构的重构,更是云原生生态对标准化与可移植性诉求的集中体现。理解这一演进脉络,对于基础设施工程师排查运行时问题、优化节点性能、以及在 Kubernetes 环境中做出正确的运行时选型决策,都具有重要的指导意义。
单体守护时代的架构困境
早期的 Docker 采用单体架构设计,dockerd 进程承担了从镜像管理、容器生命周期操作到网络配置的全部职责。这种设计在开发者快速迭代和容器概念普及的初期阶段发挥了重要作用,但随着容器规模的扩大和 Kubernetes 等编排系统的兴起,其局限性逐渐显现。
首先是耦合度过高的问题。dockerd 作为一个整体进程,任何子模块的升级或 Bug 修复都需要重启整个守护进程。对于承载数千个容器的 Kubernetes 节点而言,这意味着每次更新都可能触发大规模的服务中断。其次是接口协议的封闭性。dockerd 仅暴露了面向 Docker CLI 的 REST API,当 Kubernetes 的 kubelet 需要与容器运行时交互时,不得不通过 Dockershim 这一中间适配层进行协议转换,这不仅增加了系统复杂度,也延长了容器启动的端到端延迟。最后是资源效率的浪费。单体进程无法针对不同工作负载进行精细的资源隔离,某些只需要简单容器运行能力的场景,却不得不加载完整的功能堆栈。
containerd 独立:分层抽象的确立
2017 年 3 月,Docker 公司做出了一个影响深远的决定:将 containerd 剥离为独立项目并捐赠给云原生计算基金会(CNCF)。这一举动的核心动机是实现关注点分离,让 containerd 专注于容器运行时的核心职责,而将构建、分发、编排等能力交由上游工具处理。
从架构层面看,containerd 被定位为高级容器运行时(High-level Runtime),负责管理容器生命周期的完整流程,包括镜像拉取、存储管理、容器实例化、以及与低级运行时的协调。当开发者执行 docker run 命令时,请求首先由 Docker CLI 发送至 dockerd,随后 dockerd 通过 CRI 协议或内部接口将任务转交给 containerd。containerd 完成镜像解压和文件系统准备后,并不直接调用底层工具,而是通过一个关键的中间组件 ——shim 进程 —— 与 runC 交互。
这种分层设计带来了显著的架构优势。containerd 的升级可以独立于 Docker 其他组件进行,只要保持 OCI 运行时接口的兼容性,上游的 Kubernetes 无需感知底层运行时的变化。同时,清晰的边界划分使得针对特定场景的优化成为可能:边缘计算场景可以仅部署 containerd 和 runC,显著减少资源占用;而开发环境则可以保留完整的 Docker 工具链以获得更好的用户体验。
Shim 进程:解耦的关键设计
shim 进程是理解 Docker 架构演进的另一个核心概念。在 containerd 与 runC 之间引入这一中间层,并非简单的功能切分,而是解决了一系列工程实践中的痛点。
最直接的价值在于容器状态的独立追踪。当 containerd 将容器创建任务移交给 runC 后,runC 进程本身会退出 —— 这是 runC 的设计原则,它仅负责容器的启动,一旦容器运行起来,主进程应当直接由 init 系统管理。shim 进程的作用是保持容器标准输入输出(stdin/stdout/stderr)的管道活跃,使得即使 runC 已经退出,用户仍然可以通过 docker logs 命令获取容器日志。更重要的是,shim 进程维护了容器的主进程标识符,使得 containerd 能够在容器内部进程崩溃时进行准确的状态检测和恢复操作。
从资源消耗角度看,每个容器对应一个 shim 进程,这看似增加了开销,但实际上 shim 进程的内存占用极低(通常只有几百 KB),且其存在使得 containerd 无需维持与每个容器活跃 socket 连接的轮询,大幅降低了 kubelet 与容器运行时之间的通信负载。在大规模集群中,这种设计对于降低节点整体延迟具有可量化的收益。
OCI 规范与 CRI 标准的对齐
Docker 架构演进的另一个重要维度是对开放标准的拥抱。2015 年,Open Container Initiative(OCI)成立,旨在制定容器镜像格式和运行时规范的行业标准。runC 作为 OCI 运行时规范的参考实现,被 containerd 默认采用。这意味着符合 OCI 标准的镜像可以在任意兼容的运行时中运行,彻底打破了厂商锁定的风险。
在 Kubernetes 生态中,Container Runtime Interface(CRI)的引入进一步加速了这种标准化进程。CRI 在 Kubernetes 1.5 版本中正式提出,为 kubelet 与容器运行时之间定义了标准的 gRPC 接口。containerd 从 1.0 版本开始原生支持 CRI,这意味着 Kubernetes 节点可以直接与 containerd 通信,而无需再经过 Dockershim 的协议转换。自 Kubernetes 1.24 版本起,Dockershim 被正式移除,这一变化使得理解 containerd 与 Kubernetes 的交互机制成为运维人员的必备知识。
从实际运维角度,Kubernetes 1.26 及更高版本要求容器运行时支持 v1 CRI API。kubelet 通过 --container-runtime-endpoint 参数指定运行时地址,常见的配置值为 unix:///run/containerd/containerd.sock。在排查节点问题时,如果发现 Pod 一直处于 Pending 状态,首先应当检查 containerd 的 gRPC 服务是否正常监听在该 socket 路径上,以及 kubelet 进程是否具有访问该 socket 的权限。
实践中的关键参数与监控要点
对于在生产环境中运行 containerd 的团队,以下参数配置和监控指标值得特别关注。在 containerd 配置文件中,plugins."io.containerd.grpc.v1.cri" 下的设置直接影响 Kubernetes 的工作负载表现。containerd 默认启用的沙箱镜像拉取超时为 2 分钟,在网络条件较差的边缘节点上,建议将其调整为 3 至 5 分钟以避免 Pod 启动失败。shim 进程的默认内存限制可以通过 runc_options 参数进行调整,对于内存敏感型工作负载,适当收紧限制可以防止单个容器耗尽节点资源。
在监控层面,containerd 暴露了丰富的 Prometheus 指标。container_runtime_shim_running_tasks 指标反映了当前节点上活跃的 shim 进程数量,这一数值应当与节点上运行的 Pod 总数大致匹配。container_runtime_shim_healthchecks 指标记录了 shim 进程的健康检查失败次数,频繁的失败通常暗示着容器内部应用的异常退出或信号处理问题。image_fs_usage_bytes 和 image_fs_inodes_used 则用于监控镜像存储层的空间使用情况,当使用率超过 85% 时,应当触发镜像垃圾回收或存储扩容流程。
演进的意义与未来展望
Docker 架构从单体到分层的演进,本质上是容器技术从「单点工具」向「生态基础设施」转变的缩影。containerd 作为 CNCF 的毕业项目,其稳定性和社区活跃度已经得到了充分验证。runC 作为 OCI 标准的参考实现,持续接收来自全世界的安全审计和性能优化贡献。对于大多数团队而言,在 Kubernetes 环境中直接使用 containerd 作为容器运行时已经成为事实标准,而 Docker CLI 和 Docker BuildKit 则作为开发者工作流中的便捷工具存在,两者各司其职、相互补充。
理解这一架构演进,不仅有助于在故障排查时快速定位问题发生在哪一层,更能在技术选型和性能调优时做出更加明智的决策。当下,gVisor 和 Kata Containers 等安全容器运行时正在兴起,containerd 对这类运行时的支持同样遵循 OCI 接口规范,这为在多租户环境中运行不可信工作负载提供了可行的技术路径。
参考资料
- Docker 官方博客:containerd vs. Docker(2024 年 3 月)
- Kubernetes 官方文档:Container Runtime Interface(2025 年 10 月)
- containerd 项目版本与发布说明