Featured image of post 异地多活:Java Web 微服务的高可用终极形态

异地多活:Java Web 微服务的高可用终极形态

异地多活:Java Web 微服务的高可用终极形态

Java Web 微服务系列 · 第 1 篇 · 异地多活 阅读时长:约 28 分钟 本文写于 2026 年 6 月

引子:机房炸了的那一夜

2018 年某日凌晨,杭州某互联网公司的主数据中心核心交换机突发故障,所有线上服务瞬间不可用。从用户端看:APP 加载失败、支付接口超时、客服电话被打爆、商家无法接单。技术团队用了 45 分钟完成故障切换,业务勉强恢复,但已经损失了数百万 GMV 和大量用户信任。

事后复盘:这次故障只是"机房级"——交换机坏了,换一台就好。

但如果是"城市级“灾难呢?地震、台风、洪水、光纤被施工队挖断、变电站爆炸……机房级灾备在这种场景下完全失效。你需要把整套系统分散到不同的城市,让任意一个城市"消失"时,其他城市依然能继续服务。

这就是异地多活

但异地多活不是"建几个机房 + 同步数据"那么简单。跨城市的光纤延迟 30-100ms,是机内调用的 60 倍,直接照搬同城双活的架构会让系统慢到用户无法接受。这就是为什么业界演化出"单元化"这套精巧的设计。

一、核心概念:把术语先掰开

在正式进入架构演化史之前,先把几个核心术语定义清楚——很多文章混用"灾备/双活/多活"的概念,导致读者越看越糊涂。

1.1 什么是异地多活

异地多活(Geo-Distributed Active-Active)指在地理上分散的多个机房同时对外提供服务(读写流量),任意机房故障时其他机房可快速接管全部业务。

三个关键词:

  • 地理分散:通常跨城市(北京-上海),距离 1000 公里以上
  • 同时对外服务:所有机房都在线,不是主备关系
  • 快速接管:故障时无需人工切换,其他机房自动承接流量

💡 原理:异地 vs 同城的本质区别

同城机房之间距离 < 100 公里,光纤往返延迟 < 1ms,可以"当一个机房用”。 异地机房之间距离 1000+ 公里,光纤往返延迟 30-100ms,不能再当一个机房用——同步要异步化、调用要避免跨机房、状态要"单元化"。

1.2 高可用的衡量标准

可用性有明确的量化公式:

1
可用性 = MTBF / (MTBF + MTTR) × 100%
  • MTBF(Mean Time Between Failures):平均故障间隔时间
  • MTTR(Mean Time To Repair):平均恢复时间

业内用"N 个 9"描述:

可用性年累计停机时间日均停机
99.9%(3 个 9)8.76 小时1.44 分钟
99.99%(4 个 9)52.6 分钟8.6 秒
99.999%(5 个 9)5.26 分钟0.86 秒

异地多活是实现 4 个 9 以上可用性的必经之路。仅靠单机或主从架构,3 个 9 已经是极限。

📌 实践:4 个 9 是什么概念

假设你每天用某 APP 下单 1000 万次:

  • 3 个 9:每天约有 14.4 分钟无法下单 → 约 1 万次失败
  • 4 个 9:每天约有 8.6 秒无法下单 → 约 100 次失败
  • 5 个 9:每天约有 0.86 秒无法下单 → 约 10 次失败

4 个 9 对应"一年内最多停机 52 分钟",这已经是绝大多数大型互联网公司的目标

1.3 两个关键指标:RTO 与 RPO

异地多活的效果要落到两个具体指标上:

  • RTO(Recovery Time Objective):故障到恢复的时间——系统能停多久
  • RPO(Recovery Point Objective):数据丢失的窗口——系统能丢多少数据

异地多活的理想目标:RTO ≈ 0(无感知切换)、RPO ≈ 0(不丢数据)。

方案RTORPO
单机不可恢复全部丢失
主从副本分钟级秒级
同城双活秒级接近 0
异地双活秒级秒级(异步同步)
异地多活秒级接近 0

📌 实践:RTO/RPO 是和老板谈预算的最佳语言

别只说"系统要做异地多活",要说"业务每年可接受的停机时间是 X 分钟,每次故障可接受的数据丢失是 Y 条"。这两个数字决定了架构选型。

老板们听不懂"高可用"和"灾备",但听得懂"我一年最多能接受亏多少钱"。

二、架构演化史:从单机到异地多活

从单机到异地多活,业界走过了一段漫长的路。核心思想始终是"冗余"——用多份资源换取可用性。

2.1 七阶段演进时间线

2.2 各阶段对比

阶段抵御风险恢复速度复杂度跨机房延迟典型应用
单机-最低-内部工具、Demo
主从副本服务器级-小型 SaaS
同城灾备(冷备)机房级慢(小时级)1-5ms传统企业 IT
同城灾备(热备)机房级较快(分钟级)1-5ms中型电商
同城双活机房级极快(秒级)1-5ms大型电商
两地三中心城市级较慢(人工决策)较高30-100ms银行、金融
异地双活城市级极快(秒级)30-100ms大型互联网
异地多活城市级 + 规模压力极快(秒级)最高30-100ms阿里、美团、字节

2.3 关键阶段详解

单机:一台机器打天下

所有服务和数据都在一台机器上。任何硬件故障 = 业务中断。这在 2000 年前的很多小公司很常见,今天只剩 demo 项目还在用。

主从副本:最基础的冗余

数据库主库 + 从库,主库写、从库读。服务器硬件故障时,从库可提升为主库。

  • 优点:实现简单,MySQL 原生支持
  • 缺点:故障切换需人工/脚本介入;切换延迟通常分钟级
  • 典型场景:内部 OA、测试环境

同城灾备:机房级容灾

同城(< 100 公里)再建一个机房,实时同步数据。

  • 冷备:仅做数据备份,不提供服务。故障时需要冷启动所有服务,小时级恢复
  • 热备:实时同步数据 + 提前部署好所有服务,故障时可手动切流量,分钟级恢复

💡 原理:冷备 vs 热备的成本差

冷备只备份数据,成本是"一份存储 + 一点点带宽"。 热备要部署完整服务栈,成本是"完整机房 + 完整运维"。 很多公司为了省钱做冷备,结果故障时恢复 4 小时,损失 200 万——省小钱亏大钱

同城双活:机房都承担流量

两个机房都接入流量,但写流量只走主机房,读可走任意机房。逻辑上仍视为一个机房。

  • 优点:资源利用率高,机房级故障可自动切换
  • 缺点:写流量仍是单点(主机房故障时切换仍需决策)

两地三中心:跨城市的折中

2 个城市 + 3 个机房:同城 2 机房双活 + 异地 1 机房灾备。常见于银行、金融、政企。

  • 优点:兼顾机房级和城市级容灾
  • 缺点:异地机房仍是冷/热备状态,资源浪费严重

🎯 避坑点:两地三中心 ≠ 异地双活

很多文章把"两地三中心"和"异地双活"混为一谈,其实完全不同:

  • 两地三中心:异地机房是灾备(平时不接流量),故障时切
  • 异地双活:异地机房平时也接流量,故障时对方接管

