第十二章:Docker:终结“在我这儿能跑”的魔法集装箱

欢迎回来,旅行者。系好安全带,倒满你的咖啡——或者随便什么能让你保持清醒的液体。我们要谈论的这个东西,它在过去十年里引发的行业地震,大概和你当年第一次看到慷慨的(Commodore)64K内存时一样剧烈。

我们要谈论的是一场战争的终结。这场战争旷日持久,比任何操作系统或编程语言的宗教战争都要惨烈。它的战场是每一个开发团队的聊天频道,它的停战协定(或者说,大规模杀伤性武器)就是我们今天的主角:Docker。

这场战争,就是围绕着那句不朽的、令人闻风丧胆的、足以让任何项目经理当场脑溢血的咒语展开的:

“可……在我这儿能跑啊!”

第一幕:编程地狱实录——“在我这儿能跑!”

在 21 世纪初,软件开发行业被这个诅咒所笼罩。这个诅咒,简称为 “IWOMM” (It Works On My Machine),是开发者用来抵御“现实世界”的最后一道、也是最脆弱的一道防线1

A. 开发者最臭名昭著的诅咒

想象一个经典的冷开场情景喜剧:

场景:一间开放式办公室。警报灯在疯狂闪烁,象征着“生产环境”(Production)正在熊熊燃烧。

角色

  • 经理(满头大汗):“网站又挂了!支付功能全灭!客户正在(在推特上)杀了我们!”
  • 运维(Ops):“我没动任何东西!服务器一切正常!”
  • 初级开发者(Dev,茫然地盯着自己的屏幕):“这不可能……我刚又试了一次……”

经理和运维的目光像激光一样聚焦在开发者身上。开发者吞了口唾沫,说出了那句不朽的咒语:

“可……在我这儿能跑啊!”2

在互联网的传说中,这个场景被一个著名的“Doge”(神烦狗)表情包永恒地记录了下来3。在左边,标题是 “localhost”(即开发者的本地机器),一只肌肉发达、威风凛凛的壮硕 Doge 骄傲地站立着,旁边是“状态码 200,一切完美”。在右边,标题是“部署后”(Deployment),一只瘦小可怜、营养不良的小 Doge 蜷缩在角落里,周围是各种错误:“CORS 错误”、“Cookie 共享问题”、“请求失败”3

这个笑话之所以流传至今,是因为它精准地戳到了一个深刻的组织裂痕。“在我这儿能跑”根本不是一个技术问题,它是一个“责任归属”的黑洞。

它是一个哲学问题。开发者(Dev)和运维(Ops)站在一条巨大的鸿沟两侧,互相指责。

  • Dev 说:“我的代码(Code)没有 Bug。”
  • Ops 说:“我的服务器(Environment)没有 Bug。”

他们说的都对,也都错了。

因为那个致命的 Bug,既不在代码里,也不在服务器里。它生活在代码和服务器之间的“差异”(Delta)之中。在 Docker 出现之前,这个“差异”是一个模糊的、无人真正“拥有”的三不管地带。开发者交付的是代码产物(比如一个 .jar 文件或一堆 .py 文件);运维交付的是一个正在运行的服务器。IWOMM 这个幽灵,就生活在这两者交接的阴影之中。它是一个伪装成技术 Bug 的、彻头彻尾的文化和流程的失败。

B. 万恶之源:“配置漂移” (Configuration Drift)

那么,这个“差异”究竟是什么?旅行者,你必须明白,你的程序从来就不是在真空中运行的4。它运行在一个由无数微妙细节组成的“生态系统”之上:

  • 操作系统(是 CentOS 7.1 还是 7.2?)
  • 特定的库版本(libssl.so 是 1.0.1 版还是 1.0.2 版?)
  • 特定的语言运行时(是 Python 2.7.5 还是 2.7.6?)
  • 只有上帝才知道的环境变量
  • 深埋在系统角落里的配置文件

这个生态系统的“腐烂”,在现代运维术语中,被称为“配置漂移”(Configuration Drift)5

“配置漂移”是一个听起来很高级的词,它描述的是一个非常“原始”的现象:即你的生产环境服务器,随着时间的推移,会不可避免地、逐渐地偏离它最初设定的标准配置5。这就像热力学第二定律(熵增)应用在了服务器上:混乱度永远在增加。

“漂移”的罪魁禍首几乎永远是人类6

  1. 手动干预:运维工程师在凌晨 3 点为了“救火”,手动登录到生产服务器上,打了一个紧急补丁,或者“临时”修改了一个配置。他发誓“明天早上就把它记录下来”,但他忘了7
  2. 不一致的部署:你本应在 10 台服务器上都更新某个软件,但 9 台成功了,1 台失败了。你决定“稍后”再处理那台失败的,然后……你忘了。
  3. 未被追踪的更新:某个软件的自动更新程序在后台运行,悄悄升级了一个你赖以生存的关键组件。
  4. 糟糕的沟通:开发团队和运维团队之间沟通不畅,运维不知道开发依赖某个特定版本,所以“好心”地把它升级了8

