异地多活架构:跨地域高可用系统的设计与演进
多地多机房部署是互联网系统的必然发展方向。一个系统要走到这一步,必然要面对流量调配、数据拆分、网络延时、架构升级等一系列问题。本文从最简单的单机架构出发,沿着可用性不断提升的脉络,逐步推演出异地多活架构的完整面貌,并结合阿里单元化方案解析工业级落地实践。
为什么需要异地多活?
一个好的软件架构应当遵循三个核心原则:高性能、高可用、易扩展。其中,高可用通常用两个指标来衡量:
| 指标 | 含义 |
|---|---|
| MTBF(Mean Time Between Failure) | 两次故障的间隔时间,越长说明系统越稳定 |
| MTTR(Mean Time To Repair) | 故障恢复时间,越短说明对用户影响越小 |
可用性的计算公式为:
可用性(Availability)= MTBF / (MTBF + MTTR) × 100%
通常用"N 个 9"来描述系统的可用性等级:
| 可用性 | 年故障时间 | 日均故障时间 |
|---|---|---|
| 99%(2 个 9) | 3.65 天 | ~14.4 分钟 |
| 99.9%(3 个 9) | 8.76 小时 | ~86.4 秒 |
| 99.99%(4 个 9) | 52.6 分钟 | ~8.6 秒 |
| 99.999%(5 个 9) | 5.26 分钟 | ~0.86 秒 |
要达到 4 个 9 以上的可用性,平均每天的故障时间必须控制在 10 秒以内。每提升 1 个 9,都对系统设计提出更高的要求。
然而故障是不可避免的,主要来自三个方面:
- 硬件故障:交换机、路由器、磁盘等硬件损坏
- 软件问题:代码 Bug、配置错误、依赖服务异常
- 不可抗力:地震、水灾、火灾、停电、光缆被挖断
历史上不乏惨痛的教训:
| 时间 | 事件 | 影响 |
|---|---|---|
| 2013.07 | 微信因市政施工导致光缆被挖断 | 宕机数小时 |
| 2015.05 | 杭州光纤被挖断 | 近 3 亿用户约 5 小时无法访问支付宝 |
| 2021.07 | B站部分服务器机房故障 | 整站持续 3 小时无法访问 |
| 2021.10 | 富途证券机房电力闪断 | 用户 2 小时无法登录和交易 |
不同体量的系统关注的重点不同:体量小时关注用户增长,体量上来后关注性能体验,体量再大到一定规模后,可用性就变得尤为重要。对于全民级应用而言,再小概率的风险也不能忽视——这就是异地多活存在的根本原因。
部署架构的演进历程
第一阶段:单机架构
最简单的模型:客户端请求 → 业务应用 → 单机数据库 → 返回结果。
数据库单机部署,一旦遭遇意外,所有数据全部丢失。即使做了定期备份,也存在两个问题:
- 恢复需要时间:停机恢复,时间取决于数据量
- 数据不完整:备份存在时间差,不是最新数据
数据库越大,故障恢复时间越长,这种方案可能连 1 个 9 都达不到。
第二阶段:主从副本
在另一台机器上部署数据库从库(slave),与主库(master)保持实时同步。
| 优势 | 说明 |
|---|---|
| 数据完整性高 | 主从实时同步,数据差异极小 |
| 抗故障能力提升 | 主库异常时从库可切换为主库 |
| 读性能提升 | 业务可直接读从库,分担主库压力 |
提升系统可用性的关键就是冗余——担心一个实例故障就部署多个实例,担心一台机器宕机就部署多台机器。
第三阶段:同城灾备
机房级别的风险虽然概率小,但一旦发生影响巨大。应对方案就不能局限在一个机房内了——需要在同城再搭建一个机房,用专线网络连通。
冷备
B 机房只做数据备份,不提供实时服务,只在 A 机房故障时才启用。
- 优点:数据有异地备份
- 缺点:数据不完整、恢复期间业务不可用
热备
B 机房完整镜像 A 机房:接入层、业务应用、数据存储(从库)全部部署就位,处于待命状态。
A 机房故障时只需做两件事:
- B 机房所有从库提升为主库
- DNS 指向 B 机房,接入流量
热备相比冷备最大的优点是:随时可切换。
无论冷备还是热备,B 机房都处于备用状态,统称为同城灾备。它解决了机房级别的故障问题,可用性再次提升,但有一个隐患——B 机房从未经历过真实流量的考验,切换时不敢百分百保证能正常工作。
第四阶段:同城双活
让 B 机房也接入流量、实时提供服务,好处有二:
- 实时训练后备军:让 B 机房达到与 A 机房相同的"作战水平",随时可切换
- 分担流量压力:B 机房接入流量后,减轻 A 机房的负载
但 B 机房的存储是 A 机房的从库,默认不可写。解决方案是在业务应用层做读写分离改造:
| 操作 | 路由策略 |
|---|---|
| 读请求 | 可读任意机房的存储 |
| 写请求 | 只允许写 A 机房(主库所在) |
所有存储(MySQL、Redis 等)都需要区分读写请求,有一定的业务改造成本。A 机房为主机房,B 机房为从机房。
两个机房部署在同城,物理距离近,专线网络延迟可接受。B 机房可以从 10% → 30% → 50% → 100% 逐步接入流量,持续验证其工作能力。
同城双活比灾备更进一步:B 机房实时接入流量,且能应对随时的故障切换,系统弹性大大增强。
但两个机房在物理上仍处于同一城市。如果整个城市发生自然灾害(如 2021 年河南水灾),两个机房依旧存在全局覆没的风险。
第五阶段:两地三中心
为了应对城市级别的灾难,需要在异地(通常建议距离 1000 公里以上)再部署一个机房。
- A、B 机房在同一城市,同时提供服务(同城双活)
- C 机房部署在异地,只做数据灾备
这就是两地三中心架构,常用于银行、金融、政企项目。但问题依旧:启用灾备机房需要时间,且启用后的服务不确定能否如期工作。
异地双活:跨越延迟的鸿沟
为什么"简单异地部署"行不通?
如果把同城双活的架构直接搬到异地(例如 A 在北京、B 在上海),会遇到一个致命问题——网络延迟。
北京到上海约 1300 公里,即使光纤以光速传输,一个来回也需要近 10ms。加上路由器、交换机等设备,实际延迟可达 30ms 左右。更关键的是,远距离专线的质量远不如机房内网——延迟波动、丢包、甚至中断都是常态。
一个页面可能访问后端几十个 API,如果每次都跨机房访问,整个页面的响应延迟可能达到秒级——这是不可接受的。
虽然机房按同城双活的模型部署在了异地,但这本质上是一种伪异地双活。
真正的异地双活:机房内闭环
既然跨机房延迟是客观存在的物理限制,核心思路就是尽量避免跨机房调用——每个机房的请求在本机房内完成闭环。
这意味着每个机房都需要拥有独立的读写能力:
| 改造项 | 说明 |
|---|---|
| 数据库双主 | 两个机房的数据库都是主库,支持本地读写 |
| 双向数据同步 | 任一机房写入的数据,自动同步到另一个机房 |
| 全量数据 | 两个机房都拥有全量数据,支持任意切换 |
数据双向同步
MySQL 本身支持双主架构和双向复制。但 Redis、消息队列(Kafka、RocketMQ 等)这些有状态服务并不原生支持,需要开发专用的数据同步中间件。
数据同步中间件的核心作用:
北京机房写入 order=AAAAA → 中间件同步到上海
上海机房写入 order=BBBBB → 中间件同步到北京
最终:两个机房都有 order=AAAAA 和 order=BBBBB
使用中间件同步数据可以容忍专线的不稳定——专线出问题时中间件自动重试直到成功,达到数据最终一致性。
数据冲突问题
两个机房都可写,如果修改的是同一条数据,就会发生冲突:
用户短时间内发起两个修改请求:
→ 请求 A 落在北京机房,修改 order=AAAAA(尚未同步到上海)
→ 请求 B 落在上海机房,修改 order=BBBBB(尚未同步到北京)
→ 两个机房以谁为准?
系统发生故障并不可怕,可怕的是数据发生错误,因为修正数据的成本极高。
解决数据冲突:路由分片
核心思想是:同一个用户的所有请求,只在一个机房内完成业务闭环,从根源上避免冲突。
需要在接入层之上部署路由层,根据规则将用户分流到不同机房。常见的分片策略有两种:
策略一:哈希分片
根据用户 userId 计算哈希值取模,从路由表中找到对应机房。
用户 0~700 → 北京机房
用户 701~999 → 上海机房
对于未登录用户:
- 方案 A:全部路由到固定机房
- 方案 B:根据设备 ID 进行哈希取模
策略二:地理位置分片
非常适合与地理位置密切相关的业务(打车、外卖等)。
北京、河北、内蒙古 → 北京机房
上海、浙江、江苏 → 上海机房
以外卖为例,商家、用户、骑手都在相同的地理范围内,天然适合按地域分片。
全局数据的特殊处理
有一类数据无法做分片——全局强一致数据,典型如商品库存。这类数据只能采用"写主机房、读从机房"的方案,无法真正双活。
这意味着在交易链路中,虽然全链路都做了机房内闭环,到了库存扣减这一步又回到了中心机房,单元化闭环被打破了。
一种解决思路是库存分摊:将一个商品的库存拆分到不同机房,每个机房独立扣减本地库存,再通过库存调拨程序在机房间进行库存共享和再平衡。
| 场景 | 方案 |
|---|---|
| 普通交易 | 库存分摊 + 库存调拨程序保证机房间库存共享 |
| 秒杀场景 | 各机房独立扣减,无需调拨(库存本就要被快速消耗完) |
异地多活:从双活到 N 活
按照单元化的方式,每个机房可以部署在任意地区,随时扩展新机房,只需在最上层定义好分片规则。但随着机房数量增多,数据同步的复杂度急剧上升——每个机房写入数据后需要同步到所有其他机房,网状拓扑的复杂度为 O(N²)。
从网状到星状
业界的优化方案是将网状架构升级为星状:确立一个中心机房,所有数据同步都以中心机房为枢纽。
┌──────────┐
│ 单元机房A │──┐
└──────────┘ │
┌──────────┐ │ ┌──────────┐
│ 单元机房B │──┼──│ 中心机房 │
└──────────┘ │ └──────────┘
┌──────────┐ │
│ 单元机房C │──┘
└──────────┘
| 对比项 | 网状同步 | 星状同步 |
|---|---|---|
| 同步复杂度 | O(N²),每增一个机房所有机房都需改造 | O(N),只需同步到中心机房 |
| 扩展性 | 差 | 好,新机房只需和中心建立同步关系 |
| 中心依赖 | 无 | 中心机房稳定性要求高 |
| 容灾 | 任一机房可接管 | 中心故障时可提升任一机房为新中心 |
星状架构的优势:
- 一个机房写入数据只需同步到中心机房,中心再同步至其他机房
- 不需要关心一共部署了多少机房,扩展新机房的成本极低
- 中心机房故障时,可将任一单元机房提升为新中心,继续服务
至此,系统真正实现了异地多活——多个机房同时对外提供服务,任意机房故障可快速切换,系统具备极强的扩展能力。
阿里单元化实践
阿里在实施单元化时,根据业务特点采用了两种模式:
交易单元化 vs 导购单元化
| 对比维度 | 交易单元化 | 导购单元化 |
|---|---|---|
| 入口流量 | 入口清晰(商品详情→购物车→下单→支付) | 入口分散,大促时增加各种场景和玩法 |
| 链路特征 | 以写为主 | 大部分是读 |
| 数据库模式 | WRITE 模式(本地读写,双向同步) | COPY 模式(中心写入,单元只读) |
| 单元化范围 | 全链路必须做单元化(对用户下单有直接影响) | 仅 C 端服务做单元化,商家后台中心化部署 |
| 资源成本 | 较高(每个单元完整部署) | 较低(商家后台等只部署在中心) |
导购单元化采用 COPY 模式的原因:商家后台服务的可用性要求相对较低,故障恢复后继续操作即可,对大盘交易影响不大。中心化部署能大幅节省资源成本和维护成本,也能降低开发人员的开发成本。
单元化路由透传机制
单元化的核心在于路由信息的全链路透传——从接入层到最底层的数据层,每一层都需要能够正确识别和传递路由参数。
| 层次 | 路由机制 |
|---|---|
| 接入层 | 解析 HTTP 请求中的路由参数(cookie/header/body),路由到正确的应用 SLB |
| 应用层 | 中间件从 HTTP 请求中提取路由参数保存到上下文,供后续 RPC 和消息使用 |
| RPC 层 | RPC 客户端从上下文取出路由参数,随 RPC 请求传递到远程 Provider |
| 消息层 | MQ 客户端发送消息时从上下文获取路由参数添加到消息属性,消费时还原到上下文 |
| 数据层 | 保证数据落库到正确单元的 DB,防止数据脏写 |
单元协同与单元保护
在单元化演进过程中,有两个关键问题需要解决:
单元协同:某些特定业务场景需要保证数据强一致性(如库存扣减),这类服务只能在中心单元提供服务。所有对中心服务的调用都会直接路由到中心单元完成。
单元保护:系统自上而下各层都要具备纠错保护能力,保证业务按单元化规则正确流转:
| 保护层 | 纠错机制 |
|---|---|
| 接入层纠偏 | 流量进入接入层后,通过路由参数判断归属单元,非本单元流量代理到正确的目标单元 |
| RPC 纠偏 | RPC Consumer 端根据请求的单元信息进行路由选址,错误流量会被重定向到正确单元 |
| 数据层保护 | 数据库层面的最后防线,防止数据写入错误的单元 |
异地多活落地的关键挑战
落地异地多活远不止架构设计,还需要在多个维度做好准备:
数据一致性保障
| 挑战 | 应对策略 |
|---|---|
| 同步延迟导致的数据不一致 | 接受最终一致性,业务层做好容错设计 |
| 数据冲突(双写同一条数据) | 通过路由分片从源头避免,辅以冲突检测和仲裁机制 |
| 同步中断(专线故障) | 中间件自动重试 + 断点续传,恢复后自动追数据 |
| 数据校验 | 定期对账程序比对两地数据,发现差异自动修复 |
机房切换策略
| 切换类型 | 触发条件 | 操作 |
|---|---|---|
| 计划内切换 | 机房维护、演练 | 逐步调整路由权重,平滑迁移流量 |
| 故障切换 | 机房故障 | DNS 切换 + 路由规则调整,将故障机房流量转移到其他机房 |
| 回切 | 故障恢复 | 先同步恢复期间的增量数据,再逐步回切流量 |
业务分级与取舍
并非所有业务都需要做异地多活,需要根据业务重要程度进行分级:
| 级别 | 业务类型 | 多活策略 |
|---|---|---|
| P0 | 核心交易链路(下单、支付) | 必须做单元化,机房内完全闭环 |
| P1 | 重要辅助(购物车、搜索) | 做单元化部署,允许短时降级 |
| P2 | 一般功能(商家后台、运营工具) | 中心化部署,故障时暂时不可用 |
| P3 | 非核心(日志、统计) | 不做多活,故障后补数据 |
配套基础设施
异地多活的落地还依赖一系列配套设施:
- 全局流量调度:DNS + HTTP DNS + 接入层路由,支持按规则精细分流
- 数据同步中间件:覆盖 MySQL、Redis、MQ 等所有有状态服务
- 统一配置中心:支持多机房配置的统一管理和快速下发
- 全链路监控:跨机房的调用链追踪、数据同步延迟监控、一致性校验报告
- 演练平台:定期进行故障演练,验证切换流程的有效性
架构演进全景对比
| 阶段 | 方案 | 机房数 | 可用性 | 核心特点 | 主要局限 |
|---|---|---|---|---|---|
| 1 | 单机架构 | 1 | < 99% | 最简单 | 单点故障,数据丢失 |
| 2 | 主从副本 | 1 | ~99.9% | 数据冗余 | 机房级故障无法应对 |
| 3 | 同城灾备 | 2(同城) | ~99.95% | 机房级冗余 | 备用机房未经验证 |
| 4 | 同城双活 | 2(同城) | ~99.99% | 双机房实时服务 | 无法应对城市级灾难 |
| 5 | 两地三中心 | 3(两城) | ~99.99% | 异地数据备份 | 灾备机房启用慢 |
| 6 | 异地双活 | 2(异地) | ~99.99%+ | 机房内闭环,双主同步 | 需要大量中间件和业务改造 |
| 7 | 异地多活 | N(多地) | ~99.999% | 星状同步,任意扩展 | 实施复杂度高,需要强大的基础设施支撑 |
总结
异地多活的演进,本质上是一部用冗余换可用性的发展史。从中可以提炼出以下核心认知:
- 冗余是高可用的基石:从主从副本到多机房部署,每一次演进都是在更大的维度上做冗余
- 延迟是异地部署的核心矛盾:跨城网络延迟是客观物理限制,必须通过"机房内闭环"来规避
- 数据一致性是最大的技术挑战:双向同步、冲突避免、最终一致性保障,每一环都需要精心设计
- 路由分片是解决冲突的根本手段:通过哈希分片或地理分片,确保同一用户的请求在同一机房内闭环
- 星状拓扑是多活扩展的最优解:相比网状同步的 O(N²) 复杂度,星状拓扑将复杂度降为 O(N)
- 不是所有业务都需要多活:根据业务重要程度分级,P0 核心链路做完整单元化,非核心业务中心化部署节省成本
- 架构设计是技术与成本的平衡:异地多活需要路由层、数据同步中间件、监控体系、演练平台等大量基础设施支撑,没有足够的人力物力很难落地
好的架构不是一步到位的,而是随着业务体量的增长逐步演进的。理解每一步演进背后的驱动力和技术挑战,比直接套用某个方案更加重要。