监管要求"两地三中心"是最低标准,异地双活是更高标准

异地双活:迈向真正的多活

两个城市各建一个机房,都承担读写流量。任意城市故障时,另一个城市接管全部业务。

  • 优点:真正解决城市级故障
  • 难点:跨机房数据同步、调用延迟、状态一致性

异地多活:终态

3 个及以上城市部署机房,每个机房都是"主"。新增城市时无需重构同步链路。

  • 优点:容量大、地域覆盖广、单机房故障影响小
  • 难点:复杂度极高,运维成本巨大

💡 原理:演进的内在动力

业务量 ↑ → 故障域要求 ↑ → 单机房容量上限突破 → 必须分散到多机房 业务连续性要求 ↑ → 城市级故障不可接受 → 必须跨城市 资金可承受 → 多活成为合理选择

演进的本质是"成本 ↔ 可用性"的权衡,不是技术炫技。

三、为什么需要异地多活

3.1 城市级故障是真实存在的

来自云厂商的公开故障记录(不完全统计):

  • 2019 年 3 月:阿里云华北 2 地域可用区 C 因光缆中断服务异常
  • 2022 年 12 月:腾讯云广州区域部分服务控制台异常
  • 2023 年 8 月:多个云厂商海外 Region 因海底光缆故障网络抖动

自然灾害(地震、洪水、台风)、运营商故障(光缆挖断)、人为误操作(配置错误、误删数据)——城市级故障每年都在发生

📌 实践:业务连续性分级

业务系统应按中断影响分级,决定采用哪种灾备方案:

等级业务类型RTORPO推荐方案
P0核心交易< 5 分钟< 1 分钟异地多活
P1重要业务< 30 分钟< 5 分钟同城双活 + 异地灾备
P2一般业务< 数小时< 1 小时同城灾备(热备)
P3后台系统< 1 天< 1 天主从副本即可

3.2 业务中断的代价

故障成本远不止"少卖几单"那么简单:

  • 直接损失:交易系统每分钟损失数百万 GMV(双 11 期间可达千万级)
  • 品牌损失:用户信任度下降,监管关注,媒体曝光
  • 用户流失:一次大故障可能流失 5-10% 活跃用户
  • 员工成本:全员熬夜处理,工时损失巨大
  • 法律风险:金融、医疗行业有合规要求,故障可能引发监管处罚

🛑 误区警示

我们小公司不会遇到城市级故障"——这是最常见的误判。 即便你不用云厂商,你的云厂商会。2023 年某中型电商因云厂商 Region 故障停业 6 小时,直接损失过千万。

3.3 合规与出海

  • 强监管行业:金融、证券、医疗——监管明确要求"两地三中心”
  • 出海业务:海外 Region 故障率高于国内,且跨国专线更脆弱
  • 上市/融资需求:投资人尽调会问"你们的灾备能力如何"

异地多活已经从"加分项"变成"必选项"。

四、具体怎么操作

这是本文最核心的部分。异地多活的实施分五大块:存储双向同步 → 单元化 → 兜底机制 → 全局数据例外 → 故障演练与监控

4.1 存储层:双向同步

异地多活的前提是两个机房都能写。这要求数据层做双向同步。

主流存储的双向同步方案

存储双向同步方案工具 / 中间件同步延迟冲突解决
MySQL双主模式 + binlog 同步MySQL 原生双主、阿里 Canal、Otter秒级字段合并 / 业务回避
Redis主从双向 + 冲突解决RedisShake、自研中间件毫秒级LWW / 业务回避
MongoDBReplica Set + OplogMongoShake、自研秒级版本号 / 业务回避
消息队列双向 Topic + 幂等消费Kafka MirrorMaker、RocketMQ毫秒级幂等去重
ElasticsearchCCR 双向ES CCR秒级版本号

MySQL 双主同步的两个坑

坑 1:自增 ID 冲突 双主都从 1 开始自增会重复写入。解决方案:

  • 奇偶分片:A 库 auto_increment_increment=2, offset=1(1, 3, 5…);B 库 offset=2(2, 4, 6…)
  • 全局序列:用 Snowflake(机房号段区分)或 Redis incr 生成

坑 2:循环同步 A 同步给 B,B 同步给 A,会形成死循环。

🎯 避坑点:循环同步

MySQL 双主同步必须配置 replicate-wild-ignore-table 过滤掉"自己产生的 binlog",否则会导致:

  1. A 写一条记录 → 同步给 B
  2. B 接收到后写入自己的 binlog → 又同步给 A
  3. 无限循环,主从延迟飙升

解决方案:使用 GTID 模式(MySQL 5.7+),每个事务有全局唯一 ID,已同步过的不再同步。

阿里 Canal 实战

Canal 是阿里开源的 MySQL binlog 订阅组件,常用于异地多活的增量同步:

1
MySQL Master → Canal Server(伪装成从库)→ Kafka → 消费端写入目标库

Canal 的优势:

  • 伪装成 MySQL Slave,对主库零侵入
  • 支持多种下游(Kafka、RocketMQ、RabbitMQ)
  • 配合 Otter 可以做到全自动化同步 + 反向校验

Canal 部署架构

完整的 Canal 异地同步架构通常包含三层:

  1. Canal Server 集群:每个机房部署 3-5 个 Canal Server 实例,负责订阅本机房 MySQL 的 binlog
  2. Kafka 集群:作为 Canal Server 和消费端之间的消息缓冲,应对消费端故障或回溯
  3. 消费端集群:订阅 Kafka 消息,按顺序应用到目标机房

