资讯动态

从单体到模块化:Docker架构演进全面拆解

发布时间:2026/10/17 5:28:39 来源:尧图企业网站定制
看到这个标题的时候我愣了一下“它还是从前那个Docker吗”——这问题问得是真好啊。我从2016年就开始用Docker了那时候它几乎是“容器”的代名词一条docker run打天下。后来项目越做越多Kubernetes进来了containerd独立了CRI-O也出现了连Docker自己的构建链都换成了BuildKit。很多老朋友跟我说现在看Docker越来越陌生docker ps照用但底层那套东西早不是当年那个monolithic的大胖子了。这篇文章就想把Docker的架构彻底拆开给你看。我会从“它解决了什么问题”讲起再把经典架构里每一层——客户端、守护进程、containerd、runc、镜像存储、网络——逐个掰碎最后结合这些年的架构演进回答标题里那个问题今天的Docker到底还在不在。适合刚接触容器想建立整体认知的新人也适合用了很久Docker但没空深挖底层原理的开发者。看完你会明白一条docker run背后到底有多少个进程在替你干活。1. 容器为什么非火不可先看Docker解决的问题聊架构之前得先想明白一件事我们到底为什么需要容器我记得特别清楚早年间做交付最怕的就是“在我机器上是好的”。开发环境用Windows测试环境是CentOS生产环境是Ubuntu同一个Jar包、同一套配置到哪儿哪儿出问题。那时候主流的解法是虚拟机但虚拟机太重了每个虚机一个完整操作系统启动按分钟算资源开销大得肉疼。后来出现了Linux ContainersLXC能在共享内核的前提下做资源隔离但LXC用起来很别扭没有标准化的打包格式也没有好用的镜像分发机制。Docker真正的贡献不是发明了容器而是把容器做成了“能用的产品”。1.1 从“环境不一致”到“镜像即交付”Docker引入了一个非常关键的概念镜像Image。镜像把应用、依赖、系统库、配置文件全部打成一个不可变的包像集装箱一样。这个集装箱在开发者的笔记本上是什么样到服务器上还是什么样。过去我们要花半天时间写部署文档一个新同事照着文档折腾一天都跑不起来现在只需要docker run一条命令三分钟搞定。这个转变的本质是把“环境配置”这件极其容易出错的事从运维环节里彻底拿掉。你不再需要关心目标机器上装的是哪个发行版、有没有缺lib只要它有内核、能跑Docker就能跑你的应用。我自己的体会是Docker火起来的那两年团队里“环境问题”这个词出现的频率直线下降这就是它解决的最大痛点。1.2 Docker到底给了我们什么从产品角度看Docker这几件事是它当年脱颖而出的核心标准化的镜像格式和Dockerfile构建方式让“如何打包应用”变得有章可循分层镜像UnionFS使得镜像可以复用和增量传输拉一个基础镜像不同应用在上面叠不同层极大降低了存储和带宽成本一条命令同时搞定创建、启动、进入、停止、删除心智负担低到离谱Docker Hub这种公共镜像仓库解决了“镜像往哪放、别人怎么拉”的分发问题。但要真正理解Docker不能停留在命令行层面。你得看到一条docker run从敲下回车到容器跑起来中间发生了多少事情。这就要进入正题了。2. 经典Docker架构逐层拆解很多教程一上来就画一张架构图什么Client、Docker_Host、Registry然后就没有然后了。我打算换个讲法从你敲下命令的那一刻开始按执行路径一层层往下走看每一步是谁在干活。这是我觉得理解Docker架构最有效的方式。2.1 分层的整体架构client、daemon、containerd、runc经典的Docker架构是典型的C/S架构。你在终端敲的docker命令是客户端Docker Client真正干活的是后台的守护进程Docker Daemon也就是dockerd。客户端和守护进程通过REST API通信你自己也可以直接调这个APIdocker run本质就是往daemon的/containers/create接口发了个POST请求。这里的重点来了。很多人以为dockerd直接负责启动容器其实不是。从Docker 1.11开始Docker把容器执行的活儿拆出去了交给了containerd。再往下containerd也没亲自动手而是调用runc去真正创建和启动容器进程。我画一条链路你就清楚了docker CLI → REST API → dockerdAPI网关、镜像管理、网络管理、存储管理 → containerd容器生命周期管理、镜像解包、事件管理 → containerd-shim每个容器一个shim进程用于维持stdin/stdout等 → runc真正调用Linux内核namespace/cgroup创建容器进程runc是一个极其轻量的OCI运行时它做的事听起来很简单准备好rootfs、设置好namespaces和cgroups、然后execve启动容器里的进程。但恰恰是这一层让整个系统变得优雅。2.2 镜像与存储层、UnionFS与写时复制镜像层Layer是Docker最值得称道的设计。一个Dockerfile里的每条指令比如RUN apt-get install都会产生一个新层。这些层是只读的每一层都记录着和下一层的差异。多个层叠在一起通过UnionFS合并成容器看到的完整文件系统。这带来两个直接影响。第一镜像可以复用两个不同应用都基于同一个Ubuntu基础镜像那它们在磁盘上和网络上只需要存一份基础层。第二写时复制Copy-on-Write容器启动时不用把镜像整体拷贝一份而是在最上面叠加一个可写层。容器里改文件只是把要改的文件从只读层复制到可写层再修改这个设计让容器启动快到亚秒级。存储驱动Storage Driver就是实现这套机制的具体组件。早期最出名的是AUFS后来OverlayFS因为性能更好、更简洁成了绝对主流。Overlay2到现在依然是绝大多数Docker安装的默认驱动。2.3 网络与Volume容器不是孤立的小黑盒容器启动后它不是个与世隔绝的进程。Docker默认会创建一个docker0网桥每个容器通过veth pair接入这个网桥获得一个虚拟IP。这就是容器能互相通信、能被宿主机访问的基础。而Volume解决的是数据持久化问题。容器本身是无状态的容器删了可写层也没了。如果数据库就跑在容器里那删容器等于删数据。Volume相当于把宿主机的某个目录挂载进容器生命周期独立于容器这才是生产环境里数据应该待的地方。经典Docker架构的完整拼图大概是这样的Client下命令dockerd管资源containerd管容器runc真正拉起进程镜像靠分层存储网络靠网桥和veth数据靠Volume。这也是“从前那个Docker”的基本样貌。3. “从前的Docker”和“现在的Docker”架构演进对比现在可以正面回答标题里那个问题了。它还是从前那个Docker吗我的答案是从用户视角看它还是。docker run还是那样用Dockerfile写起来还是那个味道镜像概念也一脉相承。但从架构视角看它早就不是了。这些年Docker经历了两次重大拆分每一次都让它离“胖客户端”更远离“模块化基础设施”更近。3.1 一场拆分从monolithic到模块化早期的Docker是一个大单体程序一个进程里同时干了API处理、镜像管理、容器执行、网络配置所有事情。好处是部署简单一个docker daemon搞定全部。但坏处也很明显所有功能耦合在一起改一处可能会影响全局而且容器执行这部分本该是轻量级、高可靠的却不得不跟着整个daemon共存亡。想想看如果dockerd崩溃了正在运行的容器是不是也会跟着遭殃2015年前后Docker开始推动拆分这就是“从前的Docker”和“现在的Docker”最重要的分水岭。runc被拆出来作为独立的OCI容器运行时负责最底层的容器启停containerd被拆出来负责容器生命周期的上层管理包括镜像解包、容器的暂停/恢复/销毁等dockerd自己则专注于高层的编排、网络、存储、API。这个拆分还有一个关键产物containerd-shim。每个容器启动时containerd会为它创建一个shim进程容器主进程死后shim依然存活用来收集状态并向containerd汇报同时维持着容器的I/O通道。为什么需要shim因为runc是“一次性”的——它创建完容器主进程就退出了没人接管主进程的退出状态和输出容器就变成孤儿了。shim就是那个负责任务“善后工作”的监护人。3.2 OCI标准如何重塑了容器生态拆分背后是一套标准的诞生即Open Container InitiativeOCI。OCI定义了两份关键规范Runtime Specification容器运行时应该怎么实现包括文件系统打包、运行配置、生命周期等Image Specification镜像应该长什么样各层怎么组织、配置怎么存储。这两份规范最大的意义是让容器生态彻底解耦。以前“容器”几乎等同于“Docker”但现在任何人只要按OCI标准实现了运行时或镜像格式就能和生态无缝对接。runc是OCI运行时的一个参考实现但后来出现了很多其他实现比如性能更强的gVisor注重安全隔离、Kata Containers基于轻量虚机它们都能直接运行Docker或containerd托管的容器。镜像也一样现在你用别的工具构建出的OCI镜像照样能被docker run跑起来。可以说Docker当年把容器技术送进了标准化轨道然后自己变成了轨道上的一趟列车。这个选择看似“自降身段”实际上是让容器技术走向生产环境的必经之路。没有标准化就没有Kubernetes今天繁荣的容器运行时生态。3.3 BuildKit、WSL2、containerd独立之后除了运行时拆分Docker的构建链也变了。从Docker 23.0开始docker build默认使用的构建器是BuildKit。传统的构建器是一条指令一个层串行执行性能一般BuildKit支持并发执行构建步骤、更好的缓存复用、能直接构建OCI格式的镜像还支持不把整个构建上下文传给daemon。我实测的感受是依赖多的项目用BuildKit构建速度提升非常明显缓存命中率高一个量级。另一个大变化是Docker对macOS和Windows的拥抱方式——WSL2集成。早期Docker Desktop在Mac上靠的是HyperKit虚拟机和轻量Linux VM体验其实挺好但总隔了一层。后来在Windows上直接利用WSL2来跑Linux内核Docker Engine跑在WSL2里性能接近原生文件共享也顺畅很多。我自己用下来WSL2方案比早期VirtualBox方案爽了不知道多少倍。再就是containerd自身的发展。它后来被捐赠给了CNCF成了Kubernetes默认的容器运行时地位举足轻重。这个结果多少有点戏剧性当年Docker为了“瘦身”拆出来的部件如今成了云原生时代的地基。甚至很多人的机器上Kubernetes已经直接经由containerd来运行容器Docker已经不在调用链里了。所以你现在如果问“Docker还是从前那个Docker吗”答案真的有点复杂命令行还是那个味道但底下那套体系早就长成了整个云原生生态的一部分。4. 用命令亲眼验证架构变化光看架构图不过瘾我建议你自己在机器上跑几条命令亲眼看看现在这套体系长什么样。不夸张地说第一次看到真实进程树的时候我自己都惊了一下。4.1 docker version / docker info 读出了什么第一招用docker version看组件版本。注意输出是分段的不同版本输出格式有差异但Comunity版也好Engine版也好关键要区分客户端和服务端两部分。在较新的版本里你甚至能看到containerd和runc的版本信息。docker version --format {{.Server.Components}}在旧版Docker上会直接报错因为Server里没有Components字段。新版输出会列出Engine、containerd、runc的版本号——这就是模块化最好的证明一个接口背后的实现是由多个独立组件协同完成的。再看docker info这里有个很值得留意的字段Runtimes。docker info | grep -A 5 Runtimes老版本这个字段基本只有runc版本新一点的能看到io.containerd.runc.v2这是containerd插件化运行时架构的体现。如果你看到多个runtime列在那儿说明这台机器上已经不仅仅是“Docker的Docker”了。4.2 docker inspect 里的运行时细节当你有一个正在运行的容器最直接的观察路径是docker inspectdocker inspect $(docker ps -q) | less在输出里找Path和Args那是容器真正启动执行的命令。再找HostConfig里的Runtime字段确认容器是跑在哪个运行时之上的。接着看GraphDriver它的Name通常会显示为overlay2下面的LowerDir和UpperDir就是分层文件系统在宿主机上的真实目录。这些字段看起来枯燥全是指向存储和运行时机制的线索你的镜像层被打散存储在各个目录里容器启动时才由运行时把它们叠起来。4.3 进程视图下的真相如果上面的命令都不够直观那就直接看进程树。ps -ef | grep -E dockerd|containerd|shim|runc你大概率会看到这样一组进程root 1234 1 0 ... dockerd root 5678 1 0 ... /usr/bin/containerd root 9012 5678 0 ... containerd-shim -namespace moby -workdir ... -address /run/containerd/containerd.sock注意几个细节dockerd和containerd的父进程都是1init进程说明它们都是独立守护进程containerd-shim的父进程是containerd它是每个容器的“贴身管家”你还可能看到runc进程但更多时候容器主进程已经直接接管了。这套进程链条就是架构拆分后最真实的样子。你在宿主上找不到一个巨大的“Docker进程”包揽一切取而代之的是一群分工明确的进程协同干活。这不是退化这是工程上典型的解耦每个组件只做一件事但把这件事做好。5. 踩坑记录架构演进带来的常见问题架构变复杂了踩坑的面自然也就变了。这些年我在生产环境遇到过几个特别有代表性的问题写出来给大家排雷。5.1 /var/lib/docker 的清理与迁移/var/lib/docker是Docker所有数据的大本营包括镜像层、容器可写层、Volume、网络配置等等。这个目录膨胀极快尤其是镜像多、日志不打到stdout的应用。我见过最夸张的一次200GB的磁盘直接被干满了。问题是你不敢随便删里面的文件因为docker用的是分层存储目录之间是硬链接还是元数据关联普通用户根本分不清。正确做法是用docker system prune清理悬空镜像和停止的容器docker system prune -a --volumes这条命令会清掉所有未被运行的容器、未使用的镜像和匿名卷但也会误伤一些你想保留的构建缓存执行前先核对清楚。系统盘实在不够就要考虑迁移整个/var/lib/docker目录到数据盘修改daemon.json里的>docker exec container pkill -TERM -P 1 docker stop container如果已经删掉了容器但shim还赖着不走那就动手找一下残留进程的接管对象。正常情况shim会很快退出只有卡在某些I/O上时会变成僵尸。生产环境如果发现这种进程堆积优先检查是不是应用在容器里用了nohup或者daemonize这类操作——容器里不应该把进程“甩到后台”这正是容器和传统进程管理最大的不同。5.3 存储驱动迁移要谨慎早期AUFS时代很多老镜像的层是基于AUFS生成的后来Docker默认驱动切换到Overlay2部分老镜像在迁移时会报storage-driver相关错误。这也是很多从老版本升级到新版本的同学遇到过的问题。避免踩坑的要点升级Docker前检查daemon配置里的存储驱动设置确认目标驱动是否支持当前的内核。Overlay2要求内核版本支持overlayfs正常情况下主流发行版都没问题。但在一些老内核上可能需要手动加载overlay模块并在内核启动参数里加上overlayfs相关选项。我自己在给一台非常老的CentOS机器装新版Docker时就遇到过devicemapper和Overlay2不兼容的问题最后只能重装系统教训深刻。除非有非常明确的理由否则不建议在生产环境强行切换存储驱动。存储驱动一变所有镜像层都要重新拉取等于做了一次大手术能不动就不动。结尾回到标题那个问题它还是从前那个Docker吗我的看法是Docker的“灵魂”没变——容器、镜像、分层存储、一键运行这些核心思想在今天依然是它的底色。但它的“身体”早已天翻地覆从一个大单体拆成了client、dockerd、containerd、shim、runc各司其职的协同体系从私有标准走向了OCI开放标准从“容器的一切”变成了“容器生态的一个组件”。这篇文章先讲到这里重点是Docker自身的架构演进。下一部分我打算接着聊它和Kubernetes的关系——为什么K8s后来把Docker“踢出”了调用链containerd又是怎么一步步成为云原生时代的默认运行时以及这些变化对我们日常写Dockerfile、排障到底有什么实际影响。到时候见。

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

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

免费获取报价 →
↑