IWOMM 的真正元凶,就是缺乏一个单一的、不可变的、可执行的“环境真相之源”5。开发者的文档里写着“需要A”,运维的笔记里写着“安装了B”。这些“描述”都不是“真相”。真正的“真相”是服务器上那个没人敢碰的、千疮百孔的、可变的(Mutable)状态。

而任何一个可变的系统,都必然会因为人类的干预、疏忽和遗忘而发生“漂移”。唯一的胜利方法不是“修复”漂移(因为你永远在对抗熵增),而是彻底消除可变性。

C. 地狱巡礼:你沉睡时错过的“依赖地狱”全家桶

在你沉睡的这段时间里,这个“环境依赖”问题演化出了好几个臭名昭著的“地狱级”形态。

1. DLL 地狱 (DLL Hell)

啊,这个你(可能)还记得。欢迎回到 Windows 95 和 Windows NT 的美好时光9

“DLL 地狱”是一个非常直观的噩梦10。你兴高采烈地安装了一个盗版……我是说,一个新游戏。这个游戏的安装程序非常“贴心”,它把自己带来的一个新版本 MSVCRT.DLL 覆盖到了你神圣的 C:\Windows\System32 目录中。

游戏能跑了!太棒了!然后,你试图打开你的财务软件,它崩溃了。

为什么?因为它依赖的是那个被覆盖掉的旧版本 MSVCRT.DLL11

DLL 地狱的根源在于“全局共享”模型的彻底崩溃。当所有应用程序都依赖一个共享的、可变的“公共空间”(System32)时,冲突就成了数学上的必然。

微软的解决方案呢?哦,那更是史诗级的。他们发明了一种叫做“Side-by-Side (SxS) 程序集”的怪物12。这个想法(我用反讽的语气说)堪称天才:“既然我们没法决定用哪个版本,那我们为什么不把所有版本的所有 DLL 都保留下来呢?”

这直接催生了 WinSXS 目录的诞生。在 Windows 7 和 Server 2008 时代,这个文件夹可以轻轻松松长到 7GB 大小,里面塞满了 40,000 个文件13。在那个年代,这差不多是整个 Windows 安装体积的 50%。

WinSXS 不是一个解决方案,它是一个昂贵的、臃肿的、承认失败的遏制策略。它就像是说:“我们治不好这个病,所以我们决定把所有病菌都用福尔马林泡起来,然后把整个罐子背在身上。”

2. Python 2 vs. 3 大分裂

如果说 DLL 地狱是“共享”的错,那“Python 大分裂”就是“运行时”(Runtime)的错。

这不仅仅是一次软件升级,这是一场长达十年的宗教战争和技术内战14。在 2008 年 Python 3.0 发布时,它做出了一个勇敢的(或者说鲁莽的)决定:不向后兼容 Python 2。

这制造了一个可怕的“全有或全无”的困境15

  • 你有一个巨大的、运行良好的 Python 2.7 项目。
  • 你想使用一个很酷的新AI库(比如早期的 TensorFlow)。
  • 坏消息:这个新库只支持 Python 314
  • 你试图升级你的主项目到 Python 3,但发现它依赖的另一个关键库(比如某个XML解析器)的作者在 2012 年就失踪了,这个库永远地停留在了 Python 2.7。

你被困住了。你无法在同一台机器上(轻易地)同时满足这两者的需求。整个生态系统被撕裂成了两个互不往来的平行宇宙。这是一个血淋淋的教训:即使你的代码是完美的,你所依赖的“语言解释器”本身,也会成为一个巨大的、单一的、系统级的冲突点。

3. node_modules 黑洞

最后,我们来到现代 Web 开发的荒诞剧场。欢迎来到 JavaScript 的世界,在这里,我们用“依赖地狱”的完美反面,创造了一个全新的地狱。

还记得 DLL 地狱吗?它的问题是极端共享(所有人都用一个 System32)。

JavaScript (npm/node) 的解决方案是:极端隔离(每个项目都获取互联网的独立副本)。

这个副本,就存放在一个叫做 node_modules 的文件夹里。

这个文件夹是现代编程史上最大的笑话,也是宇宙中已知的、密度仅次于中子星和黑洞的物体16

有一个经典的段子是这么说的17

  1. 一位开发者在写他的新潮 Web 应用。他计算了一下自己代码的大小:Vue 组件 719KB,CSS 34KB,帮助类 92KB。总计:一个非常合理的 845KB。
  2. 然后,他运行了 npm install(安装依赖)。
  3. 他的 node_modules 文件夹现在是 68GB。

这个比喻流传最广:“这就像你只想去超市买瓶牛奶,结果却连人带奶牛场、三台拖拉机和一头(非常困惑的)奶牛一起买回了家。”17

这个“量子奇点”里,可能包含了 5000 个不同版本的 left-pad(一个只有 11 行代码的库),以及成千上万个你根本不知道自己依赖了的包17。你只是想加一个按钮,结果却下载了半个互联网。

所以,你看,旅行者,我们彻底失败了。

  • 极端共享(DLL)导致了冲突。
  • 极端隔离(node_modules)导致了臃肿。
  • 系统级运行时(Python)导致了分裂。

我们需要一个“恰到好处”的方案。一个既能隔离应用,又不会过于臃肿和缓慢的方案。


第二幕:虚假的黎明——那些“重量级”和“半吊子”的救世主