关键参数:

  • canal.instance.parser.parallel:是否并行解析 binlog(默认 true)
  • canal.instance.tsdb.enable:是否启用时间戳库(用于断点续传,必开
  • kafka.batch.size:批量发送大小(建议 1000-5000)

📌 实践:增量同步 + 全量校验

异地多活的数据同步不能只做增量。还需要定期全量校验:

  • 增量同步 Canal 负责(秒级延迟)
  • 全量校验用 pt-table-checksum(小时级发现不一致)
  • 不一致数据用 pt-table-sync 修复

增量只能保证"目前一致",全量校验才能保证"历史一致"。

建议校验频率:核心表每小时一次,非核心表每天一次。漏掉校验的那一天,可能就是数据漂移的开始

4.2 数据冲突:必须解决的根本问题

当同一数据被两个机房同时修改时,无法判定先后

举例:用户在机房 A 修改了自己的昵称"张三",同一秒在机房 B 登录并改成了"李四"。两个机房互相同步时,谁覆盖谁?

两种解决思路:

方案 1:中间件自动合并 依赖时钟排序,最后写入获胜(LWW, Last-Write-Wins)。问题是分布式时钟不准——NTP 同步误差可达 100ms,跨城市误差更大。

方案 2:从源头避免冲突——单元化(业界主流)

💡 原理:避免冲突 > 解决冲突

分布式系统有一句名言:"不要试图用软件解决所有问题,要用业务设计避免问题"。 单元化就是这种思路——从架构上保证同一数据只在一个机房被修改,冲突从根本上不存在。

4.3 单元化:异地多活的核心

在接入层之上加路由层,按规则把用户分流到固定机房,同一用户的所有业务在同一机房内闭环,根除跨机房调用。

三种分片规则

分片方式适用场景优点缺点
按业务类型业务边界清晰实施简单不适合通用业务
直接哈希分片通用业务(电商、社交)均衡分布机房扩容需数据迁移
按地理位置O2O(外卖、打车)用户延迟低用户跨地区会"漂移"

直接哈希分片示例

假设机房 A、B:

  • 用户 ID % 2 == 0 → 机房 A
  • 用户 ID % 2 == 1 → 机房 B

同一用户的所有读写都路由到固定机房,业务闭环。两个机房独立运行,不互相调用。

单元化的 3 大原则

  1. 入口唯一:每个用户从入口就被打上"属于哪个机房"的标签,后续所有调用都基于这个标签
  2. 数据本地化:用户数据只存在自己所属的机房,跨机房读通过异步同步
  3. 调用收敛:禁止跨机房 RPC 调用,必须通过数据复制或中转层

单元化的代价

单元化不是免费的:

  • 业务改造:所有 RPC 调用要标注"是否跨机房",跨机房调用要被显式禁止或转发
  • 数据迁移:机房扩容时(如 2 机房变 3 机房)要重新计算哈希,迁移用户
  • 全链路追踪:要能监控每个请求走的是哪个机房,否则出问题无法排查
  • 全局服务特殊处理:支付、库存、配置等"全局服务"无法单元化,需要单独设计

4.4 兜底机制

路由规则理论上保证用户不漂移,但实际会有边界情况:

  • 跨机房调用(如支付、库存等全局服务)
  • 机房切换时的请求路由
  • 用户的"漂移"(如出差、出国)

解决方案:在写存储时检测数据归属

  • 写入前判断"这条数据属于哪个机房"
  • 如果不属于本机房,报错或转发到正确机房
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 伪代码:写存储前的归属检测
public void saveOrder(Long userId, Order order) {
    String targetDC = router.getDCByUserId(userId);  // 计算用户归属机房
    String currentDC = DataCenterContext.current();  // 当前机房
    if (!targetDC.equals(currentDC)) {
        // 通过 RPC 转发到正确机房
        rpcClient.to(targetDC).saveOrder(userId, order);
        return;
    }
    // 本机房写库
    orderRepository.save(order);
}

📌 实践:兜底是单元化的"安全网"

单元化路由 + 兜底检测 = 双保险。

  • 路由层:99.99% 的请求路由到正确机房
  • 兜底层:剩下 0.01% 的异常请求被拦截或转发

缺一不可。没有兜底的单元化,出一次事故就要全量排查

4.5 全局数据例外

不是所有数据都要双活

强一致数据(系统配置、商品库存、汇率、订单号生成器)仍采用"写主机房、读从机房“方案:

  • 这类数据写入频率低,强一致要求高
  • 跨机房写入延迟反而更糟

策略:

数据类型同步方案一致性保障典型工具
配置中心单点写 + 多点读ZK/Consul/Nacos 推 + 定时拉Nacos
全局 ID机房号段隔离Snowflake 41 位时间戳 + 5 位机房号美团 Leaf
库存系统单点写 + 异步同步异步消息 + 失败重试RocketMQ
订单流水双活单元化同用户订单落在同机房-
支付清算单点写 + 多点读T+1 同步 + 实时对账-

🛑 误区警示

盲目追求"全部双活"是常见错误

  • 全局数据双活得不偿失:成本 3-5 倍,但收益仅"机房级容灾”
  • 同城双活 + 异地灾备已能满足 P1 业务
  • 只对 P0 业务做异地多活即可

4.6 故障演练:必须做的事

异地多活的最大风险是"平时一切正常,故障时不会切"。

Chaos Engineering(混沌工程):主动制造故障,验证切换流程。

演练清单

  • 单机房断网(拔网线模拟)
  • 单机房电源中断
  • 数据库主从切换
  • 跨机房专线中断
  • DNS 污染
  • CDN 故障
  • 整机房故障(最严酷场景)

演练频率

  • 新机房上线前:必须通过完整演练
  • 日常:每月一次小规模演练
  • 大版本上线前:必须做一次全链路演练
  • 年度:至少一次"整机房故障"演练

🎯 避坑点:演练 ≠ 文档

很多团队写了"故障切换手册"就以为高枕无忧。手册没演练过 = 不存在。 真正打过几次"实战",你才知道手册哪些步骤是错的、哪些是过时的、哪些根本没有可执行性。

某互联网公司在 2020 年做第一次真实整机房演练时发现,切换脚本里的目标机房 ID 写错了 3 年没人发现——因为从没真正切过。

Netflix Chaos Monkey 的启发

Netflix 是混沌工程的鼻祖,开源了 Chaos Monkey 系列工具:

  • Chaos Monkey:随机杀掉生产环境的 EC2 实例
  • Latency Monkey:注入网络延迟
  • Conformity Monkey:找不符合最佳实践的实例并关掉

Netflix 的哲学:"在生产环境演练,而不是在测试环境"。只有真实流量下的故障切换才有意义

4.7 监控告警:异地多活的"心跳"

异地多活的监控比传统架构复杂得多——要同时监控"业务指标“和”基础设施指标"。

三层监控

层级监控指标告警阈值工具
基础设施CPU、内存、磁盘、网络、专线延迟标准运维阈值Prometheus + Grafana
中间件DB 主从延迟、MQ 积压、缓存命中率按业务定制Prometheus + 自研 Exporter
业务侧机房分流比例、跨机房调用比例、错误率偏离基线 > 5%SkyWalking + 自研监控

拨测:业务侧的"心跳"

从公网模拟用户请求,每分钟拨测各机房关键接口

  • 拨测失败率 > 1% → 告警
  • 拨测延迟 > 基线 2 倍 → 告警

📌 实践:拨测是异地多活的"生命线"

基础设施监控正常 ≠ 业务可用。 唯一可靠的判断是"从公网调用业务接口是否成功"。

拨测要在公网多个地域部署,不能只部署在机房内—— 否则你连"机房和公网之间的网络"出故障都不知道。

推荐拨测点:北京、上海、广州、深圳 + 海外 1-2 个城市(AWS 东京/新加坡)。

业务侧的关键指标

除了技术指标,异地多活还有几个业务侧专属指标必须监控:

  • 机房分流比例:A 机房 50%、B 机房 50% 是健康态。漂移到 70/30 说明有 bug
  • 跨机房调用比例:理论上接近 0。突然飙升说明单元化规则被破坏
  • 异地同步延迟:MySQL 同步延迟、消息同步延迟。超过 5 秒要告警
  • 用户漂移率:同一用户多次访问落到不同机房的比例。> 1% 就要排查

五、真实案例

5.1 阿里:三地五中心

阿里在 2015 年完成"三地五中心“架构:

  • 3 个城市:杭州、北京、深圳
  • 5 个机房:每城市 1-2 个
  • 每个单元都独立支撑全量业务

关键技术:

  • 单元化 + 异地多活
  • OceanBase 分布式数据库(保障数据一致性)
  • 异地专线 + 自研流量调度系统

阿里的核心思想:”异地多活不是分布式系统的子集,而是全新的架构范式"——业务设计要按"单元"组织,数据存储要按"单元"分布,流量调度要按"单元"分发。

5.2 美团:两地三中心

美团采用"两地三中心“架构(北京 + 上海):

  • 北京 2 机房 + 上海 1 机房
  • 单元化按用户 ID 分片
  • 金融级业务做异地双活,O2O 业务做同城双活

美团的特点:

  • 业务分级:金融级(支付、贷款)= 异地双活;O2O(外卖、到店)= 同城双活
  • 自研中间件:自研 OCTO 服务框架 + 自研同步组件
  • 业务容错:单元化失败时,降级到同城双活 而不是直接挂掉

5.3 字节跳动:单元化 + 异地

字节的多活架构特点:

  • 国内多个 Region,海外多个 Region
  • 单元化粒度更细(按服务级别)
  • 自研 Service Mesh 治理跨机房调用

字节的核心创新:

  • Service Mesh 跨机房治理:跨机房调用通过 Mesh 自动处理(限流、重试、降级)
  • 全链路单元化:从入口网关到数据库,全部带"机房标签”
  • 多语言统一:Go/Java/Python 服务都能融入单元化框架

💡 原理:单元化是异地多活的"第一性原理"

不论叫"三地五中心"还是"两地三中心",核心都是:

  1. 业务单元化划分(让用户绑死在某个机房)
  2. 存储层双向同步(让机房之间数据可写)
  3. 最上层分片逻辑(决定请求去哪)

任何异地多活方案,缺一不可。

六、总结

6.1 异地多活的 3 大核心要素

  1. 业务单元化划分——让同一用户的所有请求在同一机房完成
  2. 存储层双向同步——让两个机房都能写
  3. 最上层分片逻辑——决定请求去哪

6.2 实施成本

异地多活不是"装个软件就完事",需要配套建设:

  • 业务层:微服务部署、依赖拆分、SDK、Web 框架
  • 基础设施:服务发现、流量调度、CI/CD、同步中间件
  • 数据保障:数据分类、一致性保障、机房切换一致性
  • 运维:监控体系、异常处理、故障演练

6.3 何时该上异地多活

业务特征推荐方案
用户量 < 100 万同城双活
强监管(金融)两地三中心
O2O 业务同城双活 + 异地灾备
业务中断可接受数小时同城灾备(热备)

异地多活的 ROI 拐点在"日活 1000 万 + 业务连续性要求 4 个 9"。低于这个规模做了也是浪费。

6.4 异地多活的常见陷阱

🛑 误区警示:这些坑别踩

  1. 盲目上异地双活:业务量没到,成本先到
  2. 忽视单元化改造:直接照搬同城双活,性能差 60 倍
  3. 不做故障演练:手册写了等于零,必须真打实战
  4. 过度追求强一致:所有数据都双活,成本爆炸
  5. 忽视运维成本:复杂度是同城的 5 倍,团队要相应扩

异地多活是"用钱换命"的工程,不是技术炫技

📌 工程实践:异地多活的成本对比

成本对比(同业务量级):

  • 单机:1x
  • 同城双活:2-3x
  • 异地双活:5-8x
  • 异地多活:10-15x

决策前先问:

  1. 业务真的需要 4 个 9 吗?
  2. 城市级故障的发生概率是多少?
  3. 一次大故障的最大损失是多少?
  4. 异地多活的成本,多久能通过"避免的损失"回本?

6.5 系列预告

这是 Java Web 微服务系列 的开篇。后续计划覆盖:

  • 服务治理:服务发现、配置中心、熔断降级
  • 网关与限流:Spring Cloud Gateway、Sentinel
  • 分布式事务:Seata、Saga、TCC 模式对比
  • 链路追踪:SkyWalking、Jaeger
  • Spring Cloud Alibaba 实战:Nacos + Sentinel + Seata 三件套

七、异地多活实施清单

如果你决定启动异地多活项目,按以下清单逐项检查,可以少踩很多坑。

7.1 业务层 Checklist

  • 业务分级:明确 P0/P1/P2/P3 业务,确定哪些要异地多活
  • 用户标识:确定分片键(用户 ID / 租户 ID / 业务 ID)
  • 单元化拆分:按分片键拆分服务调用链,画出"业务-数据-机房"映射图
  • 全局服务识别:识别无法单元化的服务(支付、库存、配置中心),单独设计
  • 跨机房调用禁止:通过 Service Mesh 或代码规范禁止跨机房 RPC
  • 数据归属检测:所有写操作前检测数据归属机房

7.2 数据层 Checklist

  • 数据库选型:MySQL 用双主 + GTID;Redis 用 RedisShake;MQ 用 MirrorMaker
  • 同步中间件:部署 Canal + Kafka 集群,保证 binlog 不丢失
  • 全量校验:pt-table-checksum 定期跑,pt-table-sync 修复
  • ID 方案:改用 Snowflake / Leaf 分布式 ID,不用自增
  • 数据分类:明确"双活数据"和"全局数据"的边界
  • 冲突解决策略:每个表写明冲突解决方式(LWW / 业务回避 / 人工合并)

7.3 流量层 Checklist

  • 入口打标:在负载均衡 / 网关层就打上"用户属于哪个机房"的标签
  • 路由层高可用:路由层本身要异地多活(最关键,不能单点)
  • 健康检查:实时探测机房健康度,自动剔除故障机房
  • DNS 切流:准备好 DNS 切流脚本,故障时一键切换
  • 灰度切流:先切 1% 流量观察,再切 10%、50%、100%

7.4 运维层 Checklist

  • 监控大盘:业务侧 + 中间件 + 基础设施三层监控
  • 拨测系统:公网多地域拨测,覆盖核心接口
  • 告警分级:P0 故障 5 分钟内电话告警到负责人
  • 故障演练:每月小演练、季度大演练、年度整机房演练
  • Runbook:每个故障场景有对应 Runbook,且 至少演练过一次
  • 复盘机制:每次故障 24 小时内复盘,输出 Action Item 跟踪

7.5 团队层 Checklist

  • 架构师:至少 2 名资深架构师能讲清楚单元化路由
  • DBA:有异地多活 MySQL 双主运维经验
  • SRE:能做混沌工程演练
  • 值班:7×24 小时值班,异地多活的故障不分昼夜
  • 跨团队协作:业务 / 后端 / 数据 / SRE / 安全 团队对齐

🎯 避坑点:清单的 80/20 法则

上面 30+ 项检查,80% 的项目栽在前 10 项

  1. 业务分级不清晰
  2. 全局服务识别不全
  3. 跨机房调用没禁住
  4. ID 方案没换
  5. 入口没打标
  6. 路由层单点
  7. 没有全量校验
  8. 没做故障演练
  9. Runbook 是摆设
  10. 团队没准备好

先把前 10 项做好,再考虑剩下的

八、实战案例:同城双活生产落地方案(A 主 B 备,A 挂 B 自动接管)

真实场景:自建同城双机房,域名 + GTM 用阿里云,应用跑在自建 k8s,MySQL/Redis 自建。 目标:A、B 机房同时承担流量(A 主 B 备),A 挂了 B 自动接管,A 恢复后自动回稳。

8.1 需求清单

需求说明
同城双活自建机房,< 1ms 延迟
A 主 B 备A 是 MySQL 主库,B 是备库
平时 AB 同时分担流量写 100% 走 A,读 50% A + 50% B
A 挂了 B 接管全部流量20-30 秒全自动
A 恢复后自动回稳Orchestrator 自动切回 A
B 挂了不影响业务A 独立承担全部

8.2 架构总览

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
                            用户
                    ┌──────────────────┐
                    │   阿里云 GTM     │
                    │ api.example.com  │
                    │ A:100 / B:0      │  ← 平时
                    │ A:0   / B:100    │  ← A 挂时
                    └────────┬─────────┘
                             │ DNS 解析
              ┌──────────────┴──────────────┐
              ▼                             ▼
   ┌──────────────────┐         ┌──────────────────┐
   │     机房 A       │         │     机房 B       │
   │   10.0.0.0/24    │         │   10.0.1.0/24    │
   │                  │         │                  │
   │  ┌────────────┐  │         │  ┌────────────┐  │
   │  │ k8s 集群   │  │         │  │ k8s 集群   │  │
   │  │ 3-100 副本 │  │         │  │ 3-100 副本 │  │
   │  └─────┬──────┘  │         │  └─────┬──────┘  │
   │        │         │         │        │         │
   │  ┌─────▼──────┐  │         │  ┌─────▼──────┐  │
   │  │ ProxySQL-A │  │         │  │ ProxySQL-B │  │
   │  │ 10.0.0.50  │  │         │  │ 10.0.1.50  │  │
   │  └─────┬──────┘  │         │  └─────┬──────┘  │
   │        │         │         │        │         │
   │  ┌─────▼──────┐  │  半同步  │  ┌─────▼──────┐  │
   │  │ MySQL-A    │──│──binlog─►│  │ MySQL-B    │  │
   │  │ (主→备)   │  │  < 1ms   │  │ (备→主)   │  │
   │  │ 10.0.0.1  │  │         │  │ 10.0.1.1  │  │
   │  │ VIP:      │  │         │  │ VIP:      │  │
   │  │10.0.0.100 │  │         │  │10.0.1.100 │  │
   │  └────────────┘  │         │  └────────────┘  │
   │                  │         │                  │
   │  ┌────────────┐  │         │  ┌────────────┐  │
   │  │ Redis-A    │──│─Sentinel│─►│  Redis-B    │  │
   │  └────────────┘  │  多数派  │  └────────────┘  │
   └──────────────────┘         └──────────────────┘
              │                             │
              └─────────────┬───────────────┘
                    ┌───────▼────────┐
                    │ Orchestrator   │ 部署在 B 机房
                    │ 10.0.1.100:3000│
                    └────────────────┘

8.3 关键组件选型

组件选型理由
DNS 流量调度阿里云 GTM机房级流量切换
MySQL HAOrchestratorGitHub 在维护、Web UI、A 恢复后自动切回
流量代理ProxySQL读写分离 + 应用层透明
VIP 漂移keepalived同子网标准玩法
Redis HASentinel官方方案,3 节点跨机房
k8skubespray 自建双集群Ansible 部署生产级
监控Prometheus + Grafana + AlertManagerCNCF 标准栈

💡 原理:为什么 Orchestrator 部署在 B 机房

关键洞察:B 平时是备库,挂了不影响主业务

  • A 挂了 → Orchestrator 在 B,能自动切 B 为主
  • A 恢复 → Orchestrator 在 B,能自动把 A 加回集群、自动切回 A
  • B 挂了 → Orchestrator 也没了,但 A 还是主,业务不受影响(只是失去自动切换能力)

这是用业务容错换运维简化——A 挂了能切,A 恢复能切回,B 挂了业务无感。不需要 Raft 3 节点那么复杂

8.4 MySQL HA 配置

主库 /etc/my.cnf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[mysqld]
server-id = 1
log-bin = mysql-bin
binlog-format = ROW
gtid-mode = ON
enforce-gtid-consistency = ON
log-slave-updates = ON

# ⚠️ 半同步(核心)
plugin-load = "rpl_semi_sync_master=semisync_master.so"
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_master_timeout = 1000
rpl_semi_sync_master_wait_for_slave_count = 1

innodb_flush_log_at_trx_commit = 1
sync_binlog = 1
character-set-server = utf8mb4
report-host = 10.0.0.1

备库:server-id=2report-host=10.0.1.1,加 plugin-load = "rpl_semi_sync_slave=semisync_slave.so"rpl_semi_sync_slave_enabled=1

创建用户(主库执行)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- 半同步复制用户
CREATE USER 'repl'@'10.0.%' IDENTIFIED BY 'ReplPass@123Str0ng';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.0.%';

-- Orchestrator 监控用户
CREATE USER 'orchestrator'@'10.0.%' IDENTIFIED BY 'OrchPass@123';
GRANT SUPER, PROCESS, REPLICATION SLAVE, RELOAD ON *.* TO 'orchestrator'@'10.0.%';
GRANT SELECT ON mysql.* TO 'orchestrator'@'10.0.%';

-- ProxySQL 监控用户
CREATE USER 'proxysql'@'10.0.%' IDENTIFIED BY 'ProxySQL@123';
GRANT REPLICATION CLIENT ON *.* TO 'proxysql'@'10.0.%';

备库指向主库

1
2
3
4
5
6
7
8
CHANGE MASTER TO
    MASTER_HOST='10.0.0.1',
    MASTER_USER='repl',
    MASTER_PASSWORD='ReplPass@123Str0ng',
    MASTER_AUTO_POSITION=1;
START SLAVE;
SHOW SLAVE STATUS\G
-- 验证 Slave_IO_Running: Yes, Slave_SQL_Running: Yes, Seconds_Behind_Master: 0

8.5 keepalived VIP(同子网内)

A 机器 /etc/keepalived/keepalived.conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
vrrp_script check_mysql {
    script "/usr/bin/mysqladmin ping -h 127.0.0.1 -uorchestrator -pOrchPass@123"
    interval 2
    weight -30
    fall 3
}

vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    advert_int 1
    unicast_src_ip 10.0.0.1
    unicast_peer { 10.0.1.1 }
    authentication { auth_type PASS, auth_pass YourVrrpPass }
    virtual_ipaddress { 10.0.0.100/24 }
    track_script { check_mysql }
}

