系统是被打出来的 · 系统设计实战手册
PART 4 · 高可用与多活 · 第 15 章

异地多活
以及脑裂

容灾的目标不是让两边都亮着灯,而是在网络断开、时钟分叉和自动切换同时发生时,仍能证明谁有资格写。演练当天,两座机房都宣布自己是主——这份账单叫脑裂。

15 · 1 值班现场14:07,演练变成了真故障

创业第 5 年 10 个月,「秒抢」累计用户约 7000 万,公司 295 人,研发 75 人。周四下午进行第一次机房级容灾演练:计划断开上海与杭州之间的业务链路,验证 5 分钟内切换。14:07,两边的「PRIMARY」指示灯同时亮起。

Disaster Recovery · P0 · 触发于 14:07:11
[CRITICAL] dual-primary detected:shanghai=WRITABLE / hangzhou=WRITABLE
[CRITICAL] order sequence conflict 1,284 keys,仍在增长
[WARNING]  cross-region replication lag unreachable
──────────────────────────────────────
演练控制台:
「两侧自动切换任务均返回:本地多数派成立,已提升为主」

演练脚本原本只该切业务流量,却同时触发了两套独立的本地提升逻辑。上海看不见杭州,于是按本地 3 个节点中的 2 个投票成为主;杭州也把自己的 2 个节点临时重配成新成员集,并按 2/2 成为主。固定成员集被拆成两份,每份都给自己发了写入许可证。

上海写入
7,800/s
▲ token 41
杭州写入
5,100/s
▲ 本地 epoch 1
冲突订单键
1,284
▲ 每秒新增约 160
复制链路
双向中断
● 无共同写入围栏

两边都能服务用户,成功率甚至维持在 99% 以上;但同一场次的最后一批库存被各自扣减,同一用户的订单状态也在两边向不同方向推进。现在每多一秒正常流量,就多约 160 个需要人工或规则仲裁的业务键。

这不是「等网络恢复后自动同步」能解决的问题。复制只能传递两个已经发生的事实,不能决定哪个事实有权覆盖另一个。你必须先让至少一边失去写资格,冲突集合才会停止增长。

A会把分叉变成覆盖冲突。网络恢复只让差异相遇,不会产生业务仲裁规则。若复制按最后写入时间覆盖,时钟漂移还可能让较旧业务事实看起来更新。先围栏,再复制与合并。

B只围住了前门。DNS 有缓存与传播延迟,杭州本地消费者、定时任务和内部调用仍能直写。流量切换应该配合存储 fencing;没有下游拒绝,旧主仍可能在网络恢复后补写。

C会按每秒 160 个键扩大人工账单。按地域接受订单只有在单元之间数据与库存互不重叠时才安全;本次两边共享同一场次和用户空间,继续双写就是继续出售同一份权利。

D是这一步的正解:先恢复单一写入资格。入口停止新请求,存储层再用 token 或只读开关拒绝旧主写入,防止绕过入口的消费者和补偿任务继续修改;保全两边日志后,按固定成员配置确认上海持有合法 3/5 多数派。

15 · 2 原理活着不等于有资格写

多活最难的不是启动第二套系统,而是网络断开时仍维持单一裁决。对于共享同一份业务事实、要求强一致写入的固定成员集,核心约束是:

多数派门槛 q = ⌊ N / 2 ⌋ + 1
交集性质  任意两个多数派 Q₁ ∩ Q₂ ≠ ∅
可用性边界 最多故障 ⌊( N − 1 ) / 2⌋ 个节点时仍能凑出多数派

请注意第二行才是安全性的来源,而且它常被记错:固定 5 节点集群里并不是"只存在一个多数派"——任取 3 个节点都算多数派,一共 C(5,3) = 10 个合法子集,其中很多可以同时在线。让写入安全的不是"多数派唯一",而是任意两个多数派必然相交:那个交集里的诚实节点不会在同一任期为两个主投票,冲突历史因此无法各自拿到多数确认。

交集性质也只是必要条件,不是充分条件。要真正保证写安全,共识协议还必须做到:同一任期不重复投票、提交与日志匹配规则拒绝冲突历史、成员变更走安全的联合共识、以及旧主恢复后被任期检查或 fencing token 拒绝(本章 D1 就是这一条的现场版)。

