引言
单个数据中心(或单个云厂商可用区)的故障是低概率事件,但当故障发生时,代价是全站不可用。机房断电、海底光缆中断、区域级网络故障,甚至云厂商某一 Region 的级联故障,都可能让"三个九"变成"零"。多区域(Multi-Region)架构的意义,不是追求理论上的极致可用,而是把"数据中心即单点"这个最大的单点故障从系统中消除。
但多区域架构是一条艰难的爬坡路:数据跨区域复制要面对延迟与一致性、流量调度要避免"用户被路由到最远的机房"、故障切换要处理脑裂与数据分歧、演练要冒着真实切流的风险。本文从架构模式、数据复制、流量调度、切换演练四个维度,给出可落地的多区域高可用实践。
多区域是单区域高可用的延续——如果还没有做好可用区(AZ)级别的冗余,直接上多区域会手忙脚乱。AZ 级高可用与 RTO/RPO 基础可先阅读 https://plumephp.com/high-availability-disaster-recovery/;分布式一致性的理论基础见 https://plumephp.com/distributed-systems-consistency-availability/。
目录
- 1. 多区域架构模式
- 2. 多活部署关键设计
- 3. 跨区域数据复制
- 4. 数据冲突处理策略
- 5. 流量调度:DNS / Anycast / GSLB
- 6. 会话与状态处理
- 7. 故障切换与回切
- 8. 故障切换演练与 Game Day
- 9. 一致性与可用性权衡
- 10. 总结与决策树
- 延伸阅读
1. 多区域架构模式
1.1 三种经典模式
| 模式 | 写流量 | 读流量 | 数据复制 | 典型 RTO | 复杂度 |
|---|---|---|---|---|---|
| 冷备(Cold Standby) | 仅主区域 | 仅主区域 | 定期备份/镜像 | 小时级 | 低 |
| 温备(Warm Standby) | 仅主区域 | 主区域+备只读 | 异步/同步复制 | 分钟级 | 中 |
| 多活(Active-Active) | 多区域 | 多区域 | 双向复制 | 秒级 | 高 |
1.2 Active-Passive 模式
主区域(Primary)承担全部写流量,备用区域(Standby)保持热备状态,平时只承担只读流量(或零流量):
┌──────────────┐ 复制 ┌──────────────┐
│ Primary │ ──────────────▶ │ Standby │
│ us-east-1 │ (同步/异步) │ eu-west-1 │
│ 读写 │ │ 只读/备用 │
└──────────────┘ └──────────────┘
适用:数据库强一致要求高、写冲突难解决、团队初期。
1.3 Active-Active 模式
多个区域同时提供读写,通过数据复制与冲突解决保证最终一致:
┌──────────────┐ ◀═══ 复制 ═══▶ ┌──────────────┐
│ Region A │ │ Region B │
│ 读写 │ │ 读写 │
└──────────────┘ └──────────────┘
│ ▲ │
▼ │ ▼
GSLB 流量调度(就近接入 + 故障转移)
适用:全球化业务、低延迟诉求强烈、业务分区天然可解耦(如按租户/地域分片)。
2. 多活部署关键设计
2.1 无状态优先
多活的第一个原则是应用层无状态——会话、缓存、临时文件不能落在本地磁盘。应用层必须能随时在任意区域水平扩展:
# Kubernetes 多区域部署示意
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 20
selector:
matchLabels: { app: api-server }
template:
metadata:
labels: { app: api-server }
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/region
operator: In
values: ["us-east-1"]
2.2 区域自治
- 每个区域能独立发布、独立伸缩、独立故障,不依赖跨区域调用完成核心请求
- 跨区域调用是反模式:
Region A的接口调用Region B的服务,等于把两个区域的可用性绑定 - 跨区域仅保留低频管理面调用(如配额同步、元数据更新)
2.3 区域故障隔离
| 依赖 | 多区域部署策略 |
|---|---|
| 应用服务 | 每区域独立 Deployment |
| 缓存 | 每区域独立 Redis Cluster,禁止跨区域访问 |
| 消息队列 | 每区域独立 Kafka,跨区域用 MirrorMaker |
| 数据库 | 每区域独立实例,双向复制 |
| 对象存储 | 存储服务自身跨区域复制(如 S3 CRR) |
3. 跨区域数据复制
3.1 复制拓扑
┌─── Region A ───┐ ┌─── Region B ───┐
│ MySQL Primary │◀══════════▶│ MySQL Primary │
│ │ │ 双向复制 │ │ │
│ ▼ │ │ ▼ │
│ CDC / Binlog │ │ CDC / Binlog │
└────────────────┘ └────────────────┘
3.2 复制方式对比
| 方式 | 延迟 | 数据丢失风险 | 冲突 | 适用 |
|---|---|---|---|---|
| 同步复制 | 高(跨区域几百 ms) | 无 | 少 | 金融、强一致 |
| 异步复制 | 低 | 故障时有窗口丢失 | 多 | 大部分业务 |
| 事务级同步(XA) | 极高 | 无 | 无 | 几乎不用(跨区域) |
| 基于日志的 CDC | 中 | 极小 | 需解决 | MySQL/PostgreSQL 双向 |
3.3 跨区域复制实现:双主 + 复制过滤
-- 避免循环复制:为每个区域设置不同的 server_id,并跳过自身写入
-- Region A 的复制配置
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='region-b-db.example.com',
SOURCE_USER='repl',
SOURCE_AUTO_POSITION=1;
-- 通过 server_id 过滤避免 A↔B 无限循环
SET GLOBAL server_id = 1001; -- A 区域
SET GLOBAL server_id = 2002; -- B 区域
现代实践更推荐 Debezium + 事件总线的方式,把数据变更当作事件流异步复制,天然解耦且可审计。详见 https://plumephp.com/data-sync-cdc-debezium-practice/。
3.4 复制一致性验证
复制不是"配好就完事",必须持续验证:
-- 每个区域定期比对关键表
SELECT checksum(table) FROM users; -- 分区域计算
SELECT MAX(updated_at) FROM orders; -- 检查最大变更时间接近当前
推荐引入数据比对工具(如 pt-table-checksum、数据对账任务),周期核对两地数据差异并告警。
4. 数据冲突处理策略
4.1 冲突来源
多活下,两个区域可能同时修改同一条记录。冲突处理有四种常见策略:
| 策略 | 规则 | 优点 | 缺点 |
|---|---|---|---|
| Last-Write-Wins(LWW) | 时间戳/版本最新者胜 | 简单、无阻塞 | 可能丢更新 |
| Merge(合并) | 字段级合并 | 信息丢失少 | 合并逻辑复杂 |
| CRDT | 可交换/幂等合并 | 天然收敛 | 模型受限 |
| 业务分区 | 不同业务分属不同区域 | 无冲突 | 跨区访问受限 |
4.2 LWW 实现
-- 用版本号避免时钟回拨:region + version 组合
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
region VARCHAR(16) NOT NULL,
version BIGINT NOT NULL,
status VARCHAR(32),
payload JSON,
updated_at TIMESTAMP
);
-- 更新时携带版本号,冲突以 version 较大者为准
-- (简化示意:真正的 LWW 需要复制通道去重)
4.3 业务分区(推荐优先)
最佳实践是尽量不产生跨区写冲突:按租户 ID 哈希或地域将数据"钉"在特定区域,每个区域只写自己负责的分片,复制仅用于容灾与只读。这样既获得多活能力,又规避了大部分冲突。
users: region = hash(tenant_id) % 2 → A 或 B 区域
orders: 同租户跟随 user 所在区域
global_meta: 仅在主区域写,复制到备区域
5. 流量调度:DNS / Anycast / GSLB
5.1 三层流量调度
| 层 | 机制 | 粒度 | 故障切换时间 |
|---|---|---|---|
| DNS | TTL + 多记录 | 域名/IP 集合 | 秒~分钟(取决于 TTL 与客户端缓存) |
| Anycast | BGP 路由 | IP 层就近 | 秒级(路由收敛) |
| GSLB(全局负载均衡) | 健康检查 + 加权 | 区域级 | 秒级 |
5.2 DNS 多区域 + 健康检查
api.example.com A 203.0.113.10 (Region A, 权重 80)
api.example.com A 198.51.100.20 (Region B, 权重 20)
配合 GSLB 做健康探测,区域故障时自动摘除该区域 IP:
health probe: https://region-a.health.example.com/healthz → 200?
https://region-b.health.example.com/healthz → 200?
5.3 Anycast 就近接入
Anycast 让多个区域的节点共享同一个 IP,路由器通过 BGP 把用户导向最近的节点:
┌──────────┐ BGP ┌──────────┐
│ ISP 路由 │ ───────────▶ │ Region A │ (Anycast IP: 203.0.113.10)
│ │ ───────────▶ │ Region B │ (Anycast IP: 203.0.113.10)
└──────────┘ └──────────┘
用户路由到"最近"的节点;节点故障 → BGP 撤销路由 → 自动切换
特点:切换快、就近性好;但 Anycast 通常只承担 L4/L7 代理入口(DNS/证书/会话需要在同一节点处理,长连接迁移是个问题)。
5.4 GSLB 配置示例(F5 / Cloudflare 全局负载均衡)
# 示意:Cloudflare Global Load Balancing
load_balancer:
name: api-glb
pools:
- name: region-a-pool
origins:
- { name: region-a, address: api-region-a.example.com, weight: 80 }
health_check: /healthz
- name: region-b-pool
origins:
- { name: region-b, address: api-region-b.example.com, weight: 20 }
health_check: /healthz
steering_policy: geo # 就近 + 故障转移
fallback_pool: region-b-pool
GSLB 的底层负载均衡算法可参考 https://plumephp.com/load-balancing-algorithms-and-strategies/。
6. 会话与状态处理
6.1 会话粘滞 vs 全局状态
| 方案 | 说明 | 多区域问题 |
|---|---|---|
| 会话粘滞(Sticky Session) | 同用户固定到同一区域 | 区域故障时会话失效 |
| 全局会话存储 | 会话放 Redis/DynamoDB 全球复本 | 跨区域读延迟 |
| 无状态 + JWT | 状态放客户端 Token | 最佳,无会话依赖 |
多活推荐彻底无状态:用 JWT/OAuth 承载身份(https://plumephp.com/oauth2-jwt-security-practice-guide/),业务状态放数据库,避免跨区域会话依赖。
6.2 缓存一致性
多区域缓存策略:
写入路径:Region A 写 DB → 通过 CDC 事件广播 → Region B 缓存失效
(跨区域缓存同步禁止直接用"读时回填",否则脏数据横穿区域)
若缓存允许最终一致,可在每区域独立缓存上缩短 TTL,用"本地写 + 近实时失效"替代强一致同步。
7. 故障切换与回切
7.1 切换决策
区域故障确认(健康检查 + 人工确认)
│
▼
┌─ 流量切换 ── GSLB 摘除故障区域,流量全部路由到存活区域
│
├─ 数据晋升 ── 若主库在故障区域,提升存活区域副本为主
│
└─ 降级确认 ── 依赖故障区域数据的请求是否可降级(只读/缓存兜底)
7.2 切换类型
| 类型 | RTO | 数据损失 | 触发 |
|---|---|---|---|
| 计划切换(Planned) | 分钟级 | 无 | 维护窗口 |
| 非计划切换(Unplanned) | 秒~分钟 | 视复制模式 | 区域故障 |
| 自动切换(Automatic) | 秒级 | 视复制模式 | 自动健康判断 |
7.3 数据晋升(Promote)
# 以 AWS Aurora / RDS 为例,把备区域只读副本提升为主
aws rds promote-read-replica \
--db-instance-identifier region-b-primary \
--region eu-west-1
7.4 回切(Failback)
回切比切换更危险——必须确认原区域数据已追平且稳定:
1. 原区域恢复 → 以"延迟追平"模式重新建立复制
2. 持续观察复制延迟与数据校验,确认无差异
3. 小流量灰度回切(先 10% 读流量)
4. 观察稳定后再全量回切
8. 故障切换演练与 Game Day
8.1 演练目标
| 目标 | 验证点 |
|---|---|
| 切换时间达标 | RTO 是否满足(如 5 分钟内完成 GSLB 切流) |
| 数据损失可控 | RPO 是否满足(切换后数据差异在容忍范围) |
| 流程可执行 | 运行手册(Runbook)是否有效、负责人是否知道步骤 |
| 依赖无遗漏 | 配置中心、DNS、证书、第三方回调是否同步切换 |
8.2 演练类型
桌面推演(Tabletop) → 流程与决策,无真实流量
沙箱演练(Sandbox) → 在测试环境完整执行切换
区域级 Game Day → 真实区域注入故障,真实切流
突击演练(Unannounced) → 不预告,检验团队真实响应
8.3 演练检查清单
- [ ] GSLB 健康检查与摘除逻辑是否生效
- [ ] DNS TTL 是否足够低(建议 TTL ≤ 60s)
- [ ] 数据库复制状态(延迟、冲突数)在阈值内
- [ ] 消息队列消费组是否有积压,是否可跨区切换
- [ ] 只读降级开关是否就绪
- [ ] 告警与值班响应链路是否正常
- [ ] 回切流程是否同样经过演练
混沌注入的工具与实践可参考 。
9. 一致性与可用性权衡
9.1 跨区域的一致性代价
跨区域复制的物理事实是:光速有限,两地延迟少说几十毫秒。同步复制保证一致但放大写延迟;异步复制保证可用但带来不一致窗口。
| 需求 | 复制策略 | 一致性 |
|---|---|---|
| 强一致(账户余额) | 同步 / 单区域写 + 全局读 | 线性一致 |
| 最终一致(feed、评论数) | 异步双向 | 最终一致 |
| 可调一致(购物车) | 异步 + LWW | 最终一致 |
9.2 一致性分级模型
强一致 ──────────────────────────▶ 最终一致
账户余额 订单状态 评论点赞 推荐feed 日志分析
↑ ↑
单区域写+跨区域读 多活双向复制
(牺牲跨区写延迟) (接受不一致窗口)
9.3 实践建议
- 写流量尽量落在单一区域(或按业务分区),避免同一业务跨区双写
- 读取就近:读流量可自由就近,最终一致场景无感知
- 关键路径强一致:支付、库存等强一致业务不放到跨区异步复制路径上
10. 总结与决策树
10.1 决策树
你的业务真的需要多区域吗?
│
├─ 否(单区域足够,或可用性要求 < 99.99%)
│ → 先做 AZ 级多可用区高可用
│
├─ 需要跨区域容灾,但可接受分钟级恢复
│ → 温备模式(Active-Passive + 异步复制)
│
├─ 需要秒级恢复 + 全球化低延迟
│ → 多活模式(Active-Active + 业务分区 + GSLB)
│
└─ 多活但强一致业务多
→ 混合:强一致业务单区域写,弱一致业务多活
10.2 关键结论
| 原则 | 内容 |
|---|---|
| 应用无状态 | 会话、缓存不进本地磁盘 |
| 区域自治 | 不跨区域做同步调用 |
| 数据尽量分区 | 减少冲突比解决冲突更简单 |
| 流量就近 + 可切 | DNS/Anycast/GSLB 三层保障 |
| 切换要演练 | 没有演练过的容灾方案等于没有 |
| RTO/RPO 量化 | 所有决策以数字为验收标准 |
多区域高可用不是"上云就自动获得"的能力,而是架构、数据、流量、流程四者的系统工程。从 https://plumephp.com/high-availability-disaster-recovery/ 的 AZ 级冗余开始,逐步演进到区域级容灾,并用演练持续验证——这才是可靠的跨域容灾路径。
延伸阅读
- AWS 多区域架构白皮书
- Google SRE 手册:故障演练与事后分析
- CRDT 论文:Conflict-free Replicated Data Types
- Debezium 官方文档
- RFC 2181:DNS TTL 语义
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。