B 机器:state BACKUPpriority 90unicast_src_ip 10.0.1.1,VIP 改为 10.0.1.100/24

🎯 避坑点:VIP 不能跨机房飘

VIP 只能在同子网内飘,跨子网 ARP 广播不生效。两个机房各一个 VIP 独立飘,不互相干扰

8.6 Orchestrator 部署(B 机房)

/etc/orchestrator.conf.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "MySQLTopologyUser": "orchestrator",
  "MySQLTopologyPassword": "OrchPass@123",
  "MySQLReplicaUser": "repl",
  "MySQLReplicaPassword": "ReplPass@123Str0ng",

  "RecoverMasterPrimary": true,
  "RecoverIntermediatePrimary": true,
  "FailMasterPromotionOnLagMinutes": 1,
  "RecoveryPeriodBlockSeconds": 30,
  "ApplyMySQLPromotionAfterMasterFailover": true,

  "InstancePollSeconds": 2,
  "BackendDB": "sqlite://var/lib/orchestrator/orchestrator.db",
  "HttpPort": 3000,
  "ListenAddress": "0.0.0.0:3000",

  "PostFailoverProcesses": ["/usr/local/bin/post-failover.sh"],

  "RecoveryHostnameAlias": {
    "10.0.0.1": "mysql-a",
    "10.0.1.1": "mysql-b"
  }
}