第三行则要单独看:⌊(N−1)/2⌋ 描述的是可用性边界——还能凑出多数派、因而还能继续服务的故障上限。超过它时,正确的系统应当停止写入,此时安全性依然成立,只是不再可用。把它当成"安全性的故障上限"是本节最容易犯的错。代入节点数:

3 节点
容忍 1 个故障
⌊(3−1)/2⌋ = 1
3 节点多数派
2 票
只剩 1 节点时停止写
5 节点
容忍 2 个故障
⌊(5−1)/2⌋ = 2
5 节点多数派
3 票
2 节点侧不能写

本次原始成员是 5 个:上海 3、杭州 2。链路断开后,上海 3/5 可以形成多数派,杭州 2/5 应停止强一致写。事故发生在杭州自动化没有保持原成员配置,而是把本地 2 节点重建成新的 2 节点集群;两个成员宇宙各自拥有多数,交集性质被配置变更绕开。

因此成员变更本身必须走共识,不能在网络分区时由两边独立「紧急重配」。若确需灾难恢复中重建集群,要先通过外部围栏撤销旧集群写资格,再授予新集群更高 fencing token。可用性可以暂时降低,写入权不能同时复制。

容灾系统最重要的开关不是「提升为主」,而是「让旧主再也写不进去」。没有围栏的切换,只是在相信旧世界会及时听话。

RTO 与 RPO:恢复多快,愿意丢多少

指标 定义 秒抢示例 容易混淆的点
RTO 从故障发生到业务恢复到目标服务水平的最长时间 核心查询 5 分钟恢复,出票 30 分钟追平 不是机器启动时间;要包括切流、校验和依赖恢复
RPO 恢复后可接受的数据丢失时间窗口 已付款订单 RPO=0;推荐点击可接受分钟级 不是复制延迟本身,而是业务愿意承担的丢失上限

假设 14:00 故障,14:04 恢复只读查询,14:09 恢复下单,那么核心写链路实际 RTO 是 9 分钟,不是实例在 14:02 启动所花的 2 分钟。若最后可恢复的订单日志停在 13:59:50,恢复点丢了 10 秒写入,实际 RPO 是 10 秒;对已付款订单,目标 RPO=0 意味着还要用支付账单与幂等日志补齐。

RTO 与 RPO 都要按业务能力分层。目录浏览可以先恢复并接受短时旧数据,库存扣减必须等写入资格和状态核验完成;把整站写成一个「RTO 5 分钟」,会让最快的页面掩盖最慢的资金链路。

三档多活,不是同一个名词的豪华版

档位 流量与数据 主要收益 主要账单
同城双活 两机房同时接流量,低延迟同步或共享主写 机房故障切换快,网络时延较可控 仍受城市级灾害与共享网络故障影响;双写权要明确
异地多活 多城市接流量,跨城复制,写策略按业务分级 覆盖城市级故障,用户可就近访问 延迟、复制窗口、冲突仲裁和运营成本显著增加
单元化 用户、库存与订单按单元归属,每单元尽量闭环 故障与热点限制在单元,跨城双写减少 路由、用户迁移、全局查询与单元间交易更复杂

单元化解决本次脑裂的前提,是同一业务键只归一个单元:上海用户与杭州用户若购买不同库存配额,分区时可以各自继续;若两边仍竞争同一场次最后 100 张票,地域标签没有消除共享写入事实。单元边界必须落到数据和配额,而不只是流量入口。

跨单元操作要显式承认代价。用户迁移需要冻结写入、复制状态、切路由再校验;全局订单搜索可以异步聚合,允许短时旧;支付与库存扣减则留在归属单元,避免一次交易同时要求两个城市达成同步事务。

fencing token:让下游识别旧主

第 11 章已经看到,锁过期后旧持有者可能醒来继续写。多活切换是同一问题的机房级版本。fencing token 为每次合法获得写权的主分配单调递增编号,下游记录自己见过的最大 token,并拒绝更小编号的写。

