“两地三中心"做到最后,往往变成"两个机房 + 一个只读备库”——灾难真来时切不过去。多区域双活的难点从来不在"部署两套",而在流量怎么分、数据怎么同步、冲突怎么解、切流怎么练。本文聚焦区域层(Region)的设计取舍,与单元化(Cell)这一更细的故障域配合,构成完整的容灾体系。
1. 双活的定义与常见误区
1.1 三种容灾形态
先把概念对齐,“双活"经常被当成"多活"的同义词滥用:
| 形态 | 流量 | 数据 | 恢复方式 |
|---|---|---|---|
| 主备 | 全部走主 | 主写,备同步复制 | 手动/自动切换 |
| 双活(Active-Active) | 两区域都承载流量 | 双向同步 | 无需切换,流量自动分流 |
| 多活 | 多区域都承载流量 | 分片或双向同步 | 无需切换 |
真正的双活有三个硬指标:两个区域都在对外提供服务(不是冷备)、任一区域故障另一区域能接管全量(不是只能接一半)、接管过程对用户影响可控(秒级到分钟级)。
1.2 “双活"的三个必要条件
很多号称双活的架构,实际只满足一条。三个条件缺一不可:
条件一:流量可调度
└─ 能把任意区域、任意比例的流量导到另一个区域
条件二:数据可写双活
└─ 两个区域都能接受写,且冲突有确定性的解决策略
条件三:容量有冗余
└─ 单区域容量 ≥ 总容量,否则接管时必然过载
条件三最容易被忽略:如果每个区域只按 50% 容量建设,A 区域故障后 B 区域要承接 100%,直接被打垮——这叫"双活"实为"双半活”。真正的双活要么每个区域按 100% 容量建设(成本高),要么接受降级(关闭非核心功能)。
2. 流量调度与就近路由
2.1 GSLB 与 DNS 调度
区域级流量调度的入口是 GSLB(全局负载均衡),它通常通过 DNS 解析实现:
用户请求 api.example.com
│
├─ 本地 DNS 递归查询
│
├─ GSLB 决策:
│ ├─ 健康检查:哪个区域可用?
│ ├─ 地理就近:用户 IP 属于哪个区域?
│ ├─ 容量权重:各区域当前负载?
│ └─ 策略:主备 / 加权轮询 / 故障转移
│
└─ 返回某区域的入口 IP(TTL 30~60s)
GSLB 的核心约束是 DNS TTL:TTL 越长,故障切换越慢(客户端还在用旧 IP);TTL 越短,DNS 查询压力越大。实践中常用 30~60 秒,配合客户端重试来缩短实际影响时间。
2.2 调度的三种模式
| 模式 | 策略 | 适用 | 切换速度 |
|---|---|---|---|
| 主备 | 全部流量走主,故障切备 | 数据强一致要求高 | 分钟级 |
| 加权 | 按比例分流(如 70/30) | 灰度、容量均衡 | 秒级(改权重) |
| 就近 | 按用户位置分流 | 低延迟优先 | 无需切换 |
就近 + 加权是双活最常见组合:默认按用户地理就近接入(降延迟),同时用权重控制各区域负载比例。故障时把故障区域权重调为 0,流量自动转移到健康区域。
2.3 调度的可观测性
流量调度最怕"以为切了实际没切”。必须有独立的观测确认:
# 从多个地理位置探测,确认调度生效
for region in cn-east cn-north ap-southeast; do
echo "=== from $region ==="; dig +short api.example.com @${region}-resolver
done
# 期望:cn-east -> 10.1.1.1(east 入口),cn-north -> 10.2.1.1,依此类推
踩坑点:GSLB 健康检查如果只探测入口 IP 的 TCP 端口,会在"进程活着但依赖挂了"时误判健康。健康检查必须探测业务语义(如 /healthz 返回 200 且包含依赖状态),而不是简单的端口存活。
3. 数据同步与冲突解决
3.1 同步复制 vs 异步复制
双活的核心矛盾在数据层。复制模式直接决定了 RPO 与可用性:
| 模式 | RPO | 写延迟 | 可用性影响 |
|---|---|---|---|
| 同步复制 | 0 | 高(跨区域 RTT) | 任一区域慢则整体慢 |
| 半同步 | ~0 | 中 | 从库超时降级为异步 |
| 异步复制 | 秒级 | 低 | 互不影响 |
跨区域同步复制的写延迟是致命的:北京到上海 RTT 约 30ms,一次写要等两地确认,写入延迟至少 30ms 起步,且任一区域网络抖动都会拖慢全部写入。所以跨区域双活几乎必然选择异步复制,接受秒级 RPO。
# 常见折中:同城同步 + 跨城异步
topology:
shanghai:
zone_a: { role: primary }
zone_b: { role: sync_replica } # 同城同步复制,RPO=0
beijing:
zone_c: { role: async_replica } # 跨城异步复制,RPO 秒级
这就是"两地三中心"的真正含义:同城保证 RPO=0,跨城保证机房级灾难可恢复。
3.2 写冲突的四种解决策略
异步双向复制必然产生写冲突(同一行在两地被同时修改)。解决策略按确定性排序:
策略一:单写(Single Writer)
同一数据在任何时刻只有一个区域可写,从根源上消除冲突。实现方式是按分片键把数据"归属"到某个区域:
uid 0~5000万 -> 归属 east 区域,只有 east 可写
uid 5000万+ -> 归属 north 区域,只有 north 可写
这是最可靠也最常用的策略——它把冲突问题转化成了路由问题。这也是单元化与多活的结合点:单元负责数据分片,区域负责部署位置,用户被路由到其归属区域,天然无冲突。
策略二:最后写入获胜(LWW)
用时间戳决定谁赢,简单但会静默丢数据:
-- LWW:写入时比较时间戳,较新者覆盖
UPDATE user_profile
SET nickname = '新昵称', updated_at = '2026-10-07T12:00:00Z', region = 'east'
WHERE uid = 10086
AND updated_at < '2026-10-07T12:00:00Z'; -- 只在新值更新时写入
LWW 的问题在于时钟偏差:若 east 的时钟比 north 快 2 秒,east 的旧写入会覆盖 north 的新写入。需要配合 NTP 同步 + 混合逻辑时钟(HLC)缓解。
策略三:CRDT(无冲突复制数据类型)
用数学上可交换、可结合的数据结构让冲突自动收敛:
| CRDT 类型 | 语义 | 例子 |
|---|---|---|
| G-Counter | 只增计数 | 浏览量(各区域分别计数,合并取和) |
| PN-Counter | 增减计数 | 点赞数(正负分开计数再合并) |
| OR-Set | 集合增删 | 标签集合 |
| LWW-Register | 单值覆盖 | 配置项 |
# G-Counter 合并:各区域独立累加,合并取每区域最大值再求和
def merge(a: dict, b: dict) -> dict:
return {r: max(a.get(r, 0), b.get(r, 0)) for r in set(a) | set(b)}
def value(counter: dict) -> int:
return sum(counter.values())
east = {"east": 100, "north": 40}
north = {"east": 95, "north": 60}
print(value(merge(east, north))) # 160,不重不漏
CRDT 适合计数、集合这类天然可合并的数据,但不适合"余额扣减"这类有业务约束的场景(CRDT 无法保证不超卖)。
策略四:业务补偿
对无法用技术手段解决的冲突(如库存超卖),允许冲突发生,事后用业务规则补偿:
两地各扣一次库存 -> 库存为负 -> 触发超卖补偿任务
└─ 通知用户 / 优先补货 / 赠送补偿券
这是兜底方案,代价是业务体验,应尽量避免。
3.3 一致性级别的选择
不同数据对一致性的要求天差地别,按数据分级比全局统一更现实:
| 数据 | 一致性要求 | 策略 |
|---|---|---|
| 用户资金 | 强一致 | 单写 + 同城同步 |
| 订单状态 | 会话一致 | 单写,异步跨城 |
| 用户资料 | 最终一致 | LWW |
| 点赞/浏览 | 最终一致 | CRDT |
| 缓存 | 弱一致 | 异步失效 |
详细的分布式一致性理论与算法(Paxos/Raft、CAP 权衡)可参考 数据一致性设计 。
4. 故障域与单元化
4.1 故障域的层级
容灾的本质是故障域的划分。层级越清晰,隔离越彻底:
Region(区域:北京 / 上海 / 新加坡)
└─ Zone(可用区:同城多机房,独立供电与网络)
└─ Cell(单元:可独立闭环的业务切片)
└─ Cluster(集群:K8s 集群)
└─ Node / Pod
故障域的收敛原则:故障必须被限制在某一层内,不能跨层扩散。例如一个 Zone 断电,只应影响该 Zone 内的单元;如果影响到了同城其他 Zone,说明故障域划分有泄漏(如共享的数据库主库)。
4.2 单元在多活中的定位
单元(Cell)是区域内的水平切片,多活是跨区域的部署形态。两者组合出多种拓扑:
| 拓扑 | 描述 | 复杂度 | 适用 |
|---|---|---|---|
| 单区域多单元 | 一个区域多个单元 | 中 | 区域内容量扩展 |
| 双区域单单元 | 两区域各一个完整副本 | 高(数据冲突) | 需要跨地域容灾 |
| 双区域多单元 | 每区域多个单元,单元归属区域 | 最高 | 大规模全球业务 |
“双区域多单元"是终极形态:用户先按 uid 路由到单元,单元部署在某个区域,同一单元内的读写完全闭环。跨区域只在单元迁移(用户换区域)时发生,冲突面被压缩到极小。
跨区域多集群的统一编排与联邦管理可参考 Kubernetes 多集群联邦 。
5. 切流演练与数据校验
5.1 演练矩阵
双活能力必须通过演练验证,且要覆盖所有故障组合:
| 演练项 | 模拟 | 验证目标 |
|---|---|---|
| 区域级故障 | 整个区域不可用 | 流量转移、容量承接 |
| 单 Zone 故障 | 同城一个机房断电 | 同城内切换 |
| 数据链路故障 | 跨区域复制中断 | 积压恢复、一致性 |
| 网络分区 | 区域间网络不通 | 脑裂处理、单写保护 |
| 回切 | 故障区域恢复后切回 | 数据追平、回切安全 |
网络分区演练最容易被跳过,也最危险:区域间网络断了但两个区域都还活着,如果两边都能写,就会产生无法自动合并的冲突。所以必须有分区时的单写保护——检测到与对端失联后,只保留一个区域可写。
# 脑裂保护:失联时的仲裁策略
split_brain_policy:
detection: "跨区域心跳连续丢失 10s"
action: quorum_write_only # 只有持有仲裁租约的区域可写
lease_ttl: 15s # 租约过期前必须续约,否则自动降为只读
on_recovery: "对账 + 人工确认后恢复双写"
5.2 数据校验
切流后的数据校验是双活可靠性的最后一道防线。校验分三层:
第一层:行数与汇总比对
-- 按天比对两区域的订单行数与金额
SELECT DATE(created_at) dt, COUNT(*) cnt, SUM(amount) amt
FROM orders
WHERE created_at >= CURDATE() - INTERVAL 7 DAY
GROUP BY dt;
第二层:抽样逐行比对
汇总一致不代表数据一致(可能一多一少相互抵消)。按 uid 取模抽样,逐行比对关键字段:
# 抽样比对:取 uid % 1000 == 7 的记录逐行校验
def verify_row(master_row, replica_row):
for field in ("status", "amount", "updated_at"):
if master_row[field] != replica_row[field]:
report_mismatch(master_row["uid"], field,
master_row[field], replica_row[field])
第三层:业务对账
技术层一致仍可能有业务逻辑错误(如重复扣款)。用业务口径对账:订单总额 vs 支付流水总额、库存变动 vs 出库单。
5.3 RPO/RTO 的验证
演练的最终产出是可验证的 RPO 与 RTO 数字,而不是"演练成功"四个字:
drill_report:
scenario: "east 区域整体故障"
rto_actual: "2m18s" # 目标 < 5min
rpo_actual: "3.4s" # 实测丢失 3.4 秒数据
capacity_after_failover: "78% of peak" # 单区域承接能力
degraded_features: ["推荐", "个性化排序"]
容量承接比例是最容易被忽略的数字:能切过去不等于能扛住。若承接后负载超过单区域容量上限,必须提前定义降级策略(关掉哪些非核心功能)。容量规划的完整方法可参考 SLA、SLO 与容量规划 。
6. 踩坑清单
| 坑 | 后果 | 规避 |
|---|---|---|
| 双半活(各按 50% 建) | 故障时被打垮 | 单区域按 100% 容量或明确降级 |
| 跨区域同步复制 | 写入延迟高,抖动放大 | 同城同步 + 跨城异步 |
| 健康检查只看端口 | 依赖挂了仍判健康 | 探测业务语义 |
| 无脑裂保护 | 分区时双写产生冲突 | 仲裁租约,单写保护 |
| 只比对汇总 | 一多一少相互抵消 | 汇总 + 抽样逐行 + 业务对账 |
| 演练不测回切 | 故障恢复后回不去 | 回切纳入演练矩阵 |
| DNS TTL 过长 | 故障切换慢 | TTL 30~60s + 客户端重试 |
| 忽略时钟偏差 | LWW 覆盖错误 | NTP + 混合逻辑时钟 |
7. 总结
多区域双活的四个核心问题及对应答案:
- 流量怎么分:GSLB 就近 + 加权,健康检查探测业务语义,可观测确认生效。
- 数据怎么同步:同城同步保 RPO=0,跨城异步保可用性,按数据分级选择一致性。
- 冲突怎么解:优先单写(把冲突转成路由),其次 LWW/CRDT,最后业务补偿。
- 切流怎么练:演练矩阵覆盖区域/机房/链路/分区/回切,产出可验证的 RPO/RTO 与容量数字。
双活不是"部署两套系统”,而是让故障域的隔离、数据的收敛、流量的调度三者同时成立。任何一环缺失,容灾就只是纸面能力。它也是 高可用架构与故障容错 在跨地域尺度上的具体展开——把"能容忍故障"从单机房延伸到多区域。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。