post-failover.sh 自动钩子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
set -e
LOG=/var/log/orchestrator/post-failover.log
echo "$(date '+%Y-%m-%d %H:%M:%S') PostFailover triggered" >> $LOG

NEW_MASTER=$(orchestrator-client -c which-instance-is-master 2>/dev/null | awk '{print $2}')
NEW_MASTER_IP=$(echo $NEW_MASTER | cut -d: -f1)

# 改两个 ProxySQL 的 hostgroup
for PROXYSQL_HOST in 10.0.0.50 10.0.1.50; do
    mysql -uadmin -padmin -h$PROXYSQL_HOST -P6032 <<EOSQL
UPDATE mysql_servers SET hostgroup=0 WHERE hostname='${NEW_MASTER_IP}' AND port=3306;
UPDATE mysql_servers SET hostgroup=1 WHERE hostgroup=0;
LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
EOSQL
done

# 激活新主 VIP
ssh root@$NEW_MASTER_IP "/usr/bin/systemctl restart keepalived"
sleep 3

# 告警
curl -X POST "https://oapi.dingtalk.com/robot/send?access_token=xxx" \
    -H 'Content-Type: application/json' \
    -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"⚠️ 自动切换完成,新主: $NEW_MASTER_IP\"}}"

echo "PostFailover done" >> $LOG

