专业书籍精读 · SRE · 第 23 章
Site Reliability Engineering · Ch 23 · Google(Laura Nolan 等)· 2016
在群里指定一个值班负责人,只要有人认领,事就有人管;但如果两个人同时以为自己是负责人,各改各的,那才是灾难。一堆机器天天要干这件事:谁是主库?这把锁在谁手上?SRE 第 23 章讲的就是——一群机器怎么才能可靠地「说好同一件事」。
2018 年 10 月,GitHub 东岸机房和网络枢纽之间断了 43 秒。就 43 秒,网站却降级了整整一天。那 43 秒里,东岸和西岸的数据库各自收下一批写入;等网通了,两边都有对方没有的数据,谁也不能简单覆盖谁——只能人工对账。43 秒的「说不上话」,换来 24 小时手工善后。
土办法是:主机每秒喊一声「我还活着」,备机几秒没听见就自己上位。听着合理,但有个死结——听不见对方,你分不清是他死了,还是电话线断了。若只是线断了,你一上位,世上就有了两个「负责人」,这叫脑裂。更阴险的是,它平时看着完全正常,只在你最不希望出问题的那天露馅。
假设有 5 台机器,任何决定必须凑够 3 票才算数。妙就妙在——世上不可能同时存在两拨各自凑够 3 票的人:3 加 3 是 6,比 5 还多,两拨人里必定有人被重复算了,而这个人不会替两件互相矛盾的事各投一票。于是「同时冒出两个负责人」从根上被堵死。断网时人少的那侧凑不够票,就老实停下来——停一会儿能恢复,数据分叉不能。
第二个诀窍是记同一本流水账:所有机器不各记各的账,而是把每一笔改动按完全相同的顺序抄进同一本账。账本和顺序一致,各台机器照着重放,结果就必然一样。
这套东西早有人写好并被折磨了二十年——Google 的 Chubby、开源的 ZooKeeper 和 etcd(你用的 Kubernetes 就把集群状态全存在 etcd 里)。本章最实在的一句是:别自己造。你以为在写「简单的主从切换」,其实在写一个共识算法,而且大概率写错了。
代价也说清楚:每个决定都要等过半数机器点头,所以它天生比单机慢,机器摆得越远越慢——跨大洲问一趟,光速也要来回一百多毫秒。
一群机器要就「谁是老大、现在是什么状态」达成一致,唯一靠谱的办法是过半数投票 + 记同一本流水账;心跳加超时那套土办法迟早脑裂。而这种东西,请用现成的,别自己写。
想进到具体机制、时序图和选型表? → 切到精读版
本章只有一个命题:只要一组进程需要就某个状态达成一致,你面对的就是「分布式共识(distributed consensus)」问题——没有捷径。心跳、超时、时间戳这些轻巧的替代品不是绕开了共识,而是实现了一个错的共识,只是错误要等到网络分区那天才显形。
本章在 SRE 书第 IV 部分「管理复杂系统」,作者 Laura Nolan。前两章(Ch21 处理过载、Ch22 级联失效)讲单个服务怎么别把自己拖垮;本章抬高一层,讲多台机器之间的关键状态怎么才不出错。它是全书最偏理论的一章,正面对应 DDIA 第 9 章;后面的 Ch25、Ch26 都以「状态可靠」为前提。
本章开篇摆了三个真实(脱敏)的故障案例,共同点是都在试图绕开共识:①脑裂——主备靠心跳判活,网络拥塞让心跳超时、备机自行上位,可主机根本没死,两台同时写文件,事后人工对账且有数据永久丢失;②切换必须叫人——另一套系统为避免脑裂规定主只能人工指定,于是可用性被人的反应时间钉死,还背上一堆琐务(toil);③自研组成员算法出错——用 gossip 传成员关系、自研判活,网络异常时集群裂成两个各自认为对方已死的组,双写丢数据。
作者的诊断毫不留情:这些系统本来就需要共识,只是作者不承认。「心跳 + 超时」把一个根本无解的问题——异步网络里「没回话」到底是死了还是慢了——当成了能解的。代价不是「偶尔多切一次主」,而是数据分叉;一旦分叉,没有任何自动手段能替你决定哪一份才是对的。
顺带纠正一个常被念歪的经:CAP 说的是分区期间只能在一致(C)与可用(A)之间选一个,而非平时就三选二。共识系统选 C——少数派那侧宁可拒绝服务也绝不分叉,等于把「脑裂」这个不可逆的后果换成「短暂不可用」这个可恢复的后果。
根子在于:异步网络里,「宕机」和「慢」不可区分。收不到心跳,可能是进程崩了,也可能只是它卡在一次长 GC 或网络拥塞——没有任何外部观测能把两者分开。你只能定一个「等多久算死」,而这个阈值无论定多少,都有把活人判死的那天。这就是 1985 年 FLP 不可能性说的事:完全异步模型里只要有一个进程可能崩溃,就不存在既保证安全、又保证有限时间内出结果的确定性算法。
真实系统的绕法很明确:牺牲「一定出结果」,死守「一定安全」——用超时与随机化(Raft 的随机选举超时就干这个)打破僵局;代价是网络极糟时可能反复选举、迟迟选不出主,但永远不会同时选出两个主。土办法恰好把取舍做反了:超时就上位,于是脑裂。
支柱一:法定人数相交(quorum intersection)。任意两个「过半数」集合必定至少有一个共同成员。5 副本里任何两个 3 人组都必然重叠,而那个重叠的副本记得自己已为上一届投过票,就会拒绝给冲突的新决定投票——两个互不相交的多数派在数学上不存在,脑裂被从根上堵死。这也解释了为什么偏爱奇数:6 副本的多数派是 4,容错力和 5 副本一样,却白多一台机器、多摊一份延迟。
支柱二:复制状态机(RSM)。共识的产出不是「投票选出一个值」这么单薄,而是一份所有副本内容与顺序都一致的操作日志;每个副本按同序执行同样的确定性操作,状态必然一致。本章说得直白:共识是构建块,复制状态机才是你真正想要的东西——数据库、锁服务、配置存储、消息队列,都是同一个 RSM 换了个接口。
Paxos(Leslie Lamport)的骨架是两轮:① 抢提案号——候选者带一个全局递增的编号问过半数副本「我能提案吗」,答应的副本承诺此后不再接受更小的编号,并把已知的最新「已接受值」告诉他;② 提案落定——候选者提出值,但若第一轮收到过任何已接受值,它必须沿用那个值,过半数接受即定案。「必须沿用旧值」就是共识不会前后打脸的全部秘密:新多数派一定与旧多数派重叠,所以看得见旧决定,也就推翻不了它。
Multi-Paxos 是实用版:每条日志都跑满两轮太贵,于是推举一个稳定领导者(stable leader)一次性拿下第一轮,之后每条只需一轮,把一次写入压到 1 个网络往返 + 1 次 fsync。Raft(2014)把同一件事重组成「领导者选举 + 日志复制 + 安全性」,并把可理解性当设计目标——它不比 Paxos 更快或更安全,但工程师读得懂、写得对,这本身就是可靠性;etcd、Consul、TiKV、Kafka 的 KRaft 都走 Raft,Zab 则是 ZooKeeper 的变体。
还有一条常被忽略的量级差异:这些算法只防宕机故障(节点会死但不会撒谎),容忍 f 个故障需 2f + 1 个副本;要防拜占庭故障(Byzantine,节点还会发矛盾消息骗人)则陡增到 3f + 1。数据中心内部几乎都只做前者。
IdRegistry,跨机房保证广告点击与查询的连接不重复计费。那个致命细节:分布式锁必须是租约,而且必须带递增序号。持锁者可能被长 GC、虚拟机挂起或网络问题冻住而毫不知情;等它醒来,锁早已过期、被别人拿走,可它还以为持着。解法是让锁服务发一个单调递增的序号(sequencer / fencing token),下游存储必须校验、拒绝旧序号的写入。光有锁不等于正确性——这是本章最值得贴在墙上的一条。
本章专门反驳一句流传甚广的话:共识太慢、吞吐太低,扛不住正经业务。它确实比单机贵,但贵在两个可预测的地方:网络往返——一次提交至少要等过半数副本回话,写延迟下限就是到「第 3 快的副本」(5 副本时)的往返时间,同机房 RTT 通常亚毫秒、同地域跨机房几毫秒、跨大陆几十毫秒、跨洋一百多毫秒,这是光速定的;磁盘落盘——日志得 fsync 才敢确认,机械盘一次通常几毫秒,SSD / NVMe 可压到亚毫秒。
优化手段也就那么几招:稳定领导者省掉第一阶段;批处理把多条提案合并成一次落盘一次广播,吞吐涨、单条延迟略增;流水线不等上一条确认就发下一条但严格保序,在高带宽高延迟链路上收益最大;Fast Paxos 让客户端直接发给所有副本、省掉绕经领导者那一跳,代价是更大的法定人数;法定人数租约(quorum lease)让部分副本对部分数据持读租约,从而本地直接读还保持强一致;最后,领导者本身是吞吐瓶颈,那就按分片切成多个共识组——Spanner 就是每个 tablet 一个 Paxos 状态机。
几个。n 个副本容忍 (n-1)/2 个故障。Google 的默认是 5,理由非常实际——要能在一台副本做计划内维护的同时,仍扛得住另一台意外挂掉;3 副本做不到,维护期间等于零冗余。再往上容错涨得慢、延迟与成本涨得快。
摆在哪。铁律是多数派不能落在同一个故障域里:5 副本分 3 个机房(2 + 2 + 1)能扛住任一机房整体失联,全塞进一个机房则机房一挂全完;但分得越开写延迟越高。领导者应靠近写请求最密处。GitHub 那次事故的另一半教训正在此:把自动提主放到跨大陆,等于允许系统在最坏时刻做出一个延迟与一致性都很糟的决定。
盯什么。健康副本数、是否有领导者与领导者变更频率、日志序号与各副本落后程度、提案与接受数、吞吐与延迟分布、RTT 与 fsync 等待。经验判据:领导者频繁抖动、或某个副本持续落后,通常比「整体延迟略高」更早预示故障——它说明系统已在反复重选,只是还没塌。
表 1 · 自研「心跳 + 超时」 vs 用现成共识系统
| 自研心跳 + 超时 | 现成共识系统(Chubby / ZooKeeper / etcd) | |
|---|---|---|
| 平时表现 | 一样好,甚至更快(少一跳) | 多一个网络往返,略慢 |
| 网络分区时 | 可能双主 → 数据分叉,不可自动恢复 | 少数派侧拒绝服务,永不分叉 |
| 故障切换 | 要么误切,要么只能叫人 | 自动,通常十秒级内完成 |
| 成员变更 | 自研里最容易写错的部分 | 算法内建、已被大规模验证 |
| 运维成本 | 看着是零,实为隐性债务 | 要多养一个有状态集群 |
表 2 · 副本数与摆放:容错、延迟、代价
| 配置 | 容忍故障 | 写延迟量级 | 评价 |
|---|---|---|---|
| 3 副本 | 1 台 | — | 小规模够用,但维护窗口内零冗余 |
| 5 副本 | 2 台 | — | Google 默认;本章推荐值 |
| 4 / 6(偶数) | 与低一档奇数相同 | — | 纯浪费:4 副本仍只容忍 1 台 |
| 全在单机房 | 机器 / 机架故障 | 亚毫秒 RTT + fsync | 机房整体故障 = 全挂 |
| 同地域 3 机房 | 任一机房失联 | 几毫秒 + fsync | 通常是最佳折中 |
| 跨大陆 / 跨洋 | 区域级灾难 | 几十至一百多毫秒 | 写延迟被光速钉死,交互模型要重设计 |
表 3 · 你想干的事 → 该用哪种形态
| 需求 | 正确形态 | 常见错法 |
|---|---|---|
| 选出唯一一个主 | 共识系统里的领导者选举 | 自研心跳 + 虚 IP → 脑裂 |
| 存配置 / 服务发现 | 复制配置存储(低容量、高价值) | 把大量业务数据也塞进 ZooKeeper |
| 互斥访问某资源 | 带递增序号的租约锁 | 拿到锁就当自己一定还持有,下游不校验 |
| 任务只执行一次 | 共识之上的队列,或唯一 ID 注册表 + 幂等 | 靠「应该不会重复吧」 |
| 跨机房强一致状态 | 跨故障域摆放的共识组 | 用时间戳比大小做冲突裁决 |
这一章对应一道几乎必考的架构题:「你怎么实现高可用的主从切换?」标准答案不是「心跳加虚 IP」,而是把选主交给一个共识系统,并在下游做序号校验(fencing)。今天你身边的系统几乎都是它的实例:Kubernetes 把全部集群状态放在 etcd(Raft);ZooKeeper(Zab)长期充当 HBase、早期 Kafka 的协调层;CockroachDB、TiKV 把「每个数据分片一个独立 Raft 组」做成标配;Spanner 每个 tablet 一个 Paxos 组。
100–1000 个 tablet——印证「按分片切成多个共识组」是绕开领导者吞吐瓶颈的正解。Corbett et al.《Spanner》, OSDI 2012 ↗IdRegistry 跨机房去重,官方描述其生产部署「峰值每分钟处理数百万事件、平均端到端延迟低于 10 秒」,且能在机房级故障下无需人工介入——直接反驳「共识太慢,不能用在正经系统上」。Ananthanarayanan et al.《Photon》, SIGMOD 2013 ↗① 只要一组进程要就状态达成一致,你面对的就是分布式共识;「心跳 + 超时」不是绕开它,而是实现了一个错的它。
② 根因:异步网络里「宕机」与「慢」不可区分;FLP 说明没法同时保证安全与「一定出结果」,真实系统牺牲后者、死守前者。
③ 两根支柱:过半数法定人数(任意两个多数派必定重叠,脑裂在数学上被堵死)+ 复制状态机(同一本日志、同一顺序、确定性执行)。
④ 算法:Paxos 两轮,「必须沿用已接受值」是不反悔的秘密;Multi-Paxos / Raft 用稳定领导者把一次写入压到 1 个 RTT + 1 次 fsync。防宕机要 2f+1 副本,防拜占庭要 3f+1。
⑤ 五种形态:复制状态机 / 配置存储 / 领导者选举 / 协调与锁 / 可靠队列。锁必须是租约 + 递增序号且下游校验——光有锁不等于正确性。
⑥ 性能:慢在网络 RTT(同机房亚毫秒、跨洋一百多毫秒)与 fsync 两处;批处理、流水线、按分片切成多个共识组是标准解法。
⑦ 部署:5 副本是推荐值;多数派绝不能落在同一故障域;领导者靠近写最密处。监控优先盯领导者变更频率与副本落后程度。
⑧ 最实在的一句:别自研共识。用 Chubby / ZooKeeper / etcd / Consul,把力气花在副本数、摆放位置和 fencing 上。