多区域双活架构设计

多区域双活的完整设计:GSLB 与就近路由的流量调度、同城同步与跨城异步复制的取舍、写冲突的单写与 LWW/CRDT 解决策略、故障域分层与单元定位,以及切流演练矩阵、脑裂保护与跨区域数据校验方法,并附常见踩坑清单,帮助团队把容灾从纸面能力变成可验证的工程能力。

“两地三中心"做到最后,往往变成"两个机房 + 一个只读备库”——灾难真来时切不过去。多区域双活的难点从来不在"部署两套",而在流量怎么分、数据怎么同步、冲突怎么解、切流怎么练。本文聚焦区域层(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. 总结

多区域双活的四个核心问题及对应答案:

  1. 流量怎么分:GSLB 就近 + 加权,健康检查探测业务语义,可观测确认生效。
  2. 数据怎么同步:同城同步保 RPO=0,跨城异步保可用性,按数据分级选择一致性。
  3. 冲突怎么解:优先单写(把冲突转成路由),其次 LWW/CRDT,最后业务补偿。
  4. 切流怎么练:演练矩阵覆盖区域/机房/链路/分区/回切,产出可验证的 RPO/RTO 与容量数字。

双活不是"部署两套系统”,而是让故障域的隔离、数据的收敛、流量的调度三者同时成立。任何一环缺失,容灾就只是纸面能力。它也是 高可用架构与故障容错 在跨地域尺度上的具体展开——把"能容忍故障"从单机房延伸到多区域。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「架构」更多文章

  1. 韧性工程与错误预算:从 SLO 到故障演练
  2. 微前端架构:组合、隔离与独立部署
  3. 数据网格(Data Mesh):领域数据产品与去中心化治理