📌 实践:ApplyMySQLPromotionAfterMasterFailover: true 是关键

这一个配置实现"A 恢复后自动切回 A"——Orchestrator 看到 A 重新可达,自动把 A 加回集群,A 追平数据后自动 promote A 为主。全程 0 人工

8.7 ProxySQL 部署(每个机房一个)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# /etc/proxysql.cnf
datadir="/var/lib/proxysql"

admin_variables=
{
    admin_credentials="admin:admin"
    mysql_ifaces="0.0.0.0:6032"
}

mysql_variables=
{
    threads=4
    max_connections=2048
    interfaces="0.0.0.0:6033"
    monitor_username="proxysql"
    monitor_password="ProxySQL@123"
}

mysql_servers =
(
    { address="10.0.0.1", port=3306, hostgroup=0, max_connections=500 },
    { address="10.0.1.1", port=3306, hostgroup=1, max_connections=500 }
)

mysql_replication_hostgroups =
(
    { writer_hostgroup=0, reader_hostgroup=1 }
)

mysql_query_rules =
(
    { rule_id=1, match_pattern="^SELECT .* FOR UPDATE$", destination_hostgroup=0 },
    { rule_id=2, match_pattern="^SELECT", destination_hostgroup=1 },
    { rule_id=3, match_pattern=".*", destination_hostgroup=0 }
)

mysql_users =
(
    { username="app_user", password="AppUser@123", default_hostgroup=0, max_connections=500 }
)

应用配置:

1
2
3
4
5
spring:
  datasource:
    # A 机房应用连 A 的 ProxySQL
    # B 机房应用连 B 的 ProxySQL
    url: jdbc:mysql://10.0.0.50:6033/order

平时:写走 hostgroup 0(A),读走 hostgroup 1(B)。 A 挂时:post-failover 钩子改 hostgroup,B 升 hostgroup 0,写读都走 B。

8.8 Redis Sentinel

3 个 Sentinel 节点跨机房分布(A 2 + B 1,避免脑裂):

1
2
3
4
5
6
7
# /etc/redis-sentinel.conf
port 26379
daemonize yes
sentinel monitor mymaster 10.0.0.60 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel auth-pass mymaster YourRedisPass@123

应用配置:

1
2
3
4
5
6
spring:
  redis:
    sentinel:
      master: mymaster
      nodes: 10.0.0.61:26379,10.0.0.62:26379,10.0.1.61:26379
    password: YourRedisPass@123

🎯 避坑点:Sentinel 节点不要都放 A 机房

3 个 Sentinel 如果都放 A,A 挂了 3 个全死,无法决策。A 2 + B 1 分布确保 A 挂了仍能 2 票决策。

8.9 k8s 双集群

机房 A 集群:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: apps/v1
kind: Deployment
metadata: { name: order-service }
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: app
        env:
        - { name: DATACENTER, value: "dc-a" }
        - { name: DB_URL, value: "jdbc:mysql://10.0.0.50:6033/order" }
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: order-service-hpa }
spec:
  minReplicas: 3
  maxReplicas: 100
  metrics:
  - type: Resource
    resource: { name: cpu, target: { type: Utilization, averageUtilization: 60 } }

机房 B 集群:replicas: 3,DB_URL 改为 B 的 ProxySQL。

📌 实践:机房 B 平时为什么不是 0 副本

有些方案建议"机房 B 平时 0 副本,A 挂了扩到 100"——这是错的

  • “AB 同时分担流量"需要 B 也有应用实例承担读流量
  • 0 副本意味着 B 平时不接流量,违反"双活"语义
  • A 挂了扩到 100 副本要 30-60 秒(启动慢)
  • B 平时 3 副本:承担读流量 + A 挂了 30 秒内扩到 100

8.10 故障切换全流程

场景 1:A 挂了 → B 升主

1
2
3
4
5
6
7
8
T+0s    A 失联
T+2s    B 机房 Orchestrator 探测 A 失联
T+5s    二次确认
T+15s   promote B 为新主
T+15s   post-failover.sh 钩子自动:
        ├─ 改两个 ProxySQL hostgroup
        └─ 激活 B VIP
T+20s   业务全自动恢复 ✅

场景 2:A 恢复 → 自动切回 A

1
2
3
4
5
6
7
8
T+30min A 恢复
T+30min Orchestrator 自动把 A 加回集群
        - A 执行 CHANGE MASTER TO B
        - A 应用 B 的 binlog
T+1h    A 追平数据
T+1h    Orchestrator 自动切回 A 为主 ← ⚠️ ApplyMySQLPromotionAfterMasterFailover
        - post-failover.sh 再次触发
T+1h+   稳态恢复

关键:A 追平 B 的时间

核心公式

1
追平时间 = B 期间写的数据量 ÷ MySQL 复制速度