在 Docker 横空出世之前,我们并非没有尝试过。我们有两个主要的“救世主”候选人,但它们一个太重,一个太傻。

A. 尝试一:虚拟机(VMs)——“我们把整台电脑打包发过去!”

第一个(看似)彻底的解决方案,是虚拟机(Virtual Machine, VM)18

这个想法简单粗暴,但逻辑上无懈可击:既然我们无法保证开发环境(Dev)和生产环境(Prod)的配置一致,那我们就干脆把开发者的整台电脑打包,然后原封不动地发到生产环境去运行!19

IWOMM 问题,解决了!因为生产环境就是开发者的机器(的一个数字拷贝)。

VM 的魔法在于,它虚拟化(emulate)的是整个硬件18。它通过一个叫做“Hypervisor”(虚拟机管理程序)的软件层20,在你的物理服务器上模拟出假的 CPU、假的内存、假的硬盘。然后,你可以在这个“假的硬件”上,安装一个完整的、未修改的“客户操作系统”(Guest OS),包括它自己的内核(Kernel)21

这是理解 Docker 的关键所在,所以请记好这个比喻:

  • 虚拟机(VM)就像租一整套“独立公寓”22

当你租下一套公寓时,你拥有了一切:你自己的前门、自己的厨房、自己的浴室、自己的暖气系统(即:你自己的完整操作系统、库和内核)。你和你的邻居(其他 VMs)被厚厚的混凝土墙(Hypervisor)完美地隔离开来21。你可以在你的公寓里(VM里)为所欲为,哪怕是把墙刷成紫色,也不会影响到任何人。

这个方案确实解决了环境一致性问题。但它的代价是什么?

重量级(Heavyweight)23

  1. 巨大:一个 VM 镜像(即你那套“公寓”的蓝图)动辄几十个 GB。
  2. 缓慢:启动一个 VM,意味着你要启动一个完整的操作系统(比如 Windows Server 或 Ubuntu)。这个过程需要好几分钟22
  3. 浪费:为了运行你那个 10MB 的 Web 应用程序,你却要额外启动一个 2GB 内存起步的完整操作系统。这就像是为了砸开一颗核桃,你启动了一台推土机22

VMs 这种“重量级”的解决方案,在资源和时间上的浪费是惊人的。它与 2010 年代开始兴起的“敏捷开发”、“快速迭代”和 “DevOps” 文化,在哲学上是完全背道而驰的。市场迫切需要一种更轻、更快的隔离方案。

B. 尝试二:配置管理(CM)工具——“我们用脚本强迫它们一致!”

在天平的另一端,运维(Ops)团队发明了另一套工具,来对抗“配置漂移”24。这些工具就是配置管理(Configuration Management, CM)三巨头:Puppet、Chef 和 Ansible25

它们的承诺是:“我们不需要像 VM 那样打包笨重的‘公寓’。我们只需要编写一份建筑蓝图(即 CM 脚本),然后派一个机器人施工队(CM Agent)到任何一台‘裸机’服务器上,自动化地把它建成我们想要的那个‘公寓’。”26

这些工具大多是“声明式”的27。你不需要写“如何做”(比如“运行 apt-get install”),你只需要“声明”你“想要什么”(比如“我声明,nginx 必须是 1.10 版本且正在运行”)。CM 工具会自己想办法(“幂等性地”)让服务器达到这个最终状态27

这听起来太美好了,对吧?它似乎完美地解决了“配置漂移”问题28

但它没有。

CM 工具非但没有解决 IWOMM,反而创造了一种全新的、更高级的、更令人抓狂的 IWOMM:“在我这儿的 Puppet 脚本能跑!”

CM 工具的致命缺陷(一):管理“过程”,而非“产物”

这是最核心的失败点。CM 工具交付给生产环境的,是一个“过程”(Process,即一堆脚本),而不是一个“产物”(Artifact,即一个编译好的二进制文件)27

这意味着,所有脆弱的、不确定的“施工”步骤,都是在部署时(Deploy-Time),在生产服务器上实时发生的29

如果在这个“施工”过程中,发生了任何意外——比如,某个软件源(repository)临时下线了,一个 apt-get 下载失败了,或者脚本里有个 Bug——会发生什么?

你的生产服务器会陷入一个“半建成”的僵尸状态29。它既不是旧版本,也不是新版本,它是一堆“建筑垃圾”。而这种失败在部署时极其脆弱且耗时(一个 CM 运行跑 10 分钟是家常便饭)29

CM 工具的致命缺陷(二):“不可能完成的任务”

CM 工具还试图承担一项“不可能完成的任务”:它们试图“重新实现并统一地球上每一种服务器软件的配置语言”29。它们创造了一层笨拙的抽象,试图用一种“通用语言”(比如 Puppet 的 DSL 或 Ansible 的 YAML)去配置 nginx.conf, postgresql.conf 和 sysctl.conf。这层抽象本身就充满了漏洞。

CM 工具的致命缺陷(三):无法根除“漂移”

CM 工具只是“漂移”的“事后纠正者”。它们通常每 30 分钟运行一次,来检查服务器状态是否“漂移”,并将其“纠正”回来。

