Skip to content

异地多活架构:单元化、数据同步与「三地五中心」怎么落地

本文是分布式系统学习系列的 G2 容灾篇。前置:32. 分布式设计模式手册33. 工程案例复盘。 学完可以配合面试题食用:G1 故障检测与雪崩隔离

异地多活不是什么高深概念,是容灾的自然演进

先理清一条线:从单机房到异地多活,解决的问题是逐步升级的。

  1. 单机房单机 —— 挂了就挂了,没有容灾
  2. 同城双活 —— 同一个城市两个机房,距离几十公里,光纤专线直连,延迟 1-2ms。数据库同步通常用主从复制或 MGR 跨机房部署。问题:一个城市地震/停电/光缆全断,两个机房一起完蛋
  3. 两地三中心 —— 同城双活 + 异地一个冷备。冷备只接受数据同步,不接流量。成本高(三套机房),且异地冷备的 RTO 很长(小时级,要拉起冷备机、追数据、切流量)
  4. 异地双活 —— 两个城市都接流量,不是"冷备",是"热备"。难点:数据怎么跨城同步且不冲突
  5. 异地多活 —— 三个及以上城市同时服务,典型如"三地五中心"。解决了单个城市全挂时其他城市还能扛量

每个阶段不是凭空出现的,是上一阶段解决不了某个问题才升级的。比如"两地三中心"的冷备太慢,才催生了异地双活。

异地多活的核心矛盾:数据一致性与可用性

异地多活最难的不是"怎么让流量进来",而是数据怎么跨城市同步而不冲突

写冲突是最大敌人

假设你在上海机房下了一个订单,用户在广州操作同一笔订单。如果两个机房都允许写,那么订单状态可能在上海机房里是"已支付",在广州机房里是"已取消"。谁是对的?

解决方案只有两条路:

  • 不让冲突发生 —— 单元化架构,每个用户的数据只在一个单元(机房)里写,别的地方只读
  • 允许冲突并仲裁 —— 写的时候不阻塞,但同步后通过一定规则(如时间戳、版本号、单元优先级)决定谁对

大部分生产系统走第一条,因为第二条实现太复杂,而且业务上很难接受两个订单都"成功"之后又撤销一个。

跨机房数据同步的三种方式

DB 原生复制

MySQL 主从复制、MGR 跨机房部署。延迟低(同城 < 2ms,异地 < 50ms),但写冲突无解——主从架构下两地都写会冲突,除非你做成"上海写、广州读"的伪双活。而且异地跨机房延迟一旦超过 100ms,主从复制就容易出问题(半同步超时、复制延迟膨胀)。

CDC / Binlog 订阅

用 Canal / Otter / DRC 等工具,把 MySQL binlog 解析后跨机房发送到目标端执行。好处是业务代码不用改,坏处是延迟不可控——binlog 消费有积压、跨城网络有抖动。典型场景:异步数据同步,不做强一致。

业务层双写

业务代码里显式写两个库。可控性最强,但改造成本最大,且原子性保证困难(双写一个成功一个失败怎么办?通常用事务消息或本地消息表兜底)。

下面是一个简化版的异地多活数据同步架构图:

mermaid
flowchart LR
    subgraph SH["上海机房"]
        A1[App 单元 A] --> DB1[(MySQL A)]
        DB1 -->|Binlog| Canal1["Canal 订阅"]
        Canal1 --> MQ1["MQ 跨城"]
    end
    
    subgraph GZ["广州机房"]
        MQ2["MQ 接收"] --> Canal2["Canal 回放"]
        Canal2 --> DB2[(MySQL B)]
        A2[App 单元 B] --> DB2
    end
    
    MQ1 -.->|跨城专线| MQ2
    DB2 -.->|Binlog| Canal2 -.->|反向同步| MQ1 -.->|给上海| DB1

注意:双向同步必须做去环——防止 A 机房产生的 binlog 同步到 B 机房后又回写给 A 机房。Otter 的做法是在每行数据上加一个 sync_flag 标记。

单元化:多活的前提

"单元"(Cell / Unit)是异地多活的核心概念。一个单元是一组自包含的服务集群,包括应用层、缓存层、数据库层。用户请求根据路由规则(比如用户 ID 哈希后取模)被分配到某个固定单元,该单元处理该用户的所有读写请求。

为什么要单元化

如果没有单元化,所有流量同时打到所有机房,每个机房都要有一份完整数据,写冲突就不可避免。单元化让每个用户的数据只在一个单元里写入,其他单元只读或同步副本。

单元化路由

java
// 用户路由:按用户 id 取模定向到单元
public class UserRouter {
    // 单元映射表:单元编号 -> 机房
    private static final Map<Integer, String> CELL_MAP = Map.of(
        0, "shanghai-a",
        1, "shanghai-b", 
        2, "guangzhou-a",
        3, "beijing-a"
    );
    