上海旧主
token 41
网络恢复后补发
杭州新主
token 42
已获新租约
存储已见最大值
42
写入护栏
旧主写入结果
拒绝 41
41 < 42

token 必须由具备单调性与持久性的权威发放,下游也必须真正校验;把 token 只写进日志,没有形成围栏。若某个旁路消费者不校验,它就是旧主重新进入系统的后门。

分叉数据不能靠最后写入时间草率合并

网络恢复后,两边会带着同一业务键的不同版本相遇。按 last_write_wins 合并看似简单,却依赖跨机房时钟可比较;附录 No.17 会说明时钟漂移如何让时间轴自相矛盾。订单、支付和库存更需要业务仲裁:

数据类型 冲突示例 仲裁原则 恢复动作
订单状态 一边 PAID,一边 CANCELLED 按支付权威账与状态机版本判断,不按墙上时间 保留证据,生成补偿状态
库存 两边扣了同一份配额 以预先分配的单元配额或合法 token 为准 冻结受影响场次,逐笔履约与补偿
用户偏好 两边修改不同字段 可按字段合并,冲突字段再用版本策略 自动合并并记录审计
统计计数 两边各自累加 若操作可交换,可按增量合并 重算或使用可合并数据结构

第 3 章的复制延迟在同机房里制造了「写完暂时读不到」;跨机房后,延迟会扩到秒级甚至分钟级,网络分区还会让「暂时旧」升级为「双边各自写」。异地多活不是读写分离加一条更长的网线,而是必须重新定义哪些数据允许分叉、哪些写入需要多数派。

15 · 3 病历卡两条都在前进的时间线

脑裂

病历卡 No.14
Split Brain · 两边同时认为自己有写入资格
机理
网络分区、错误成员变更或失效围栏让两个节点组同时自认主节点,并接受同一业务空间的写入;复制恢复后出现无法自动判定权威的冲突事实。
症状
██‖██ 双线分叉型。两地写入吞吐都不为零、各自成功率正常,同一业务键的版本号或状态却沿两条线并行增长;复制恢复时冲突集中暴露。
易发场景
跨机房链路中断;两地独立自动提升;分区时紧急重配成员;DNS 只切流量但旧主仍可写;旧 token 未被下游拒绝。
处方
① 固定多数派:成员变更也走共识;② 双层围栏:入口停流 + 存储 token 拒绝;③ 单元化配额:减少跨城共享写;④ 业务仲裁:分叉后按支付、库存与状态机证据合并。
案例
「秒抢」容灾演练误触发两套本地提升逻辑,上海与杭州同时写入共享订单空间;冲突键以约 160/s 增长。团队围栏杭州写入、保全双边日志,并按合法多数派与业务账本逐笔收敛。

██‖██ 与常见宕机曲线不同:两边都活着、都忙、都认为成功。诊断钩子不是某条线掉下去,而是同一业务键出现两个并行版本。关卡四会把这个场景变成交互推演,让你在恢复速度与写入安全之间亲自做选择。

15 · 5 权衡同城双活,还是异地多活

容灾不是地理距离越远越好。距离购买更大的故障隔离,也带来更长复制延迟、更难写入仲裁和更昂贵演练。每一个都是对的,每一个都有账单。

决策点 D2

「秒抢」下一阶段应选择哪种多活形态?

A · 同城双活:链路延迟低,强一致多数派与切流更容易达到分钟级 RTO。账单:城市级电力、网络或灾害仍可能同时影响两处;共享依赖会削弱隔离。适合先解决单机房故障、核心写强一致且团队演练经验有限的阶段。

B · 异地多活:覆盖城市级故障,用户可就近访问,区域容量可以互相接管。账单:跨城延迟、复制窗口、脑裂围栏、冲突仲裁和双边运营显著更复杂;不是每类数据都适合双写。适合业务已分级、RTO/RPO 清晰并能持续演练的团队。

C · 单元化异地多活:大多数交易在本单元闭环,故障和热点不易跨区域传播,共享强一致写减少。账单:单元路由、迁移、全局查询和跨单元业务需要新模型,容量配额可能碎片化。适合用户规模大、业务键可稳定归属的长期架构。