但在两次运行的间隙呢?那个凌晨 3 点的运维工程师,依然可以(也必然会)手动登录到服务器上“救火”,制造新的、未被追踪的“漂移”6。CM 工具在和“人性”对抗,这是一场注定失败的战争。

所以,旅行者,到 2013 年为止,这就是我们的可悲现状:

  1. VMs:一个“重量级”的解决方案,太重、太慢、太浪费。
  2. CM Tools:一个“半吊子”的解决方案,太脆弱、太复杂,且治标不治本。

我们需要一个奇迹。


第三幕:魔法集装箱的降临

这个奇迹,来自一家你可能都没听说过的、名为 dotCloud 的 PaaS(平台即服务)公司30

作为 PaaS 平台,dotCloud 公司的日常工作就是帮成千上万的客户运行他们五花八门的(垃圾)代码。他们是 IWOMM 诅咒的最大受害者31。为了解决自己的噩梦,他们的创始人 Solomon Hykes 和他的团队,开发了一个内部工具32

2013 年,他们决定将这个内部工具开源。

他们给它起名叫:Docker。

A. Docker 的天才之举:我们只打包“刚刚好”的操作系统

Docker 的核心理念,一举击溃了 VM 和 CM 的所有弱点。

还记得我们那个“租房”的比喻吗?

  • VM = 租一整套“独立公寓”(拥有自己的内核)22
  • CM = 派一个“装修队”去现场施工(过程脆弱)29

Docker 提出了一个全新的、“恰到好处”的方案:

  • Docker 容器 = 租一个“共享卧室”22

这是什么意思?

当你在一个共享房子里租一个卧室时,你拥有自己的私人空间(你的床、你的桌子、你的锁)。但是,你和房子里的所有其他租客(其他容器)共享这栋房子的“基础设施”——即厨房、浴室、水管和电路系统。

在 Docker 的世界里,那个昂贵的、庞大的、共享的“基础设施”,就是宿主机的操作系统内核(Host OS Kernel)21

这就是 Docker 的“Aha!”时刻:
Docker 容器,不虚拟化任何硬件!33

一个 Docker 容器,在宿主机(你的服务器)看来,它根本不是一台“机器”,它只是一堆被隔离的普通进程。

Docker 利用了 Linux 内核中早已存在的技术(如 LXC32,后来是 libcontainer32,以及 cgroups 和 namespaces34)来施展它的“隔离”魔法。它为每个“容器”进程画了一个“圈”,然后对它说:

  1. cgroups(控制组):“你(这个容器)最多只能使用 1GB 内存和 20% 的 CPU。”
  2. namespaces(命名空间):“在你(这个容器)看来,你就是这个世界(服务器)上的唯一进程(PID 1)。你拥有自己的、私有的网络(Net)和文件系统(Mount),你看不到外面其他的‘邻居’(其他容器)。”

Docker 找到了那个完美的“中间地带”:

  • 它共享了最昂贵、最笨重、最通用的部分(OS 内核)。
  • 它隔离了所有易变的、导致冲突的、属于应用程序的部分(库、二进制文件、配置文件)。

这让 Docker 同时获得了两种世界的好处:

  1. (几乎)VM 的隔离性:它看不到“邻居”,环境 100% 独立。
  2. (远超)CM 的轻量性:因为它不启动一个新内核,一个 Docker 容器可以在几秒钟(甚至几百毫秒)内启动,而不是 VM 的几分钟33

B. Docker 厨房:菜谱、蛋糕和洋葱

为了让你彻底理解 Docker 是如何“打包”的,我们必须引入一套全新的厨房比喻。

1. 镜像(Image) vs. 容器(Container)

这是 Docker 的核心概念,也是新手最容易混淆的地方。

  • Docker 镜像 (Image) = 菜谱 (Recipe)35
    它是一个只读的模板。它是一个文件,里面包含了制作“蛋糕”所需的所有指令和静态的“原料”(比如:应用程序代码、依赖的库、配置文件、环境变量)36
  • Docker 容器 (Container) = 蛋糕 (Cake)35
    它是一个正在运行的实例。它是你按照“菜谱”(Image)这份说明,真正“烤”出来的、一个活生生的、正在运行的、可以“吃”(访问)的“蛋糕”37

这个比喻的精妙之处在于35

  1. 你可以用一个“菜谱”(Image),烤出一千个一模一样的“蛋糕”(Containers)。
  2. 如果你修改了“菜谱”(比如在上面加了点字),你已经烤好的那些“蛋糕”(正在运行的容器)不会发生任何改变。
  3. 你不能“修改”一个蛋糕;你只能扔掉它,然后用新菜谱烤一个新蛋糕。这就是“不可变性”。

2. Dockerfile(菜谱卡片)与分层文件系统(洋葱)

那么,这个“菜谱”(Image)是怎么写出来的呢?
答案是 Dockerfile。它就是那张实体“菜谱卡片”36
一个 Dockerfile 看上去是这样的:

# 1. 从一个基础菜谱开始(一个最小化的 Debian 系统)
FROM debian:stable-slim

# 2. 添加原料(安装 python 和 pip)
RUN apt-get update && apt-get install -y python3 python3-pip