    // 路由规则:用户 id 后四位取模
    public static String route(long userId) {
        int cellIndex = (int)(userId % CELL_MAP.size());
        return CELL_MAP.get(cellIndex);
    }
    
    // 注意:路由规则一旦上线就不能轻易改
    // 改了就变成"同一个人去了不同单元",数据分片错乱
    // 扩容时用虚拟节点 + 一致性哈希 或 双写后灰度切流
}

路由规则必须稳定——上线后不能随意改,否则用户数据会跑到不同单元,从"写冲突"变成"找不到数据"。扩容时一般采用虚拟节点 + 一致性哈希,或者双写迁移的方式。

单元内闭环

一个单元里的服务之间调用走本地,不跨单元。比如订单服务调用库存服务,如果都在同一个单元内,直接 gRPC 调用;跨单元就得走 MQ 或 RPC 但降级为异步。

单元化最难受的地方是跨单元的查询——比如用户 A 在上海单元,用户 B 在广州单元,管理员想查他俩的订单合并报表。这需要一个全局的聚合层,定期从各单元拉数据,或者通过数据同步汇总到中心库。

活的程度:流量怎么切、切多少、数据怎么追平

"活"不是二态——不是"活"或"死",而是有多少流量能正常处理、数据能追到多近

切流策略

  • 全量切:一个机房挂了,DNS/GSLB 把 100% 流量切到另一个机房。简单粗暴,但另一个机房必须能扛两倍流量,且跨城延迟会放大
  • 灰度切:按用户 id 尾号,逐步切流量。比如先切 5% 到异地机房,观察 15 分钟,如果正常再切 10%、20%……直到 100%。这种做法的好处是发现异常可以回滚,但持续时间长,需要有人值守

切流时的数据瓶颈

流量切到异地机房了,但数据可能还没同步完。比如上海机房挂了,广州机房接管流量,但广州还有 30 秒的 binlog 延迟没追完。这时候用户查订单,看不到最后一笔支付记录——这就是 RPO(Recovery Point Objective)不为零。

RPO 和 RTO 是异地多活的两个核心指标:

  • RTO(Recovery Time Objective):从故障发生到恢复服务要多久。异地多活期望做到秒级到分钟级
  • RPO(Recovery Point Objective):最多丢失多少数据。异地多活通常做不到零丢失,因为异步复制有延迟,目标一般是秒级

防脑裂:两个机房同时认为对方挂了

网络分区时,两个机房都以为对方死了,都开始接管流量,数据就分裂了——这就是脑裂

常见的防脑裂手段:

  • 仲裁节点:引入第三个机房或云上的仲裁节点,判断哪个机房存活
  • 租约机制:租约过期后自动释放写权限,避免同时写
  • 单元归属丢弃:两个机房存了冲突数据,按单元归属丢弃非本单元的数据(比如 user_id 属于上海单元,广州单元写入的数据就丢弃)

真实案例:某金融公司的三地五中心

(脱敏简化)某金融公司要求 RTO < 60s,RPO < 5s,部署模式如下:

  • 三个城市:上海、杭州、北京,每个城市两个可用区(AZ),共 5 个可用区
  • 交易类数据:单元化,按用户 id 路由到上海/杭州,北京只做冷备
  • 基础数据(产品信息、费率表)用 CDC 双向同步,走 DRC 平台,带去环标记
  • 每季度做一次"故障演练":随机停掉一个可用区的网络,观察系统是否能自动切流

演练结果:第一次演练时,切流后 20% 的查询超时。原因是跨城查询走了同步 RPC,异地延迟 50ms 叠加业务层超时 3s 导致上游线程池打满。修正是把所有跨单元查询改为异步或降级为本地缓存查询(允许最终一致)。

总结

异地多活不是"买几台机器放不同城市"就完事,它是从数据一致性流量调度故障切换三个维度一起决策的架构选择。

  • 同城双活做读写分离,异地双活必须做单元化
  • 数据同步选 CDC 还是业务层双写,取决于对延迟的容忍度
  • 流量切多少、切多快,取决于异地机房的容量和数据延迟
  • 防脑裂靠仲裁或租约,不是靠"多写几个 if"

面试时问到异地多活,别只讲概念。能说出"单元化解决了写冲突"、"CDC 双向同步需要去环"、"RPO 和 RTO 不是零"这类工程细节,才说明你真的做过。

参考

  • Google 的全球数据中心架构(Spanner + F1)
  • 阿里十年架构演进中的"三地五中心"实践
  • 美团分布式 ID 与单元化改造
  • DynamoDB Global Tables 设计
  • Otter / Canal 的双向同步去环机制

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1