序章:午夜凶铃——一次价值千万的宕机事故
第 1 章:序章:午夜凶铃——一次价值千万的宕机事故
1.1 凌晨 2:17 的告警短信
2016 年 11 月 25 日,星期五,凌晨 2:17。林墨被手机震动从浅睡中拽出来时,眼皮沉重得像挂了铅块,但屏幕上那条红色告警短信让他的肾上腺素瞬间飙到顶点——
``` 【P0级告警】订单中心服务不可用 触发时间:2016-11-25 02:14:33 影响范围:全站下单链路 当前错误率:87.3% 值班人:林墨 ```
他翻身下床,光脚踩在冰凉的地板上,一边拨通 NOC(网络运维中心)的电话一边打开笔记本电脑的远程桌面。这一天是欧美"黑色星期五"促销日的延续,国内某头部跨境电商平台"耀购"做了 72 小时不间断的促销活动。两个月前,市场部拍着胸脯在 CEO 面前立下军令状——GMV 冲击 3.8 亿。而现在,距离活动收官只剩 6 小时,订单中心却像被人拔了电源。
林墨是这个电商平台后端架构组的资深工程师,组里习惯叫他"救火队长"。不是因为技术最拔尖,而是因为他有一个近乎偏执的习惯:每次故障复盘,他都会用工程化的方法把事故链路画到像素级。也正是这种习惯,让他在前几次 P0 事故中屡屡找到别人忽略的"幽灵进程"。
"订单中心 6 台应用服务器,3 台 CPU 跑到 100%,另外 3 台负载不到 10%。"NOC 同事小郑的声音从电话那头传来,背景里是嘈杂的告警声和急促的键盘敲击声,"数据库主库没死,从库延迟在 200 毫秒以内,Redis 集群正常。"
"那为什么订单写入失败?"林墨一边穿衣服一边问。
"应用日志里报的是 `NoSuchMethodError`,调的是支付服务的 SDK 0.9 版本里的一个方法。"
林墨停下动作。`NoSuchMethodError`。这个错误他在 2014 年那次事故复盘里见过——那一次是因为测试环境的 Spring 版本是 4.1.2,生产环境部署的却是 4.0.6。"这个 SDK 是哪个版本?"他问。
"我们订单服务在测试环境是 0.9,运维在生产环境部署的是……"小郑翻了一下部署系统的记录,"0.8.2。"
林墨闭上眼,深吸一口气。
这是经典的环境不一致问题:开发在 Mac 上写代码,用的是 Mac 自带的 Java,测试用 Linux 容器跑 `mvn package`,而运维部署用的是三个月前打包好的基础镜像。三套环境,Java 版本不一样、Spring 版本不一样、连支付 SDK 的版本都不一样。开发在测试环境测了 200 个用例全部通过,部署到生产就崩。
凌晨 2:34,林墨用 VPN 登录到生产机房的堡垒机,远程到那 3 台 CPU 100% 的服务器。他用 `top -H` 看到几个 Java 进程把 CPU 跑满,但奇怪的是——这三台机器上跑的不只是订单服务的进程,还有日志收集的 Filebeat、监控的 Node Exporter、甚至一个被遗忘的 Python 写的对账脚本。这些进程在"看似"闲置的时候抢着申请内存和 CPU 时间片,把订单服务的 GC(垃圾回收)周期拉到了正常值的 5 倍。
凌晨 3:12,林墨临时关掉了非核心进程,重启订单服务集群,错误率从 87% 降到 4%,但促销流量还在涌入。凌晨 3:28,他接到上级的电话,让他立刻把故障等级从 P0 降级为 P1——"影响收尾的促销数据,CEO 说不要再扩散了"。
林墨没有反驳。但他知道,这不是一次事故,这是一次必然。一个 32 人的后端团队,一个 3.8 亿 GMV 的促销活动,部署链路里只要有一处不一致,崩盘只是时间问题。
上午 10 点,CTO 召开紧急复盘会。会议室里 12 个人围坐在长桌前,林墨用 40 分钟把故障时间线画了出来:02:14 流量开始激增、02:14:12 订单服务开始 OOM、02:14:33 告警触发、02:18 第一波人工响应、02:34 误判为数据库问题、02:58 才定位到环境差异……整条链路上的每一环都写满了"被动"二字。
"我们 12 个工程师,6 个小时,没救回一次部署就能避免的事故。"林墨敲了敲白板上的时间线,"问题不在我们不够努力,问题在于我们的部署模式本身,就建立在沙子上。"
会后,CTO 让他写一份事后分析报告。林墨回到工位,打开 Confluence,新建了一个文档,标题敲了 6 个字:"部署十二宗罪"。
---
1.2 传统部署的"十二宗罪"
事后分析报告的初稿,林墨花了整整一个周末。他没有按照 ITIL 标准的"故障描述—根因分析—改进措施"格式写——他按照工程师能看懂的方式,把耀购当时的部署模式拆成了 12 个具体的、可以量化的"罪状"。他后来才知道,这份报告在公司内部被打印出来贴在了 7 楼茶水间的墙上。
罪状一:环境漂移的"薛定谔"
耀购当时有 4 套环境:开发环境(每个工程师的 Mac)、测试环境(IDC 机房的 3 台物理机)、预发布环境(2 台虚拟机)、生产环境(36 台物理机 + 12 台虚拟机)。"在我这里跑得好好的" 是当时最流行的一句话,也是最危险的一句话。
环境漂移带来的不是 1% 的不一致,而是 N 个变量组合出来的不一致:操作系统内核版本、glibc 版本、JVM 垃圾回收器、文件编码、时区设置、内核参数(如 `net.core.somaxconn`)、磁盘 IO 调度器、甚至是 `/etc/hosts` 里的 hostname。林墨在那次故障里查到的支付 SDK 版本差异,只是冰山一角。真正的隐患,是那些只有生产环境才会触发的边缘 case。
罪状二:进程级别的"公有悲剧"
Linux 是一个多进程操作系统。当 32 个工程师共用 36 台物理机时,每个人的进程都在和所有人的进程竞争资源。这不是技术问题,这是社会问题——Tragedy of the Commons(公有地悲剧)。
Filebeat 想读日志文件、监控代理想定时 ping 数据库、对账脚本想在凌晨 3 点跑批、订单服务想处理支付请求。它们都在争 CPU、争内存、争磁盘 IO、争网络带宽。在低负载时,大家都相安无事;一旦流量激增,弱肉强食就开始了。林墨在生产机上看到的 Filebeat 进程抢走订单服务 30% 的 CPU 周期,就是这种竞争最直观的体现。
罪状三:部署的"慢动作回放"
耀购当时的部署流程是这样的:
1. 开发者本地打包,生成 200MB 的 WAR 包 2. 上传到内部 FTP 3. 通知运维,运维在控制台点击"发布" 4. 部署脚本把 WAR 包传到 36 台生产机器 5. 逐台停机、备份、部署、启动 6. 6 台订单服务机器,每台 3 分钟,全部部署完成需要 18 分钟
这套流程在 2014 年还能接受,2016 年的大促场景下已经是灾难。当流量从 1 万 QPS 在 5 分钟内飙升到 8 万 QPS 时,18 分钟的部署窗口期足够让系统崩溃两次。
罪状四到十二:被遗忘的"基础设施债"
剩下的 9 条"罪状",林墨在报告里写得简短但致命:
- **回滚困难**:部署失败后,回滚需要逐台回退 WAR 包,耗时 25 分钟,期间只能干等
- **资源利用率极低**:36 台物理机,平均 CPU 利用率 8%,内存利用率 23%,大促期间临时扩容需要 3 天采购周期
- **环境复制成本高**:新建一套测试环境需要运维手动配置 30+ 项参数,耗时 2 个工作日
- **故障隔离靠"运气"**:不同应用的进程跑在同一台机器上,一个 OOM 可能把无辜的邻居一起拖下水
- **跨环境迁移困难**:物理机到虚拟机、虚拟机到云主机,每一次迁移都是一次"开盲盒"
- **版本管理混乱**:"上周部署的版本到底是什么" 这个问题,在公司内部能问出 5 个不同答案
- **多语言栈无法统一**:Java 团队用 Maven,Python 团队用 pip,Node.js 团队用 npm,每个团队都有自己的"部署圣经"
- **本地开发体验割裂**:新员工入职第一天,90% 的时间花在"让代码跑起来"上,而不是读业务代码
- **CI/CD 名存实亡**:虽然有 Jenkins,但每天只能跑 3 次构建,因为构建过程太慢、占用资源太多
- **安全合规无法审计**:不知道哪台机器跑了什么代码、不知道哪个进程访问了哪些文件、不知道谁在凌晨 3 点登录了生产机器
林墨写完这份报告后,加了一段总结:
> "以上 12 条不是孤立的问题,它们共同指向一个根本性的架构缺陷:我们把应用和基础设施混在了一起。应用是变化的(每天发布),基础设施应该是稳定的(操作系统、依赖库),但我们用同一台机器承载两者,又没有清晰的分层。当应用爆炸时,基础设施跟着一起爆炸。"
这份报告最终被打印了 200 份,CTO 在封面写了一行红字:"2017 年 Q1 必须解决"。
---
1.3 容器化思想的诞生:cgroup 与 namespace
解决"应用和基础设施混在一起"这个问题的思路,在 2016 年并不新鲜。它的技术原型,在 Linux 内核里已经躺了十几年。
1.3.1 两次技术遗产:2006 年的 cgroup 和 2002 年的 namespace
2002 年,Linux 2.4.19 内核引入了 namespace(命名空间)机制。最初的目的是支持 UTS namespace(独立的 hostname)和 IPC namespace(独立的进程间通信)。到 2006 年的 2.6.19 内核,Linux 增加了 PID namespace,让一个进程可以"看到"自己是一号进程。紧接着,2008 年的 2.6.26 内核加入了 Network namespace,2012 年的 3.8 内核加入了 User namespace。
namespace 解决的是"看得见的世界"问题:通过把全局内核资源包裹在抽象的命名空间里,让一组进程看到一个独立的视图。两个不同的 PID namespace 里的两个进程,PID 都是 1,但它们彼此看不见。
2006 年,Google 的工程师在 Linux 2.6.24 内核中合入了 cgroup(control group,控制组)机制——最初的名字甚至不是 cgroup,而是"process containers",后来因为和 Solaris Containers 概念冲突才改名。cgroup 解决的是"用得着的世界"问题:通过把进程组织成层级化的组,对它们的 CPU、内存、磁盘 IO、网络带宽做配额和限制。
把 namespace 和 cgroup 结合在一起,理论上就可以实现:一个进程组,拥有独立的进程视图、独立的网络栈、独立的文件系统视图、独立的用户权限,同时受到严格的资源配额限制。这就是容器的本质。
但"理论"和"工程"之间,隔着一道天堑。
1.3.2 2008—2013:LXC、OpenVZ 与"为什么没有人用"
2008 年,IBM 的工程师 Daniel Lezcano 和其他贡献者发起了 LXC(Linux Containers)项目,尝试把 namespace 和 cgroup 包装成一个用户态工具,让用户能用简单的命令启动一个"容器"。LXC 的核心思想是:
```bash
创建一个容器
lxc-create -t ubuntu -n my_container
启动容器
lxc-start -n my_container
进入容器
lxc-attach -n my_container ```
听起来很美好。但 LXC 在 2008—2013 年的企业市场几乎无人问津。林墨后来研究 LXC 文档时发现,当时的 LXC 有三个致命问题:
1. 依赖完整的 Linux 发行版:一个 Ubuntu 容器需要完整的 rootfs(根文件系统),动辄 800MB,下载和启动都很慢 2. 没有镜像标准:A 机器上创建的容器,在 B 机器上无法保证能跑起来 3. 没有生态:没有镜像仓库、没有编排系统、没有 CI/CD 集成、没有云厂商支持
同一个时期,Red Hat 在企业市场推 KVM(基于内核的虚拟机),VMware 在企业市场推 vSphere。"虚拟化"是那个年代最性感的词。 容器?太 geek 了,离生产太远。
直到 2013 年 3 月,PyCon 大会上,dotCloud 公司的工程师 Solomon Hykes 在一个 5 分钟的 Lightning Talk 里,演示了一个叫做 Docker 的小工具。
那个 5 分钟的演示,被后来无数人描述为"改变云计算历史的 5 分钟"。
---
1.4 Docker 崛起:从 dotCloud 到开源革命
1.4.1 dotCloud 的"无心插柳"
2008 年,Solomon Hykes 在巴黎创立了 dotCloud——一家 PaaS(平台即服务)公司。在那个 VMware、CloudFoundry、OpenShift 群雄混战的年代,dotCloud 只是众多 PaaS 创业公司中不起眼的一家。dotCloud 的核心产品允许开发者上传代码,平台自动处理部署、扩缩容、负载均衡等基础设施问题。
为了支撑 dotCloud 平台的部署,Hykes 的团队需要解决一个工程难题:如何在共享的物理机上,快速启动多个隔离的用户环境?当时他们尝试过 KVM、尝试过 Xen、尝试过 OpenVZ,最后都因为太重、太慢、太难管理而放弃。最终,他们选择了 LXC——但 LXC 的"无标准、难分发、难管理"问题,让他们痛苦不堪。
于是,2010—2013 年间,Hykes 的团队内部开始构建一个工具,把 LXC 的能力包装成"开发者友好的 API":简单的 Dockerfile 格式描述应用、统一的镜像格式、镜像分层(layer)的概念、镜像仓库的雏形。这个内部工具,后来被命名为 Docker。
Hykes 后来回忆,2013 年初他们面临一个艰难的选择:dotCloud 的 PaaS 业务增长缓慢,公司现金储备只够再撑 8 个月。Docker 在内部用得很好,但商业上找不到突破口。一个深夜,Hykes 和联合创始人 Sebastien Pahl 做了一个赌上公司命运的决定:把 Docker 开源。
> "我们当时的想法是,如果 Docker 能成为行业的标准,dotCloud 自然能活下来;如果不能,那 dotCloud 也活不下来。" —— Solomon Hykes,2014 年 DockerCon 主题演讲
1.4.2 2013 年 3 月:那 5 分钟的 Lightning Talk
2013 年 3 月 15 日,PyCon US 大会,加州圣克拉拉会议中心。Solomon Hykes 站在 Lightning Talk 的舞台上,用 5 分钟演示了 Docker。他从一个 `Dockerfile` 开始:
```dockerfile FROM ubuntu:14.04 MAINTAINER Solomon Hykes <solomon@dotcloud.com>
RUN apt-get update && apt-get install -y python python-pip RUN pip install flask COPY app.py /app.py EXPOSE 80 CMD ["python", "/app.py"] ```
然后一行命令构建镜像:
```bash docker build -t myapp . ```
再一行命令运行:
```bash docker run -d -p 80:80 myapp ```
5 分钟结束,全场起立鼓掌。
林墨后来在 YouTube 上反复看过这段视频不下 20 次。他说,每次看到 Hykes 在演示时那种"在座各位都没意识到自己看到了什么"的表情,都会被感染。那 5 分钟的演示,本质上回答了一个问题:"如何让一个应用,连同它的整个运行环境,打包成一个标准化的、可移植的、自包含的单元?"
1.4.3 开源后的指数级增长
2013 年 3 月 Docker 在 GitHub 开源后,增长曲线不是线性的,是指数级的:
- 2013 年底:GitHub Star 数突破 1 万
- 2014 年 6 月 DockerCon 首届大会:1.0 版本发布
- 2014 年底:Star 数突破 3 万,Red Hat、谷歌、亚马逊、微软宣布支持
- 2015 年:Docker 成立公司,融资 9500 万美元,估值 10 亿美元(独角兽)
- 2016 年 9 月:累计下载量突破 120 亿次
为什么增长这么快?因为 Docker 解决了 LXC 留下的三个致命问题:
1. 镜像标准:Docker 引入了"分层镜像 + Dockerfile"的标准化格式。一个 Ubuntu 基础镜像可以被无数次复用,应用层作为独立 layer 叠加在上面,镜像大小从 LXC 时代的 800MB 压缩到几十 MB 2. 镜像仓库:Docker Hub 在 2014 年正式上线,截至 2016 年已经有超过 100 万个公共镜像 3. 生态爆发:Kubernetes(谷歌)、Mesos(推特)、Swarm(Docker 公司)、Rancher、CoreOS、CloudFoundry……所有云厂商和编排系统都开始支持 Docker
林墨在 2016 年那个不眠之夜,偶然点开同事分享的一篇博客,第一次看到了 `docker run` 命令。他愣了 10 分钟——他意识到,这不就是他"部署十二宗罪"里想要的那个答案吗?
---
1.5 事故复盘与容器化提案
1.5.1 凌晨 6 点的技术债清单
2016 年 11 月 25 日早上 6:18,促销活动收官。林墨回到家洗了个澡,睡了 3 个小时,又回到公司。他知道今天下午要开全员复盘会,CTO 让他准备一份"技术债务清单"。
他打开 Confluence,在"部署十二宗罪"的基础上又补充了 5 条:
- **#13**:缺乏统一的镜像标准,导致开发、测试、生产环境无法对齐
- **#14**:缺乏资源隔离,应用之间相互影响
- **#15**:缺乏弹性能力,无法在分钟级别扩缩容
- **#16**:缺乏版本回滚机制,部署失败后没有"一键回滚"
- **#17**:缺乏多语言栈统一能力,Java/Python/Node.js 各自为政
下午 4 点的复盘会开得异常激烈。运维总监质疑:"容器