系统设计 31:多地域切换与数据恢复的不同边界
地域切换解决的是服务入口和写入权限的变化;数据恢复解决的是哪些已确认记录仍然存在。DNS 已切到另一座城市,既不能证明旧地域停止写入,也不能证明最后一笔订单没有丢失。多地域设计必须分别给出这三个答案。
本文把教学分享服务扩展到两个地域,采用每个账户只有一个写入归属地域的候选方案。它不描述任何公司的内部架构,也不实现跨地域共识协议。正常情况下归属地域提交元数据,另一地域异步接收日志;只有公开且允许陈旧的统计可以就近读取。
从业务损失反推复制确认
需求卡先区分内容和权限:普通草稿允许灾难后丢失最近五秒更新,撤销权限的成功确认不允许在恢复后被当作从未发生。前者可以接受有界的数据损失目标,后者需要更强的提交条件,或在无法确认撤销状态时拒绝访问。不能用“系统 RPO 五秒”抹平两个业务对象之间的差异。
RPO 是恢复点目标,用时间表达可接受的最近数据损失范围;RTO 是恢复时间目标,从业务中断开始,直到指定业务功能恢复。目标必须说明故障范围:本题只讨论单地域失效、备用地域及控制面仍可用;两个地域同时永久丢失、账号密钥泄露和逻辑误删不由地域复制自动处理。Google SRE 的数据完整性章节讨论备份与恢复的独立价值,复制不能替代可恢复历史。
教学目标是普通写入 p99 小于 250 ms、读取 p99 小于 150 ms,故障后五秒 RPO、六十秒 RTO。它们尚未通过真实部署测量。若公网往返本身需要 120 ms,同步跨地域确认就消耗了很大一部分写入预算;这时只能缩短其他阶段、放宽延迟或改变确认契约,不能把网络延迟从图里删掉。
接口为 PUT /accounts/{id}/drafts/{draft},携带 request_id、expected_version;响应包含 version 和提交地域。路由元数据 ownership(account_id, region, epoch) 与正文 draft(id, account_id, value, version, epoch) 分开。epoch 是单调递增的任期编号,写入存储必须校验当前任期;仅网关缓存一个地域映射不足以阻止旧进程接收写入。任期发放需要一个可靠的排他协调机制,本篇模型将其作为前提,不声称实现了它。
跨地域流量与恢复窗口
假设日新增 864 万条操作、每条日志 400 B,则平均 8640000/86400=100 write/s;八倍峰值为 800 write/s,原始复制流量 800×400=320000 B/s,即 0.32 MB/s。保留三十天、两份日志的逻辑容量是 8640000×400×30×2=207.36 GB,未计协议、索引、快照、压缩和重传。GB 按十进制;换为 GiB 约 193.12。
五秒未复制窗口在恒定峰值下包含 4000 条、1.6 MB 日志。这个数字只是字节上界算例,若单笔交易价值差异很大,金额风险不能按平均记录数换算。链路中断五分钟会积累 96 MB,恢复后如果有效复制能力只有 0.4 MB/s,且新增仍为 0.32 MB/s,净追赶速率只有 0.08 MB/s,至少需要 1200 秒清空。恢复带宽不能只比平均吞吐高一点。
把峰值系数从八改为十六,输入变为 0.64 MB/s,原先链路连新增都追不上;增加备份频率不能修复复制积压。约束改变要求提升链路能力、限制写入或重新协商 RPO。对读多写少的公开内容,可把大对象按内容哈希异步复制,只让小型权限记录走更强确认,但公开对象 URL 仍不得绕开约定的授权边界。
flowchart LR
C[客户端] -->|账户 ID 与幂等键| G[归属路由]
G -->|当前 epoch 写入| P[主地域写服务]
P -->|提交正文及请求映射| D[(主地域存储)]
D -.->|按位点复制日志| B[(备用地域存储)]
O[归属控制面] -->|单调任期| G
O -->|写入栅栏| D
O -->|提升新任期| B
S[历史备份] -.->|独立恢复路径| B
正常确认在主地域提交之后,异步备库尚未收到也可能已经返回成功。因此普通数据出现有界丢失并非实现异常,而是确认契约的结果。若权限撤销也沿同一确认路径,其“灾难后仍然撤销”的承诺就没有依据。可选方案是跨地域同步确认权限记录,或者让灾难恢复后的相关读取停在拒绝状态,直至撤销日志完成核对。
切换顺序决定是否产生两个写入者
故障检测到主地域失联时,不能区分服务器已停止与网络分区。安全切换先限制旧写入能力,再授予新任期,然后开放流量。若无法保证旧地域被隔离,新地域应保留只读或拒绝写入;这会增加 RTO,却避免把两个相互冲突的成功响应交给用户。
sequenceDiagram
participant P as 旧地域
participant O as 故障控制面
participant B as 新地域
participant C as 客户端
P--xO: t=100 链路失联
O->>O: 检测 10 秒
O->>P: 隔离旧任期,预算 5 秒
O->>B: epoch 从 1 提升为 2,预算 12 秒
B->>B: 核对数据与授权,预算 8 秒
B-->>C: t=135 开放受限写入
P->>B: 旧 epoch=1 的迟到提交
B-->>P: 拒绝旧任期
该顺序下恢复耗时是 10+5+12+8=35 秒,不是只统计十二秒提升过程。控制面不可达、连接缓存没有刷新、业务核对失败都会延长恢复。把 DNS TTL 设为十秒,也不能保证所有客户端在十秒内换连接;长连接、连接池和已在途请求都应包含在恢复探针中。
旧地域回归后先作为待核对副本,不能直接重新成为写入主节点。旧日志里未复制的 w96–w100 应进入恢复清单;如果其中的业务操作可重放,要沿原幂等键提交,遇到版本冲突进入人工或领域规则处理。直接把旧文件覆盖到新地域可能覆盖切换后的合法写入。撤销、扣款等动作也不适合任意“最后时间戳获胜”,时钟偏差和业务语义都使它不可靠。
单写归属的好处是每个对象只有一条更新顺序,代价是远端写入延迟与切换协调。双地域同时写适合可独立分区或能明确定义合并的对象,例如各自追加且有唯一 ID 的观察事件;它不自动适合一个座位、一辆司机或一个余额。若九成写入集中在一个国家,单写归属通常已经满足局部性,不必为了“多活”引入业务合并。
可重跑的故障时间线
运行 python3 examples/system-design/labs/31/run.py。整数时间模型写入 w1 至 w100,备用端仅收到 w1 至 w95,在 t=100 注入地域失效。正常检查得到 RPO 五秒、丢失清单五项、RTO 三十五秒;新任期拒绝旧写入。负例关闭任期校验后,旧地域把新地域的值覆盖为 stale-region-value。源码、Python 版本、SHA256 与输出分别保存在当篇 lab 和 examples/system-design/evidence/31/,文章附件提供同一份源码与记录。
模型没有真实网络、数据库复制和 DNS,五秒与三十五秒是输入推导,不是灾备演练实测。它能证伪“路由切换就等于恢复”和“旧地域回来可直接接写”,不能证明任期协调器或某款数据库已经满足这些性质。
面试追问可从三处展开:若 RPO 从五秒收紧到零,成功确认要等谁;若控制面本身失联,允许多少只读降级;若旧地域包含一笔客户已确认的交易,恢复流程怎样保留证据与处理冲突。每个回答应改变确认路径或故障处置,不能只增加一个“容灾模块”。
[PATTERN] 按数据类别确定损失边界,再选择复制确认;按旧写入者能否继续工作决定切换条件。地域数量不是保证,确认点与排他写入才是。
| 约束 | 选择 | 代价与变化条件 |
|---|---|---|
| 普通草稿允许五秒损失 | 异步副本、位点核对 | 复制落后失去上界时限制写入 |
| 权限撤销不可复活 | 更强确认或恢复期间拒读 | 跨地域延迟进入写入预算 |
| 单对象不能双写 | 任期栅栏后提升 | 无法隔离旧主时延长恢复 |
实验附件与导航
可运行实验源码 · 本次原始结果系列导读;容量和数据承诺分别沿用系列的方法,本文数字为独立教学假设。






