多机房与异地多活:单元化、RPO/RTO、故障切换、脑裂与回切
“两个机房都部署了应用”不等于双活,“数据库有从库”不等于灾备,“DNS切过去”也不等于业务已经恢复。多机房系统必须同时解决流量入口、服务发现、数据写入归属、复制延迟、消息路由、缓存、任务调度、身份与密钥、故障检测、旧主隔离、切换和回切。
本页以订单、支付和医疗采集平台为背景,解释单地域多可用区、主备、双活、多活和单元化架构的原理与失败窗口。数据库具体复制机制见各数据库专栏,本页关注跨组件的端到端业务正确性。
一、学习目标
- 区分高可用、灾备、主备、双活、多活和单元化。
- 从业务影响倒推RPO、RTO和依赖恢复顺序。
- 解释GSLB/DNS、全局入口、地域网关和单元路由。
- 区分全局数据、单元数据和派生数据。
- 解释同步、半同步、异步复制的延迟与丢失窗口。
- 说明网络分区时为什么必须Fencing旧写主。
- 设计跨机房MQ、缓存、定时任务和分布式锁。
- 完成故障宣告、切流、数据核对和回切。
- 处理读己之写、单调读、全局唯一ID和时钟边界。
- 用演练证明RPO/RTO,而不是只写架构图。
二、先把几个概念分清
| 概念 | 核心目标 | 典型形态 | 常见误解 |
|---|---|---|---|
| 高可用 | 单组件故障快速恢复 | 同城多AZ、副本、自动选主 | 等于异地灾备 |
| 灾备 | 大故障后恢复业务和数据 | 异地备份、备用集群、Runbook | 备机存在就算可恢复 |
| Active-Passive | 主站服务,备站待命 | 数据持续复制,故障切换 | 备站从未演练也可用 |
| Active-Active | 两站同时承接业务 | 分区写入或冲突处理 | 两边可任意写同一数据 |
| Multi-Active | 多地域同时提供服务 | 单元化、全球路由 | 所有数据全球强同步 |
| 单元化 | 用户或业务键固定归属单元 | 路由码、独立应用和数据闭环 | 只是按地域部署Pod |
多活的难点不是应用无状态副本,而是“同一业务事实由谁写、两个分区冲突时谁有效、恢复后怎样合并”。
三、RPO与RTO必须由业务倒推
- RPO:恢复后最多允许丢失多长时间或多少业务数据。
- RTO:故障发生后,业务最多允许多长时间恢复到约定能力。
故障发生时间 = 10:00
最后可恢复数据 = 09:58
实际数据损失窗口约2分钟,RPO约2分钟
业务在10:12恢复
实际恢复时间约12分钟,RTO约12分钟| 业务 | RPO示例 | RTO示例 | 设计方向 |
|---|---|---|---|
| 支付账务 | 接近0 | 分钟级 | 强约束写入、同步确认、对账 |
| 订单状态 | 秒到分钟 | 分钟级 | 复制、幂等、状态机、补偿 |
| 医疗原始采集 | 数分钟或按批次 | 30分钟 | 本地落盘、断点续传、重放 |
| ES搜索视图 | 可重建 | 小时级 | MySQL事实源、CDC重建 |
| 报表 | 小时级 | 小时到天 | 离线重算、备份恢复 |
RPO=0并不是一句配置。跨地域同步提交会增加写延迟,并在远端不可达时面临“停止写入还是降低保证”的业务选择。要求越强,成本和可用性代价越高。
四、架构演进层级
4.1 单地域多可用区
应用和副本跨AZ部署,数据库主从或共识成员分散故障域。它能抵御单Node、机架或AZ故障,但无法覆盖整个地域控制面、网络或账号级事故。
4.2 异地主备
主地域承接流量,备地域持续接收数据、配置、镜像和密钥。故障时将备站提升。结构简单,但备站长期无真实流量容易配置漂移、容量不足和证书过期。
4.3 双活/多活
多个地域同时服务。若两边任意写同一订单,需要全局同步一致、冲突合并或单写所有权;多数商业系统更常按用户、租户或业务键分区写入,而不是所有记录全局多主。
五、单元化架构
一个单元尽量包含完成业务闭环所需的Gateway、服务、缓存、数据库分片和MQ资源。用户或业务键通过稳定路由码归属某个单元。
flowchart TD
A["用户请求进入全局流量入口"] --> B["认证后得到可信tenantId或userId"]
B --> C["路由服务计算unitCode"]
C --> D{"目标单元"}
D -- "华东单元" --> E["华东Gateway、服务、DB分片、MQ"]
D -- "华南单元" --> F["华南Gateway、服务、DB分片、MQ"]
E --> G["本单元完成大部分业务闭环"]
F --> G5.1 路由键
路由键应稳定、可验证且覆盖主要数据访问,例如tenantId、userId或accountId。订单号可编码或查询其归属单元。不能完全信任客户端传入X-Unit,应从认证身份或服务端映射得到。
5.2 为什么要让数据闭环
若用户在华东单元,但每个请求仍跨地域访问华南主库,网络分区时单元无法自治,延迟也受跨地域RTT影响。单元化应让主要读写、消息和缓存留在归属单元,只把必要全局信息复制或聚合。
5.3 跨单元业务
转账、跨租户协作等会跨单元。不能直接假装成一个本地事务,应使用全局业务流水、幂等、状态机、可靠消息、补偿和对账;必要时由专门全局服务协调。
六、数据先分类再谈复制
| 数据类别 | 示例 | 典型策略 |
|---|---|---|
| 单元事实数据 | 用户订单、租户资产 | 归属单元单写,异地复制 |
| 全局强约束数据 | 全局账户、唯一配额 | 共识/单主/专门全局服务 |
| 全局配置 | 路由表、产品配置 | 有版本发布、签名、缓存 |
| 派生数据 | ES、报表、推荐 | 事件同步,可重建 |
| 临时数据 | Session、缓存 | 可失效重建,事实源兜底 |
把所有数据都做跨地域强一致,会让每次提交承担WAN延迟和分区可用性代价;把所有数据都异步复制,又无法满足余额、唯一配额等约束。分类决定方案。
七、复制确认点决定丢失窗口
flowchart TD
A["应用向主地域提交事务"] --> B["主库本地日志持久化"]
B --> C["复制日志发送到远端"]
C --> D["远端接收并持久化"]
D --> E["远端回放并对外可读"]客户端在哪一步收到成功,决定故障窗口:
- 本地持久化即成功:延迟低,主站永久故障时未复制部分可能丢失。
- 等远端收到/持久化:RPO更小,但WAN抖动影响提交。
- 等远端Apply:可见性更强,延迟和可用性成本更大。
“同步复制”也必须问清同步到哪里、需要几个副本ACK、超时后怎样降级。复制延迟指标不能只看秒数,还要看日志位点、字节差和最老未复制事务。
八、多地域读取的一致性问题
8.1 读己之写
用户在主地域提交订单后被GSLB路由到远端只读站,复制尚未到达,会看到订单不存在。可选方案:
- 会话在短窗口内粘到写入单元。
- 响应返回写位点,读取等待副本追到该位点。
- 核心状态读主/归属单元。
- UI明确显示“处理中”并按业务流水查询。
8.2 单调读
用户已经看到PAID,随后读到延迟副本的PENDING,会发生状态倒退。可用版本号、状态机序号、会话位点或路由粘性防止返回更旧版本。
8.3 时钟不能决定业务真相
跨机房时钟会漂移,不能只用update_time较大者覆盖另一边。应使用数据库提交序、业务版本、全局流水、向量/混合逻辑时钟等与场景匹配的顺序依据。
九、脑裂与Fencing
网络分区时,旧主可能仍能服务本地客户端,新站又被提升为主。如果两边同时写相同数据,就会形成冲突事实。
flowchart TD
A["主备之间网络中断"] --> B["控制面决定提升备站"]
B --> C["先隔离旧主网络、存储或写权限"]
C --> D["颁发更高Epoch或Fencing Token"]
D --> E["新主写入必须携带新Epoch"]
E --> F["数据库或外部资源拒绝旧Epoch写入"]
F --> G["确认安全后切换业务流量"]仅在新站执行“promote”不够。必须证明旧主不能再写:STONITH、撤销租约、网络隔离、存储Fencing、数据库只读和Epoch校验都可能是手段。Fencing检查必须落在真正副作用资源上,不能只在客户端内存判断。
十、GSLB、DNS和全局入口
全局入口可按地域、健康、容量和策略把用户引向站点。DNS切换有缓存链:权威DNS、递归DNS、OS/JVM、客户端和已有连接。TTL降低只能减少部分缓存时间,不能强制所有客户端立即刷新。
已有HTTP/2、gRPC、WebSocket连接不会因DNS记录改变自动迁移。还要处理:
- 全局LB健康检查是否覆盖真实业务。
- 站点入口和内部依赖是否同时健康。
- Session和Token能否跨站验证。
- 证书、WAF规则和密钥是否同步。
- 流量切入速度是否超过备站预热容量。
十一、跨机房MQ
常见方案:
- 每单元独立Topic/集群,本单元生产消费,异步复制必要事件。
- 全局MQ跨地域复制,但要明确Leader位置和WAN故障行为。
- Outbox/CDC把事实事件从归属数据库同步到其他单元。
跨站复制可能重复、延迟和乱序。消费者必须使用稳定eventId、业务版本和幂等状态机。故障切换后不能从旧、新两个集群同时消费却共用不安全副作用。
消息Lag应按单元、Topic、Partition和复制链路观察。切换前记录生产位点,切换后核对缺口、重复范围和最老事件。
十二、缓存、任务与锁的多活边界
12.1 Redis缓存
缓存可以每单元独立,数据库事实变化通过事件失效。不要把跨地域Redis复制当强一致账本;切换后允许缓存丢失并有界重建,防止全量缓存雪崩回源。
12.2 定时任务
两个集群中的CronJob、XXL-JOB执行器可能同时运行。任务本身要有业务幂等、分片归属和Epoch/Fencing;“备站平时关闭Scheduler”也要确保切换时配置状态可靠。
12.3 分布式锁
两个地域各自Redis锁无法提供全球互斥。核心正确性应由归属单元、数据库唯一约束、状态机和Fencing保证。跨WAN强一致锁会增加延迟和分区不可用性,应先重新设计数据所有权。
十三、全局ID与路由信息
Snowflake类ID需要唯一workerId和时钟回拨保护;数据库号段要按单元分配;UUID全局碰撞概率低但不携带路由信息。
商业系统可把unitCode编码在独立字段或受版本管理的ID结构中。不能永远依赖位段不变,解析器应识别版本;敏感地域和租户信息也不应直接暴露在公开ID中。
十四、故障切换不是一个按钮
flowchart TD
A["监控发现主站异常"] --> B["多证据确认影响与故障范围"]
B --> C["冻结发布、任务和高风险写入"]
C --> D["Fencing旧主并记录复制位点"]
D --> E["评估数据缺口和备站依赖健康"]
E --> F["提升数据库、MQ与必要控制面"]
F --> G["小流量切入并验证核心业务"]
G --> H["分批放量并持续核对业务事实"]切换顺序应由依赖图决定,通常先恢复身份、密钥、配置、数据库、MQ、缓存基础能力,再开放应用流量。应用先切过去但数据库仍只读或证书过期,会把基础设施故障变成大量业务错误。
故障宣告需要明确权限和证据。自动切换速度快,但误判会造成脑裂;人工切换稳妥但RTO更长。可采用自动检测、自动准备、关键提升动作双人确认的分级策略。
十五、回切比切换更容易被低估
旧主恢复后不能直接把流量切回:
- 保持旧站隔离,确认没有继续接受写入。
- 以新主为事实源重建或追平旧站。
- 比较日志位点、行数、金额、状态分布和业务对账。
- 恢复复制方向并观察稳定性。
- 小流量读验证,再开放低风险写。
- 分批回切,保留快速停止能力。
- 处理切换期间重复消息、缓存和任务状态。
直接反向复制可能把旧主分叉数据覆盖新事实。冲突数据应进入隔离、业务核对和可审计修复。
十六、JDK 8 Demo:单元路由与Epoch Fencing
public class UnitRoutingDemo {
static String route(long tenantId) {
return tenantId % 2 == 0 ? "unit-east" : "unit-south";
}
static final class FencedRecord {
private long maxEpoch;
private String value;
synchronized boolean write(long epoch, String newValue) {
if (epoch < maxEpoch) return false;
maxEpoch = epoch;
value = newValue;
return true;
}
}
public static void main(String[] args) {
System.out.println("tenant 1001 -> " + route(1001));
System.out.println("tenant 1002 -> " + route(1002));
FencedRecord record = new FencedRecord();
System.out.println("old primary epoch 7 -> " + record.write(7, "old"));
System.out.println("new primary epoch 8 -> " + record.write(8, "new"));
System.out.println("old primary retries -> " + record.write(7, "stale"));
System.out.println("final=" + record.value + ", epoch=" + record.maxEpoch);
}
}取模只是教学路由,生产需要可扩容的一致映射、迁移状态和版本;内存Fencing也只是演示,真实检查必须在数据库、存储或外部副作用端原子执行。
十七、商业场景:医疗采集平台异地容灾
- 采集Agent先把原始报文和sequence写本地有界磁盘队列。
- Agent按医院归属unitCode连接主站,eventId保证重传幂等。
- 主站原始数据落库并写Outbox,异步复制到灾备站。
- 灾备站持续校验最后sequence、对象存储校验和与数据库位点。
- 主站故障时先停止旧站接入凭据和任务所有权,再提升灾备Epoch。
- Agent抖动重连到灾备站,从最后确认sequence续传。
- 恢复后按医院、批次和sequence对账,不把重复报文直接删除掩盖证据。
此设计的RPO不是一句“异步复制约几秒”,还取决于Agent本地队列是否持久、确认点在哪里、对象存储是否完成、Outbox是否提交和灾备是否可读取。
十八、生产Runbook
18.1 主站疑似故障
- 区分入口、应用、数据库、网络和整个Region故障。
- 检查是否只有部分运营商或地域访问失败。
- 记录数据库、MQ、CDC和对象存储位点。
- 判断旧主是否仍能写,切换前完成Fencing。
- 评估备站容量、证书、配置和依赖健康。
18.2 切换后订单查不到
- 按orderId解析/查询归属单元。
- 查看复制位点和切换时间,判断是否落在RPO缺口。
- 检查请求是否被错误路由到非归属单元。
- 查询Outbox、MQ和业务流水,不能只看缓存。
- 进入待确认和对账,不创建重复订单。
18.3 两边都出现写入
- 立即停止低Epoch或无法证明所有权的一侧。
- 保留双方日志、事务位点和业务流水。
- 按业务键识别冲突集合,不直接全量覆盖。
- 资金和医疗事实进入人工审计或确定性合并。
- 修复Fencing后才恢复写入。
18.4 DNS已切但仍有旧站流量
- 检查权威、递归、OS/JVM和应用缓存。
- 区分新连接和已有HTTP/2、gRPC、WebSocket连接。
- 在旧入口返回可识别迁移响应或主动Drain。
- 不依赖TTL保证瞬时全量切换。
十九、灾备演练验收
必须实际测量:
- 从故障注入到检测、宣告、Fencing、切流各阶段耗时。
- 恢复后最后可见业务流水,计算实际RPO。
- 核心接口恢复到多少容量,计算实际RTO。
- 复制缺口、重复消息、任务双跑和缓存重建。
- 回切耗时与冲突数据处理。
- 人员权限、Runbook命令、审计和沟通链路。
只做“备库能启动”不算灾备演练;必须从真实客户端入口完成一笔可核对业务,并完成回切闭环。
二十、常见误区
| 误区 | 后果 |
|---|---|
| 两站部署应用就是双活 | 数据仍单点,远端无法闭环 |
| 从库就是备份 | 误删会复制到从库 |
| DNS切换是瞬时的 | 缓存和长连接继续访问旧站 |
| 两边都可写更可用 | 无所有权和合并规则会脑裂 |
| promote新主即可 | 旧主未Fencing仍可写 |
| 异步复制一定只丢几秒 | 大事务、网络和回放延迟扩大缺口 |
| Redis锁能全球互斥 | 各地域锁状态独立且复制不强一致 |
| 切换成功就结束 | 还需数据核对、积压处理和回切 |
| 架构文档写RPO/RTO就达标 | 必须用真实演练测量 |
二十一、面试回答主线
先区分HA、灾备、主备和多活;按业务定义RPO/RTO;用单元化说明流量和数据归属;解释复制确认点与读一致性;网络分区时先Fencing旧主再提升新主;切流还要考虑DNS缓存、长连接、MQ、缓存和任务;最后讲数据核对、渐进回切和灾备演练。
标准回答见多机房与异地多活面试题。
二十二、关联知识点
本章小结
多活的核心是业务写所有权和故障状态机,而不是机器数量。真正可用的方案必须让流量路由、数据归属、复制确认、Fencing、消息、缓存、任务、切换和回切形成闭环,并通过真实业务演练证明RPO/RTO。
