2017 年 1 月 31 日深夜,一位工程师在错误的服务器上清空了数据目录。真正让这一夜载入史册的不是那条命令,而是接下来的两个小时里,他们一层一层揭开五条恢复路径——正式灾备体系里的三条没能兑现,最后救场的,是一份为了做负载测试临时打的快照。
这一夜的开头,是一件完全正常、甚至值得表扬的事。
1 月 31 日下午,一位 GitLab 工程师准备在预发环境搭几台 PostgreSQL,试试用 pgpool-II 做读负载均衡,看能不能减轻数据库压力 [S1]。为了让压测结果更接近真实,他需要一份足够新的生产数据。
系统本来每 24 小时(01:00 UTC)会自动做一次这件事,但他想要更新的副本,于是在 约 17:20 UTC 手动打了一个生产库的 LVM 快照,灌进预发 [S1]。
两个小时后,事情开始不对劲。
手动 LVM 快照。为压测而打,灌进预发环境。这是当晚后来实际被采用的那个恢复点,而做它的人当时并不知道 [S1]。
数据库负载开始升高,团队怀疑是垃圾内容(spam)造成的。很多用户无法在 issue 和合并请求下发表评论。把负载压下去花了好几个小时 [S1]。
部分负载来自一个后台任务:它正在删除一名 GitLab 员工及其关联数据——该账号被人恶意举报为滥用,因此被误安排了删除 [S1]。一个内容审核流程的疏漏,变成了数据库上的一场重负载。
复制断了。持续的高负载让从库复制开始落后;等它想追时,主库上它需要的 WAL 段已经被删掉了。由于当时没有启用 WAL 归档,从库只能手工重建 [S1]。
手工重建从库的步骤写在运维手册里,只有两步:清空从库的数据目录,然后跑 pg_basebackup [S1]。听起来五分钟的事。
接下来的一个半小时,是每一个值过夜班的工程师都认得的那种时间:
pg_basebackup —— 挂住不动,加了 --verbose 也没有任何有意义的输出pg_basebackup —— 还是挂住strace 查看 —— 卡在一个 poll 调用上,看不出别的信息这里有一个残忍的细节,是复盘里才写明的:pg_basebackup 那个"挂住"根本不是故障。它就是在安静地等主库开始发送复制数据——这是正常行为。当时 GitLab 的内部运维手册没写,官方文档也没写清楚 [S1]。
也就是说:这一个半小时里,一群疲惫的工程师在排查一个并不存在的故障,而工具从头到尾一言不发。
B。这里有两个问题,而它们的紧迫性不一样。负载是眼前的痛,「为什么会有这个删除任务」是真正危险的问题——事实上它删的是一名员工的数据,起因是账号被恶意举报后误判 [S1]。一个能被外部人员触发、且会硬删除数据的自动流程,本身就是事故源。暂停它同时解决了负载和数据安全两件事。
A 是当晚实际发生的路径的近似:把注意力全放在压负载上。它的问题在于,删除任务跑完之后数据就真的没了——而且没有人在那个时刻问过"它凭什么在删"。故障处置里最贵的错误,往往不是做错了动作,而是没问一个问题。
C 是本能反应,也是本书反复出现的错误答案。扩容需要时间(第 5 章我们已经算过这笔时间账),而且它解决的是"扛得住",不是"该不该发生"。给一个不该运行的任务扩容,等于花钱加速一场事故。
D 方向对,力度不够。限速能立刻缓解数据库压力,是个合理的止血动作——但它默认了"这个删除是应该发生的"。如果这个前提本身是错的,限速只是让错误发生得更慢。先暂停、再判断、最后才决定要不要限速重跑,顺序不能反。
strace 也看不出名堂。B。这不是技术判断,是人因工程。四个半小时的连续故障处置之后,人对"我在哪台机器上"这类基础事实的把握会显著下降——而接下来要执行的恰好是一条不可逆的命令。换人的成本是十五分钟的交接,不换人的成本是 300 GB。事实上,那个"挂住"的真相正是由一位当时不在场的工程师后来指出的 [S1]:新鲜的眼睛看见了疲惫的眼睛看不见的东西。
A 就是当晚实际走的那一步,并且它的推理完全合理——工程师确实怀疑之前几次 pg_basebackup 在数据目录里留下了文件 [S1]。合理的推理 + 疲劳 + 两个长得一模一样的终端,就是这一夜的全部配方。请注意:这个选项之所以是错的,不是因为推理错了,而是因为它把一条不可逆命令交给了一个已经工作四个半小时的人。
C 的直觉非常好,但它在这一夜救不了场——它救的是下一夜。动手前先给主库留一个快照,正是「危险操作前先建立退路」的标准做法。它之所以只能算半对,是因为此刻主库并没有出问题,团队完全意识不到自己正在接近主库;真正让 C 有价值的,是把它变成制度:任何触碰数据目录的操作前一律先快照,而不是靠当事人当场想起来。
D 会让你更快地走到同一个悬崖边。「彻底重装」意味着更多的删除动作、更多次在两台机器之间切换终端,而你的判断力正在下降。判断力已经下降时,正确动作是减少不可逆操作的次数,不是增加。
B。关键区别在于让系统回答,而不是让人回忆。窗口标题、提示符颜色、你三分钟前敲的 ssh——这些都是记忆,而记忆正是凌晨最不可靠的东西。让机器当场打印出主机名和"我是主库还是从库",这个事实无法被疲劳篡改。这也是为什么现代运维会给危险命令加"说出目标身份再确认"的护栏:护栏防的不是无知,是疲劳。
A 是绝大多数人的做法,也确实拦下过很多事故——但它拦不住这一次。当晚的问题恰恰是工程师确信自己在从库上 [S1]。当一个人已经确信时,再看一眼只会强化确信,因为你看到的是你以为会看到的东西。视觉确认对抗不了"我以为"。
C 是整页最危险的一个选项,因为它听起来最专业。命令来自手册这件事是真的,手册本身也没错。但手册假设你在正确的机器上——手册管的是「怎么做」,从来管不了「在哪做」。这一夜之后,GitLab 复盘里反复出现的正是这类"流程正确但上下文错误"的问题。
D 有价值,但它把一个 30 秒的问题变成了一个要等人回复的问题。凌晨的群里可能没人醒着,而等待本身会推着你去点那个回车。四眼原则的正确形态是阻塞式的——工具要求第二个人确认才放行,而不是靠你自觉去问。这一点在关卡三(Knight Capital)会以 4.6 亿美元的标价重新出现。
回车按下去了。命令执行在了主库上 [S1]。
他在发现的一两秒后就终止了进程。约 300 GB 的数据已经不在了 [S1]。
接下来发生的事,才是这一夜真正的教材。团队去找备份,并在 Slack 上求助 [S1]。GitLab 当时有五条可走的恢复路径。请跟着他们的顺序,一层一层打开。
pg_dump 是 9.2 版,而数据库跑的是 PostgreSQL 9.6——大版本不匹配会让 pg_dump 直接报错退出 [S1]。
为什么版本会错?因为备份跑在一台普通应用服务器上,那里没有 PostgreSQL 数据目录,打包工具因此回退到了 9.2 [S1]。
团队最终选了第 5 层,因为「这是把数据损失降到最小的唯一选项」 [S1]。
把数据从预发主机拷回生产花了约 18 小时。磁盘是网络盘、被限速到约 60 Mbps;没有网络瓶颈也没有 CPU 瓶颈,瓶颈就在磁盘 [S1]。
预发环境用的是便宜档位的存储,为省成本 [S1]。一个在预发环境上省下的成本,在生产恢复时以 18 小时的形式还了回来。
数据库恢复完成(尚不含 webhooks),状态回到 1 月 31 日 17:20 UTC [S1]。
补回 webhooks,收尾完成 [S1]。恢复过程中把所有数据库序列加了 100,000,以免复用事故前可能已经用过的 ID [S1]。
值得注意的是最后一格:代码仓库和 wiki 一点没丢,因为它们和数据库分开存储 [S1]。对一家代码托管公司来说,这是那一夜最大的运气——也是"把不同性质的数据分开放"这个决定,在多年以后替他们挡下的一刀。
GitLab 在恢复期间做了三件在当时看来相当反直觉的事:把进度记录在一份公开可见的文档里;把恢复过程放到 YouTube 直播,峰值约 5,000 人观看,一度是全站第 2 的直播;同时用状态账号在推特通报 [S1]。CEO 在复盘里写下「丢失生产数据是不可接受的」并公开道歉 [S1]。
这里也有一条边界值得学:那份文档最初包含了误删数据的工程师的名字——是他本人加上去的,他本人也不介意公开;但 GitLab 表示今后会打码,因为别的工程师未必愿意 [S1]。透明的对象是事实与流程,不是人。
此刻的「秒抢」:约 1 万用户、12 个人、一主一从的 MySQL(第 3 章刚加的从库)。GitLab 那一夜的五层,你现在有几层?诚实的答案通常是一层,而且是第 1 层——那层在 GitLab 的复盘里排第一个失效。
| GitLab 的失效层 | 「秒抢」现在的状态 | 本周就能做的动作 | 关联章节 |
|---|---|---|---|
| 第 1 层 复制 ≠ 灾备 | 刚加的从库,团队心里默认它"就是备份" | 在架构图上把从库标注为「高可用」而不是「备份」;误删数据时从库会忠实地把删除也复制过去 | 第 3 章 |
| 第 2 层 备份静默失败 | 有一个每日 dump 的定时任务,没人看过它的输出 | 给备份任务加成功心跳:超过 26 小时没有新备份就告警。不要用邮件做唯一通道 | 第 14 章 |
| 第 4/5 层 恢复没演练 | 从来没有从备份恢复过 | 本季度做一次计时恢复演练:从备份拉起一个可查询的实例,记录耗时。这个耗时就是你真实的 RTO | 第 16 章 |
| 护栏 不可逆命令 | 生产和预发的终端长得一模一样 | 给生产终端换配色 + 提示符打印主机名与主从角色;危险命令走带二次确认的脚本,不裸敲 | 第 16 章 |
最后留一个问题,它会跟着你走完这本书的后半程:GitLab 那一夜,救命的是一个不属于灾备体系的临时快照。你的系统里,此刻有没有类似的"其实靠运气在兜底"的东西?如果有,它是不是也随时可能因为"这次不需要更新的数据"而消失?
第 16 章我们会给这个问题一个正式的答案:把运气变成演练。
逐句核对清单见构建仓库 sources/FACTS-level1.md;来源原文留档见 sources/raw/gitlab-2017-postmortem.txt。二手文章未被采信为事实依据。