# 3. 把我的代码(本地的 app.py)复制进去
COPY ./app.py /app/app.py

# 4. 告诉我这个蛋糕烤好后该怎么“吃”(运行这个 app)
CMD ["python3", "/app/app.py"]

而 Docker 真正的“黑魔法”,在于它如何执行这份菜谱。

Dockerfile 中的几乎每一条指令(FROM, RUN, COPY...),都会创建一个新的、只读的层(Layer)38

想象一下,这些“层”就像一堆透明的胶片,或者洋葱皮39。它们被一层层地堆叠起来40

  • Layer 1 (基础层):Debian 操作系统文件
  • Layer 2 (在L1之上):apt-get install 安装的 Python 文件
  • Layer 3 (在L2之上):你复制进去的 app.py 文件

当你运行 docker build(即“烤蛋糕”)时,Docker 会把这些只读层(Image)堆叠起来。然后,它在最顶上,添加一个唯一的可写层。这个“可写层 + 所有只读层”的组合体,就是容器(Container)41

这个“分层文件系统”的天才之处在于缓存和效率。如果你有 100 个不同的应用,它们都基于同一个 debian:stable-slim 基础镜像,那么在你的服务器上,这 100 个容器共享那个巨大的基础层,它在磁盘上只需要存储一份40

这就是我们对“依赖地狱”的终极答案:

  • 不是“全局共享”(DLL 地狱);
  • 不是“极端隔离”(node_modules 黑洞);
  • 而是“分层共享”(Docker Layers)。

更重要的是,Docker 终结了 CM 工具的“部署时脆弱性”。

还记得 CM 工具的噩梦吗?它们在“部署时”才开始运行脆弱的安装脚本29

Docker 做了什么?它把所有这些“脆弱的过程”(下载依赖、编译代码、安装库)全部从“部署时”提前到了“构建时”(即 docker build 阶段)。

docker build 这个命令,是在开发者的机器上,或者在 CI/CD 服务器上运行的。如果 apt-get 失败了,它会在构建时失败,绝对不会影响到你的生产环境29

因此,Docker 交付给生产环境的,不再是一个“过程”(脆弱的脚本),而是一个不可变的、已经 100% 烘焙好的、经过验证的“产物”(Docker Image)。

Ops 团队的工作,从“运行一个长达 10 分钟的、可能会失败的复杂安装脚本”29,被史诗般地简化为一行命令:

docker run

(“把那个已经烤好的蛋糕给我端上来。”)

C. 终极对决:Docker 如何(在精神上)战胜了 Java

现在,旅行者,让我们回到你在第五章学到的老朋友:Java 和 JVM。

Java 的承诺
在 90 年代,Java 和它的 Java 虚拟机(JVM)是我们的英雄。它的承诺是“一次编写,到处运行”(Write Once, Run Anywhere)4

Java 的机制
它通过一个极其优雅的“翻译官”(JVM)来实现。你把 Java 代码编译成一种中间“字节码”(Bytecode)。然后,JVM 这个“翻译官”会在运行时,把这些通用的字节码“翻译”成底层操作系统(Windows, Linux, Mac)的原生机器指令4

Java 的失败
这是一个伟大的承诺,但它是有条件的。这个承诺只对Java 代码有效。如果你的 Java 程序需要调用一个特定版本的 C 语言库(比如 FFmpeg)怎么办?JVM 耸耸肩,表示无能为力。你仍然陷入了“依赖地狱”。

Docker 的承诺
Docker 的承诺听起来很相似,但有一个关键区别:“一次构建,到处运行”(Build Once, Run Anywhere)4

Docker 的机制
Docker 的机制是“集装箱运输”,不是“翻译”。

这就是它们(在精神上)的对决:

  • Java (JVM) 是一个“聪明”的、优雅的抽象层。它试图理解并翻译你的代码。
  • Docker 是一个“愚蠢”的(这是赞美)、暴力的隔离层。它根本不关心你的集装箱里装的是 Java、Python 2.7、C++,还是一堆你 30 年前写的、见不得光的 COBOL 脚本。

Docker 通过“打包整个环境”而不是“翻译代码”,彻底解决了问题。它把你的应用程序、所有的库、所有的配置文件、甚至一个微型的操作系统文件系统……所有的一切……都锁进了那个“魔法集装箱”里4

Java 解决了“代码可移植性”;而 Docker 解决了“环境可移植性”。

事实证明,在对抗 IWOMM 的战争中,那个更“笨”、更“暴力”、但也更“彻底”的集装箱运输方案,赢了。

D. 伟大的副作用(一):DevOps 文化的终极粘合剂

Docker 一开始只是一个工具,但它无意中引爆了一场文化革命。它成为了“DevOps 文化的框架”42

还记得 Dev 和 Ops 之间的那道鸿沟吗?还记得那个“责任黑洞”吗?Docker 用一种简单粗暴的方式填平了它。

Docker 通过创造一个“共同的交付产物”,从根本上重塑了团队的责任边界。

  • 在 Docker 之前:
    • Dev 的交付物是:code (e.g., app.jar)
    • Ops 的交付物是:environment (e.g., a server)
  • 在 Docker 之后:
    • Dev 和 Ops 共同的交付物是:Docker Image