MySQL 复制速度(ROW 格式半同步):

硬件配置复制速度1 GB 追平时间
高配(NVMe SSD + 万兆网)500-1000 MB/s1-2 秒
中配(SSD + 千兆网)100-200 MB/s5-10 秒
普通(SATA SSD)50-100 MB/s10-20 秒

追平时间估算表

A 失联时间中等业务追平量追平时间(高配)
秒级< 1 MB0 秒
1 小时~100 MB几秒
半天~500 MB - 5 GB几秒 - 1 分钟
1 天~1-50 GB几秒 - 几分钟
1 周~10-500 GB几秒 - 1 小时
1 个月~50 GB - 5 TB几小时 - 1 天

判断追平进度(A 机器执行):

1
2
3
SHOW SLAVE STATUS\G
-- 重点看 Seconds_Behind_Master: 0 (已追平)
-- Read_Master_Log_Pos == Exec_Master_Log_Pos (binlog 应用完毕)

Orchestrator 的保护

1
2
3
4
{
  "FailMasterPromotionOnLagMinutes": 1,                 // 延迟 > 1 分钟不切
  "ApplyMySQLPromotionAfterMasterFailover": true        // 自动切回 A
}

Orchestrator 切回 A 的逻辑:

  1. A 加回集群,开始追 B 的 binlog
  2. 持续监控 Seconds_Behind_Master
  3. Seconds_Behind_Master = 0 才切
  4. A 数据没追平时不会切回,不会丢数据

🎯 避坑点:特殊场景——大库追平