「秒抢」的选择:目标采用 C,但分两步走。先把同城双活的多数派、fencing、RTO/RPO 和演练做扎实;再按用户与场次配额建立上海、杭州单元,订单写在归属单元闭环,全局搜索异步聚合。没有通过关卡四式脑裂演练的能力,不进入跨城双写名单。

15 · 6 复盘那场演练后来怎么停下双写

回放 14:07 的正确剧本——恢复服务排在恢复写入安全之后:

14:08

双层围栏。入口暂停该业务写入,存储侧拒绝杭州旧 epoch;冲突键停在 1,436,消费者与定时任务也无法绕过入口继续写。

14:11

确认合法成员集。按演练前 5 节点配置核对投票与 token,上海持有 3/5 多数派;撤销杭州临时成员配置,保全两边 WAL、订单事件和流量日志。

14:19

只读先恢复,写入慢放量。目录和历史订单先恢复;上海写入从 10% → 30% → 100% 放开,每级核对支付、库存与订单不变量,核心写 RTO 实测 12 分钟(14:07 告警 → 14:19 全量放开)。

15:06

仲裁分叉数据。按合法 token、支付账与库存凭证处理 1,436 个冲突键;自动合并低风险偏好,订单与库存逐笔补偿。完成后才恢复跨城复制。

壹
切流不等于切写入资格。DNS 和网关只控制一部分入口;旧主、消费者与任务仍可能写,下游 fencing 才能关上后门。
贰
多数派依赖固定成员宇宙。分区时两边独立重配,会让各自多数派失去交集。成员变更必须受同一共识与外部围栏约束。
叁
RTO 不能以数据分叉为代价。多停几分钟是可计算的可用性账单;让两边继续写会生成需要法律、客服与人工仲裁的正确性账单。

15 · 7 自测网络断开时,谁还能写

Q1 · 看形状诊断

跨城链路中断后,两边写入曲线都保持高位,同一订单键出现两个并行版本:██‖██。这是什么故障?

A。双边同时写、同键版本分叉,是脑裂的诊断钩子。D 很诱人,因为异步复制也会短时不一致;区别是正常架构仍有明确写入权,脑裂时两边都在批准同一业务空间的新事实。

Q2 · 多数派算术

固定成员的 5 节点集群,最多容忍多少个节点故障仍可形成多数派写入?

B。⌊(5−1)/2⌋ = 2,剩余 3 节点仍是多数派。C 会只剩 2/5,不能安全批准强一致写;若把这 2 个节点紧急重配成新集群,必须先围栏旧集群,否则会破坏交集。

Q3 · RTO 与 RPO

14:00 故障,14:09 恢复核心写;恢复点只能还原到 13:59:50。实际 RTO 与数据窗口分别是多少?

A。RTO 看业务恢复到目标水平,用时 9 分钟;可恢复点落后故障时刻 10 秒,数据窗口是 10 秒。D 把机器启动当成业务恢复,忽略切流、依赖与校验;C 只有在补齐这 10 秒数据后才成立。

Q4 · fencing token

下游已经接受 token 42 的新主写入,旧主携带 token 41 在网络恢复后补写。下游应怎么做?

B。单调 token 代表写入资格代际,旧代写入必须被围栏。A 把网络迟到误当成更新;D 会把安全依赖到可能漂移的时钟,正是附录 No.17 要提醒的陷阱。

🎯 面试映射 · 本章直接覆盖的高频追问

「异地多活架构怎么设计?」 答题骨架:业务分级 RTO/RPO → 同城/异地/单元化选择 → 数据归属与写策略 → 多数派和围栏 → 路由与容量接管 → 冲突仲裁 → 演练与回滚。
「脑裂怎么防?」 答题骨架:固定成员集多数派 → 成员变更走共识 → 入口与存储双层 fencing → 单调 token 拒绝旧主 → 单元化减少共享写 → 分叉后按业务账本仲裁。
「RTO/RPO 怎么定?」 答题骨架:按业务影响分层 → RTO 是业务恢复时间、RPO 是可接受丢失窗口 → 映射复制与备份能力 → 计算成本 → 用演练测实际值。反直觉加分点:目标写在文档里不算达成,演练实测才是当前能力。