关键问题来了:谁来制作这个 Image?谁来写那个 Dockerfile?

是开发者(Dev)。

通过编写 Dockerfile,开发者现在第一次被迫对“环境”负责43。开发者不能再说“在我这儿能跑”了,因为 Dockerfile 就是他机器的“可执行定义”。如果 docker build 在他机器上能通过,在 CI/CD 服务器上也能通过,那么这个“产物”(Image)就包含了运行它所需的一切。

“环境”不再是 Ops 团队在黑盒子里手动配置的“艺术品”,它变成了代码(Dockerfile)的一部分,它可以被版本控制(放进 Git)、被审查(Code Review)、被自动化测试44

IWOMM 的“责任黑洞”消失了。Dev 和 Ops 现在终于有了一个可以共同讨论、测试和交付的单一产物43。Docker 成了连接这两个(曾经)敌对部落的“粘合剂”。

E. 伟大的副作用(二):微服务的“完美”引擎

如果说 Docker 是 DevOps 革命的“粘合剂”,那它就是微服务(Microservices)革命的“物理引擎”。

在你沉睡的年代,我们构建的是“单体应用”(Monoliths)——一个巨大的、包含所有功能的、盘根错节的程序。

而在 2010 年代,“微服务”架构开始流行:把那个巨大的“单体”拆分成一系列迷你的、各自独立部署的小服务(比如一个“用户服务”、一个“支付服务”、一个“邮件服务”)45

这个理念在理论上很美妙:每个服务都可以独立开发、独立升级、独立扩展。

但在实践中,它遇到了一个“物理障碍”:VMs 太重了!46

你不能为了一个只占 20MB 内存的“邮件服务”,就去启动一个 2GB 内存的完整虚拟机。如果你把应用拆成了 50 个微服务,你总不能启动 50 个 VM 吧?那会吃光你所有的资源。

而 Docker 来了。它轻量、隔离、启动快(只需几秒钟)46

Docker 的架构与微服务的原则“完美契合”45

  • 微服务需要隔离?Docker 提供廉价的进程隔离45
  • 微服务需要独立部署?Docker 将服务打包成可移植的镜像47
  • 微服务需要快速扩展?Docker 容器可以在几秒内启动46

Docker 的出现,使得微服务架构从一个“理论上的好主意”变成了“工业界的主流实践”。Docker 为微服务提供了完美的物理封装48

可以说,Docker 和微服务是一场“幸福的联姻”,它们互相成就了对方。


结语(与新的诅咒):“我该死的集装箱太多了!”

好了,旅行者。我们胜利了。

我们用 Docker 这个“魔法集装箱”,彻底杀死了“在我这儿能跑”的诅咒。

我们用 Docker,粘合了 Dev 和 Ops。

我们用 Docker,引爆了微服务革命。

我们赢了……对吗?

不。我们只是用一个老问题,换来了一个新问题。

Docker 完美地解决了一个应用的环境问题。但是,当我们兴高采烈地拥抱了微服务,把我们的“单体”巨兽拆分成了 500 个微小的 Docker 容器(集装箱)后……新的噩梦开始了49

一个全新的、更高级的、更复杂的“地狱”在我们脚下展开49

  1. 服务发现:这 500 个集装箱在服务器集群里到处漂浮,A 容器(支付服务)如何找到 B 容器(用户服务)的 IP 地址?
  2. 负载均衡:如果“主页”服务有 20 个一模一样的容器副本,一个新来的用户请求应该被发给哪一个?
  3. 弹性伸缩:当“黑色星期五”的流量洪峰涌入时,谁来负责自动把“购物车”服务的容器数量从 10 个扩展到 100 个?流量过后又由谁缩减回 10 个?50
  4. 自我修复:如果那个运行数据库的容器(集装箱)因为内存溢出而崩溃(沉没)了,谁来负责自动重启(打捞)它?51

事实证明,复杂性不会消失,它只会转移。

我们用 Docker 消除了“环境依赖”的复杂性,却创造了“分布式系统管理”的(史诗级的)复杂性。

手动管理 3 个集装箱是愉快的。手动管理 3000 个集装箱,是一场灾难51

我们需要一个全新的角色。我们不再需要“搬运工”(CM)或“公寓管理员”(VM)。我们需要一个自动化港口的“总调度官”,一个“集装箱舰队的总司令”52

这个新的领域,叫做“容器编排”(Container Orchestration)50

恭喜你,旅行者。你刚刚逃离了“依赖地狱”,却一头扎进了“容器编排”的风暴眼。