A 失联超过 1 周(B 写了 100+ GB 数据):单纯 binlog 复制太慢,用 xtrabackup 物理备份

    1. B 上做物理备份(xtrabackup --backup
    1. 拷贝到 A
    1. A 替换数据目录(xtrabackup --copy-back
    1. A 接 binlog 追最后一段增量
    1. 切回 A

总耗时 2-4 小时

💡 原理:A 失联 1 天内的真实场景

中等业务 A 失联 1 天,B 期间写 ~1-10 GB:

  • 追平时间:分钟级
  • Orchestrator 等追平后自动切回 A
  • 业务感知:完全无感,0 人工

这正是 ApplyMySQLPromotionAfterMasterFailover: true 的价值——全自动,不需要 DBA 守在屏幕前

场景 3:B 挂了

1
2
3
4
5
6
T+0s    B 失联
        - A 还是主,业务无影响
        - Orchestrator 也挂了(但不影响业务)
T+30min B 恢复
T+30min Orchestrator 自动重启(systemd 拉起)
T+30min Orchestrator 接管监控

场景 4:备库 B 短暂失联 → 自动追 A

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
T+0s    B 失联(B 机器死 / 网络断 / 重启)
        ├─ A 还是主,业务无影响
        ├─ Orchestrator 探测 B 失联(但不切——B 是备)
        └─ ProxySQL 探测 B 不可用,自动剔除 B
           读流量全部回 A

T+X min B 恢复(B 机器 / MySQL 重新启动)
        ├─ B 自动启动 SLAVE
        ├─ B 用 GTID 协议自动定位 A 当前 binlog 位置
        ├─ B 自动拉取并应用 binlog
        └─ B 持续追平

T+X min + 追平时间  B 追平
        ├─ Seconds_Behind_Master = 0
        ├─ ProxySQL 自动把 B 加回 hostgroup 1
        └─ 读流量恢复 A + B 分担

关键:B 失联期间 A 还在主位,业务完全无感。B 恢复后 MySQL GTID 复制原生自动追——0 人工、0 配置。

追平时间估算(B 失联时间 → 追平耗时):

B 失联时间中等业务追平量追平时间(高配)追平时间(普通)
1 分钟< 10 MB< 1 秒< 1 秒
1 小时~100 MB - 1 GB几秒几秒 - 1 分钟
1 天~10-100 GB几秒 - 几分钟几分钟 - 半小时
1 周~50 GB - 500 GB几秒 - 半小时半小时 - 1 小时

判断追平进度(B 机器执行):

1
2
3
4
5
SHOW SLAVE STATUS\G
-- 重点看:
-- Slave_IO_Running: Yes
-- Slave_SQL_Running: Yes
-- Seconds_Behind_Master: 0    ← 0 = 追平

💡 原理:B 追 A 是 MySQL 自己的事

  • A 追 B(A 失联恢复)→ 涉及主从角色切换,需要 Orchestrator 协调
  • B 追 A(B 失联恢复)→ 备库追上主库,MySQL GTID 复制原生自动

这是 MySQL 主从复制的核心能力,不需要任何额外配置

🎯 避坑点:大库追平(> 100 GB)

B 失联超过 1 周(A 写了 100+ GB)时,单纯 binlog 复制太慢:

  • xtrabackup 物理备份:A 备份 → 拷贝到 B → B 替换数据目录 → 接 binlog 追增量
  • 总耗时 2-4 小时

但这种场景罕见——B 失联 1 天内追平都是分钟级,完全可接受。

RTO 总结

阶段耗时谁做
A 失联 → Orchestrator 探测2-5s自动
Orchestrator 二次确认5-10s自动
promote B + 切流量15-20s自动
数据库层 RTO20-30s全自动
k8s B 副本扩容30-60s自动
GTM 切流 + DNS TTL 过期1-5min半自动
完整 RTO(A 挂)1-5min
备库 B 失联恢复(追平 A)分钟级(B 失联 1 天内)全自动(MySQL GTID 复制)

8.11 落地 Checklist

阶段 1:基础设施(1-2 周)

  • 同城专线开通(< 1ms 延迟)
  • 机房之间防火墙放通(3306/6379/6033/26379/3000/9090/6443/443)
  • 时间同步(chrony,机房间 < 1ms 偏差)
  • SSH 免密(所有机器之间互信)

阶段 2:MySQL HA(1 周)

  • MySQL 8.0 安装(主 + 备)
  • 半同步 + GTID 配置
  • keepalived 部署(同子网 VIP 验证)
  • Orchestrator 部署(B 机房)
  • 演练 1:kill A mysqld → 30s B 升主

阶段 3:ProxySQL(3 天)

  • 每个机房部署 ProxySQL
  • 应用连接本地 ProxySQL
  • 验证读写分离

阶段 4:Redis HA(3 天)

  • Redis 主从 + Sentinel 3 节点
  • 演练 2:kill A Redis → 15s 切 B

阶段 5:k8s(1-2 周)

  • kubespray 部署机房 A 集群
  • kubespray 部署机房 B 集群
  • 应用部署(A 3 副本 + B 3 副本)
  • HPA 配置
  • 演练 3:杀光 A Pod → B HPA 扩容

阶段 6:监控告警(1 周)

  • Prometheus + Grafana + AlertManager
  • mysqld_exporter / redis_exporter / node_exporter
  • blackbox_exporter 多地域拨测
  • 告警规则(半同步降级必须电话级)

阶段 7:GTM(2 天)

  • 阿里云 GTM 配置
  • 健康检查 URL 指向 /health
  • DNS TTL = 60 秒
  • 演练 4:摘 A → GTM 切 B

阶段 8:端到端演练(1 周)

  • 演练 5:拔 A 网线 → 整套自动恢复
  • 演练 6:A 恢复 → 自动切回 A
  • 演练 7:kill A mysqld → 整套自动恢复
  • Runbook 完善
  • DBA 培训

阶段 9:灰度上线(1 周)

  • 1% 流量灰度 → 1 周
  • 10% → 3 天
  • 50% → 3 天
  • 100% 上线

🎯 避坑点:演练是核心

写好的脚本不演练等于不存在:

  • 演练必须包含全部场景:A 挂、A 恢复、B 挂、双机房挂
  • 每次演练都验数据一致性SHOW SLAVE STATUS + 业务对账
  • 演练失败要修脚本:不要凑合"差不多”
  • 月度常态化演练:保持肌肉记忆

8.12 风险与缓解

风险影响缓解
DNS TTL 是 RTO 瓶颈切流后老用户等 1-5minTTL 60 秒 + 切流前调小
半同步降级为异步RPO 突然变差半同步状态告警必须电话级
Orchestrator 误切数据丢失FailMasterPromotionOnLagMinutes: 1 保护
post-failover.sh bug切换后 ProxySQL 没切必须演练 5+ 次,第一次发现 bug 很正常
机房 B k8s 启动慢A 挂时 B 扩到 100 慢镜像预热 + 应用启动优化
A 恢复后追数据慢切回 A 延迟大库用 mysqldump + 物理备份恢复

🛑 误区警示:Orchestrator 部署位置

  • 不要部署在 A 机房——A 挂了 Orchestrator 也挂,没人能自动切
  • 不要以为 B 机房就完美——B 挂了 Orchestrator 也没了,但 A 还是主,业务不受影响,只是失去自动切换能力
  • 正确:Orchestrator 部署在 B 机房,接受 B 挂了失去自动切换能力(业务不受影响),B 恢复后自动起来接管

这是用业务容错换运维简化的最优解——比 Raft 3 节点简单一个数量级。

8.13 成本估算

自建方案年成本约 ¥24 万(含 30 台服务器 5 年折旧、同城专线、阿里云 GTM + 域名 + 拨测公网 IP、机房机柜电力)。对比阿里云托管方案(PolarDB + Tair + ACK)约 ¥60-80 万/年自建节省 60-70%


九、常见问题 FAQ

Q1:异地多活和微服务是什么关系?

A:微服务是架构风格,异地多活是部署形态。两者正交:

  • 微服务可以单机部署(最简单)
  • 微服务可以同城双活部署(最常见)
  • 微服务可以异地多活部署(最高级)
  • 单体应用也可以做异地多活(少见但可行)

微服务让"单元化"更容易实施(服务粒度细,可以独立切分),但异地多活不强制要求微服务

Q2:异地多活和分布式事务是什么关系?

A强烈不建议在异地多活架构上用分布式事务

分布式事务(2PC / 3PC / TCC)依赖"事务协调者"在多个机房之间协调,跨机房延迟 30-100ms 会让事务延迟飙升到秒级。用户无法接受秒级的下单响应

异地多活的正确做法:通过单元化避免跨机房事务。如果实在无法避免(全局服务),用最终一致性(异步消息 + 幂等消费)替代强一致事务。

Q3:异地多活一定要用云厂商吗?

A:不一定。自建机房也可以做异地多活,但成本更高:

  • 自建机房要买地、买设备、招运维
  • 云厂商提供现成的 Region + 专线 + 中间件

多数公司选择公有云 + 多 Region 部署。少数大厂(阿里、字节)会自建机房 + 私有云。

Q4:异地多活最大的坑是什么?

A:根据多家公司公开复盘,最大的坑是"故障时不会切"

具体表现:

  • 切换脚本 3 年没演练过,目标机房 ID 是错的
  • 切换流程依赖某个离职员工的个人经验
  • 监控系统告警了但没人知道该做什么
  • 切过去后业务不通,又切回来,来回切几次

异地多活不是"建好就完事"没有持续演练的异地多活形同虚设

Q5:异地多活的成本能降下来吗?

A:能,但有上限。

降成本的方向:

  • 复用云厂商资源:用公有云 + 容器化,资源按需扩缩
  • 共享存储:用云数据库(RDS / PolarDB)替代自建 MySQL,运维成本降 50%
  • 自动化运维:用 K8s + Operator 自动化部署和切换
  • 共享中间件:用 Nacos / Sentinel / Seata 等开源组件替代自研

但底线是:异地多活的硬件成本不会低于同城的 3-5 倍。这是物理规律(要买双倍资源),不是软件能省的。

Q6:异地多活和 Service Mesh 是什么关系?

AService Mesh 是异地多活的"好搭档"

异地多活要求"禁止跨机房调用",Service Mesh 可以在 Sidecar 层面自动拦截所有跨机房调用:

  • 标记请求属于哪个机房
  • 拦截违规的跨机房调用
  • 自动重试 + 限流
  • 统一的灰度切流

没有 Service Mesh 也能做异地多活(靠代码规范 + 人工 Review),但有 Service Mesh 会轻松很多。Istio / Linkerd / 自研 Mesh 都是常见选择。

Q7:异地多活会影响性能吗?

A会,但可控

性能影响来源:

  • 入口打标 + 路由:每次请求多 1-3ms(路由层查询)
  • 同步复制:写入延迟可能增加(等同步完成)
  • 跨机房调用:理论上 0,违规时可能 30-100ms

通过单元化设计,可以把跨机房调用控制在 0.01% 以下。剩下的性能开销 ≈ 3-5ms,用户几乎感知不到。

十、推荐阅读

如果想深入异地多活的工程实践,以下资料值得一读:

书籍

  • 《数据密集型应用系统设计》(DDIA, Martin Kleppmann)—— 分布式系统圣经,第 5、6 章讲复制与分区
  • 《大型网站技术架构:核心原理与案例分析》(李智慧)—— 阿里技术专家作品,前几章讲架构演化

公开技术博客

  • 阿里中间件团队博客:异地多活、双 11 备战系列
  • 美团技术团队博客:OCTO 框架、单元化实践
  • 字节跳动技术博客:Service Mesh 与多活架构

开源组件

  • Canal:阿里开源 MySQL binlog 订阅
  • Otter:阿里开源数据库同步工具
  • RedisShake:Redis 数据同步
  • MongoShake:MongoDB 数据同步
  • Nacos:阿里开源服务发现 + 配置中心
  • Sentinel:阿里开源流量治理

💡 学习路径建议

  1. 入门:本文 + DDIA 第 5 章,理解复制模型
  2. 进阶:阿里中间件博客 + 美团 OCTO 系列,理解工业实践
  3. 实战:研究 Canal / Sentinel / Seata 源码,理解组件原理
  4. 深潜:动手做小规模双活 PoC,体验真实延迟与同步问题

理论 + 实践 + 源码,缺一不可。

参考文章

本系列共 16 篇,本文为第 1 篇 · 查看全部
使用 Hugo 构建
主题 StackJimmy 设计