引用的著作


  1. It Works On My Machine! : dylanbeattie.net, 访问时间为 十月 27, 2025, https://dylanbeattie.net/2017/04/27/it-works-on-my-machine.html ↩︎

  2. It works on my machine... : r/ProgrammerHumor - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/ProgrammerHumor/comments/70we66/it_works_on_my_machine/ ↩︎

  3. Dev Vs Prod: A Tale Of Two Environments - ProgrammerHumor.io, 访问时间为 十月 27, 2025, https://programmerhumor.io/debugging-memes/dev-vs-prod-a-tale-of-two-environments-cvwz ↩︎ ↩︎

  4. 程序员复活手册:从BASIC到AI(大纲) ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  5. Configuration Drift Explained | Wiz, 访问时间为 十月 27, 2025, https://www.wiz.io/academy/configuration-drift ↩︎ ↩︎ ↩︎

  6. What is Configuration Drift? Tools, Causes & Risks - Spacelift, 访问时间为 十月 27, 2025, https://spacelift.io/blog/what-is-configuration-drift ↩︎ ↩︎

  7. Configuration Drift: How It Happens, Top Sources + How to Stop It for Good | Puppet, 访问时间为 十月 27, 2025, https://www.puppet.com/blog/configuration-drift ↩︎

  8. What Is Configuration Drift? | DevOps Glossary Terms - Mad Devs, 访问时间为 十月 27, 2025, https://maddevs.io/glossary/configuration-drift/ ↩︎

  9. Windows 95: 28 years old already. What are your memories of that era, good or bad?, 访问时间为 十月 27, 2025, https://www.reddit.com/r/windows/comments/164035b/windows_95_28_years_old_already_what_are_your/ ↩︎

  10. Dll hell Memes - ProgrammerHumor.io, 访问时间为 十月 27, 2025, https://programmerhumor.io/memes/dll-hell ↩︎

  11. JAR Hell | by Arjun Ram - Medium, 访问时间为 十月 27, 2025, https://medium.com/arjun-rams-blog/jar-hell-687636c08862 ↩︎

  12. This is incomprehensible. "DLL Hell" is what happens you use dynamically linked ... - Hacker News, 访问时间为 十月 27, 2025, https://news.ycombinator.com/item?id=4472322 ↩︎

  13. Dll hell, WinSXS monster, and Delphi paradise (Page 1) / Delphi / mORMot Open Source, 访问时间为 十月 27, 2025, https://www.synopse.info/forum/viewtopic.php?id=114 ↩︎

  14. Good. The glacial migration from Python 2 to 3 is one of the worst ..., 访问时间为 十月 27, 2025, https://news.ycombinator.com/item?id=15708136 ↩︎ ↩︎

  15. Python 2->3 transition was horrifically bad - LWN.net, 访问时间为 十月 27, 2025, https://lwn.net/Articles/843660/ ↩︎

  16. Can someone explain the node_modules joke to me please? I've seen it quite a bit now, but I still don't get it. (Attache - devRant, 访问时间为 十月 27, 2025, https://devrant.com/rants/1558252/can-someone-explain-the-node-modules-joke-to-me-please-ive-seen-it-quite-a-bit-n ↩︎

  17. The Black Hole Called Node_modules · ProgrammerHumor.io, 访问时间为 十月 27, 2025, https://programmerhumor.io/javascript-memes/the-black-hole-called-node_modules-ta0o ↩︎ ↩︎ ↩︎

  18. Docker vs VM - Difference Between Application Deployment ... - AWS, 访问时间为 十月 27, 2025, https://aws.amazon.com/compare/the-difference-between-docker-vm/ ↩︎ ↩︎

  19. How is Docker different from a virtual machine? - Stack Overflow, 访问时间为 十月 27, 2025, https://stackoverflow.com/questions/16047306/how-is-docker-different-from-a-virtual-machine ↩︎

  20. Comparing VMs and Containers - Aqueduct Tech, 访问时间为 十月 27, 2025, https://aqueducttech.com/aq-blog/containers-and-virtual-machines/ ↩︎

  21. Containers vs. virtual machines | Microsoft Learn, 访问时间为 十月 27, 2025, https://learn.microsoft.com/en-us/virtualization/windowscontainers/about/containers-vs-vm ↩︎ ↩︎ ↩︎

  22. How I Finally Understood Virtual Machines vs Containers (Like ..., 访问时间为 十月 27, 2025, https://medium.com/illumination/how-i-finally-understood-virtual-machines-vs-containers-like-apartment-rentals-e6fa6b21e24c ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  23. Bare Metal vs. Traditional VMs: Which is Better for LLM Training? - Runpod, 访问时间为 十月 27, 2025, https://www.runpod.io/articles/comparison/bare-metal-vs-traditional-vms-llm-training ↩︎

  24. Infrastructure as Code: Guide to IT Automation & Management - Valorem Reply, 访问时间为 十月 27, 2025, https://www.valoremreply.com/resources/insights/guide/infrastructure-as-code-a-guide-to-automating-and-managing-your-it-landscape/ ↩︎

  25. Understanding Ansible, Terraform, Puppet, Chef, and Salt - Red Hat, 访问时间为 十月 27, 2025, https://www.redhat.com/en/topics/automation/understanding-ansible-vs-terraform-puppet-chef-and-salt ↩︎

  26. Chef vs Puppet vs Ansible: Comparing DevOps Tools - ServerMania, 访问时间为 十月 27, 2025, https://www.servermania.com/kb/articles/chef-vs-puppet-vs-ansible ↩︎

  27. Ansible vs. Puppet: What you need to know - Red Hat, 访问时间为 十月 27, 2025, https://www.redhat.com/en/topics/automation/ansible-vs-puppet ↩︎ ↩︎ ↩︎

  28. Understanding Ansible: A Gateway to Intelligent IT Automation - CertLibrary Blog, 访问时间为 十月 27, 2025, https://www.certlibrary.com/blog/understanding-ansible-a-gateway-to-intelligent-it-automation/ ↩︎

  29. Docker and Configuration Management - Jama Software, 访问时间为 十月 27, 2025, https://www.jamasoftware.com/blog/ruminations-on-docker-and-configuration-management/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  30. jpetazzo/From dotCloud to Docker, 访问时间为 十月 27, 2025, https://jpetazzo.github.io/2017/02/24/from-dotcloud-to-docker/ ↩︎

  31. What led to the invention of Docker? - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/docker/comments/18tkspm/what_led_to_the_invention_of_docker/ ↩︎

  32. Docker (software) - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Docker_(software) ↩︎ ↩︎ ↩︎

  33. VM vs. Container: Ultimate 2024 Showdown - Aqua Security, 访问时间为 十月 27, 2025, https://www.aquasec.com/cloud-native-academy/docker-container/vm-vs-container/ ↩︎ ↩︎

  34. ELI5: What is the difference between a container and a virtual machine? : r/compsci - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/compsci/comments/f2d3a6/eli5_what_is_the_difference_between_a_container/ ↩︎

  35. Docker Images vs Docker Containers: A Comparison - Mend.io, 访问时间为 十月 27, 2025, https://www.mend.io/blog/docker-images-vs-docker-containers-a-detailed-comparison/ ↩︎ ↩︎ ↩︎

  36. What is Docker, How It Works, and Why We Need It (In Simple Terms with Fun Analogies) - DEV Community, 访问时间为 十月 27, 2025, https://dev.to/syssyncer/what-is-docker-how-it-works-and-why-we-need-it-in-simple-terms-with-fun-analogies-2b17 ↩︎ ↩︎

  37. What is the difference between a Docker image and a container? [closed] - Stack Overflow, 访问时间为 十月 27, 2025, https://stackoverflow.com/questions/23735149/what-is-the-difference-between-a-docker-image-and-a-container ↩︎

  38. Understanding the image layers - Docker Docs, 访问时间为 十月 27, 2025, https://docs.docker.com/get-started/docker-concepts/building-images/understanding-image-layers/ ↩︎

  39. Docker Image Layers Explained: A Beginner's Guide - YouTube, 访问时间为 十月 27, 2025, https://www.youtube.com/watch?v=24VY7NsbkFI ↩︎

  40. Docker Layers: Why They Matter and How to Optimize Them | by ..., 访问时间为 十月 27, 2025, https://medium.com/@leroyleowdev/in-docker-an-image-is-essentially-a-collection-of-interconnected-read-only-layers-where-each-7a3af791b210 ↩︎ ↩︎

  41. What are Docker image "layers"? - Stack Overflow, 访问时间为 十月 27, 2025, https://stackoverflow.com/questions/31222377/what-are-docker-image-layers ↩︎

  42. Docker as a framework for your DevOps culture - DevOps.com, 访问时间为 十月 27, 2025, https://devops.com/docker-as-a-framework-for-your-devops-culture/ ↩︎

  43. Exploring Docker for DevOps: What It Is and How It Works | Docker, 访问时间为 十月 27, 2025, https://www.docker.com/blog/docker-for-devops/ ↩︎ ↩︎

  44. Leveraging Docker to Complement DevOps Workflows and Accelerate Delivery - Medium, 访问时间为 十月 27, 2025, https://medium.com/@vesper7/introduction-72caab9da34a ↩︎

  45. Chapter 15: Docker and Microservices Architecture | by Praneeth ..., 访问时间为 十月 27, 2025, https://praneethreddybilakanti.medium.com/chapter-15-docker-and-microservices-architecture-c28f1b21919a ↩︎ ↩︎ ↩︎

  46. How to Deploy Microservice Architecture in Docker? - Middleware.io, 访问时间为 十月 27, 2025, https://middleware.io/blog/microservices-architecture-docker/ ↩︎ ↩︎ ↩︎

  47. Microservices and Containerization: Challenges and Best Practices - Aqua Security, 访问时间为 十月 27, 2025, https://www.aquasec.com/cloud-native-academy/docker-container/microservices-and-containerization/ ↩︎

  48. Why Docker is the Future of DevOps? | Coffee And Code - Medium, 访问时间为 十月 27, 2025, https://medium.com/techtrends-digest/why-docker-is-the-future-of-devops-3be23969e155 ↩︎

  49. Multi-container applications | Docker Docs, 访问时间为 十月 27, 2025, https://docs.docker.com/get-started/docker-concepts/running-containers/multi-container-applications/ ↩︎ ↩︎

  50. What Is Container Orchestration? | IBM, 访问时间为 十月 27, 2025, https://www.ibm.com/think/topics/container-orchestration ↩︎ ↩︎

  51. What is Container Orchestration? - AWS, 访问时间为 十月 27, 2025, https://aws.amazon.com/what-is/container-orchestration/ ↩︎ ↩︎

  52. Understanding Docker and Kubernetes: A Simple Explanation | by Deepak Sharma, 访问时间为 十月 27, 2025, https://medium.com/@deepaksharma2494/understanding-kubernetes-and-docker-easiest-explanation-9505638107e3 ↩︎