
你好,我是码哥。
行业里有过这样一个真实案例的重述。某公司的业务全压在单机房,一次机房网络割接公告发出来,全员待命,运维盯着监控,一旦抖动就得手动切。后来那机房真断过网,按他们的体量估算,停摆一小时就是千万级的损失。
这故事听多了,就会想,要是有两个机房同时扛活,挂一个另一个顶上,不就不用半夜爬起来了吗。
但真去落地的人会发现,同城双活最难的不是把机器摆到两个机房。是数据怎么保持一致,流量怎么调度,这两件事怎么咬合到一起。
很多人以为双活就是「在两个机房都部署一套」。坦白讲,那是把问题想简单了。真正的双活,核心是尽量别让请求跨机房。
先别急着选方案,得先认清楚你面对的到底是哪一种容灾。这三者常被混为一谈,但它们的工程难度和成本根本不在一个量级。
主备灾备,是最朴素的一种。主机房扛读写,备机房平时只同步数据、不接流量,利用率只有 30% 到 50%。一旦主机房挂了,人工把流量切到备机房,RTO 通常要分钟级。问题是备机长期闲置,真到切换时谁心里都没底。
同城双活,两个机房都在扛流量,机房级故障能做到 RTO 秒级,RPO 可以压到 0。它覆盖的是「单机房断电、断网、光缆被挖」这类城市内故障,已经能挡住 99% 的容灾场景。
异地多活,跨城市甚至跨国部署,挡的是城市级灾难。代价极其昂贵,数据延迟从 1ms 跳到 30ms 以上,冲突处理复杂到多数公司根本驾驭不了。
多机房部署「能不做则尽量不要做」,真要做同城双活已足够,异地多活「轻易不要尝试」。
演进路径其实是一条清晰的决策链,单机房受制于人,一次断网就损失千万,升级到同城双活拿到机房级容灾,复杂度还可控,再往上走异地多活是给淘宝、支付宝那个体量准备的。

三种容灾形态对比:主备 / 同城双活 / 异地多活的 RTO、RPO、利用率与适用故障域
延迟是这一切的物理天花板。同机房内 0.1ms,同城小于 100km 约 1ms,北京到天津小于 10ms,北京到上海约 30 到 38ms,北京到广州约 50 到 53ms,跨国能到 100 到 200ms。同城双活之所以可行,根子就在那 1ms 的专线延迟上。

网络延迟阶梯:同机房 0.1ms → 同城 1ms → 京津 10ms → 京沪 38ms → 京广 53ms → 跨国 200ms,同城双活可行边界在 1ms 专线
很多讲双活的文章把重点放在「部署」,但码哥眼里,部署只是体力活。真正卡死人的,是两件事咬合,一是数据怎么在两个机房保持一致,二是流量怎么调度才能让两边不打架。
一个反直觉的事实,同城双活的核心不是「双活」,是尽一切可能避免跨机房调用。所有设计,就近路由、本机房闭环、用户绑定单机房,全是围着这个目标转。
为什么跨机房读不可接受,算一笔账就清楚。一次接口调用里,业务可能读缓存或 DB 十几到几十次。如果这几十次里有跨机房的,每次几十毫秒乘上调用次数,几百毫秒就叠出来了,直接突破 200ms 的接口预算。所以核心设计目标必须定成,读和 RPC 都在本机房闭环。
再说一个更反直觉的判断,市面上一堆文章吹「完全双活」,但真正读写都双活的极少,绝大多数生产系统是「读双活、写单主」。承认这一点,比吹嘘诚实得多,也更能帮你做对选型。
数据同步和流量调度是同一枚硬币的两面。数据同步决定了你敢不敢让两个机房都写,流量调度决定了用户会不会在两边来回横跳把数据写冲突。下面两节,我们把这两件事拆开讲透。
这是最适合刚起步的团队。主库集中在 A 机房,B 机房部署从库, MySQL 通过 MHA 主从复制同步,只适合相距小于 50km 的机房。
读请求,各机房读本机房的从库,再叠一层本地缓存压住主从延迟。写请求,跨机房打到 A 机房主库。这么设计的前提是,你接受「写必然跨机房一次」,但读全部本地闭环。
一致性兜底有个巧办法。客户端本地记一条数据的「最后修改时间戳」,带上它去请求,服务端比对缓存更新时间,如果发现缓存比客户端记录的时间还旧,就触发一条同步指令回源从库查最新数据,放进本机房缓存,避免读到过期值。
请求调度上,让一个用户短期内只访问一个机房,靠 HttpDNS 把用户绑死。这一步很关键,它能防止用户来回切换,导致两个机房同时改同一条数据引发合并冲突。

核心数据中心架构:A 机房主库 + B 机房从库,写跨机房打到主库、读本地从库闭环
但这套方案的天花板很低。核心机房一离线,其他机房就没法写。故障后要人工改各个 proxy 的主从配置才能恢复,恢复完还得人工把主从同步接上。
主从延迟会让刚写的数据延迟才出现在备机房。更坑的是,专线偶发故障,临时走公网延迟在 10 到 500ms 之间波动,主从延迟直接大于 1 分钟。
还有一个经典雷,主从不同步期间自增 ID 会重复,直接主键冲突。解法只有一个,换 Snowflake,让两机房的 ID 区间天然错开。
// 自增主键在双主双向同步时会撞车,必须换成 Snowflake
// datacenterId 按机房分配,机房 A 用 1,机房 B 用 2,ID 区间天然错开
long id = (timestamp << 22) | (datacenterId << 17) | (workerId << 12) | sequence;
// 这样双向同步时两边生成的 ID 永不重叠,根治主键冲突
跨过单主的天花板,就到了第二套方案,Otter 双向同步。每机房各自有主库和从库,Otter 用 Canal 监控本机房主库的 binlog,经 SETL 整理后同步到对面机房的 Node,实现双机房双向同步。业务只操作本地主库,不用管对面。
冲突处理分两层。行冲突,比对修改时间或者回源查询,用较新的覆盖目标库。字段冲突,按修改时间覆盖,或者把多个修改动作合并,比如 A 机房减 1、B 机房减 1 合并成减 2,实现最终一致。

Otter 双向同步数据流:Canal 监听 binlog 经 SETL 整理推送到对面机房 Node
这里有个要命的警告,合并方式不适合库存类数据。库存合并会超卖,A 减 1、B 减 1 合并成减 2,但库存只有 1 的时候,超卖就发生了。库存类数据要用长期缓存,或者干脆单机房整体事务解决,别迷信合并。
业务改造逃不掉两件事。自增主键换成 Snowflake,唯一索引互斥换成分布式互斥锁。Otter 自身也做了优化,对同一条数据多次修改的操作日志做合并,网络传输和同步策略上做滑窗并行。
故障切换时,Manager 点一下「切换」就能切 Canal 和主从方式。同城双向同步下,机房故障只需要把流量引到正常机房,代码里的 MySQL 主从 IP 不用改,因为同步是双向的,故障修好后恢复同步即可。Otter 相比 MySQL 原生同步约有 5 倍性能提升,但只适合 50 到 100km 的同城。

Otter 双主双活架构:两机房各自主库经 Canal 与 Node 互相同步
避坑清单,这几条都是踩过的。pipeline 建议设为忽略 DDL 同步错误,变更表结构一般先改从库。表加字段只能加在表尾,不能删老字段,而且先把新字段同步到目标库,再同步主库,否则丢数据。双向同步表新增字段不要带默认值。Otter 不支持无主键表同步,建表时主键是硬要求。
前面两套方案,核心数据中心是自己写主从调度,Otter 是在应用层用 Canal 兜一层。它们本质上是「给单机 MySQL 打补丁」。
要少打补丁,就把同步能力往下沉——沉到数据库内核,甚至直接用出生就是分布式的库。
MHA,主备时代的自动切换标准件。 MHA(Master High Availability)不解决双活,它解决的是「主库挂了谁顶上」。
它常驻监控主库心跳,主库失联后从一堆从库里挑数据最全的那个,把差异的 relay log 补齐、提升为新主,再把 VIP 漂过去。
但它依赖异步复制,切换瞬间没同步过去的事务就丢了,RPO 大于 0;切换本身也有秒级到分钟级中断。所以 MHA 是主备灾备时代的答案,到同城双活这档,它只算「切换更自动」,离双活还差得远。
MGR,MySQL 内核自带的组复制。 MGR(MySQL Group Replication)是 MySQL 8 内置的能力,底层跑的是 Paxos(具体是 XCom 组通信)。单主模式(single-primary,生产推荐)下全局只有一个可写节点,事务要等组内多数派确认才提交,天然靠 Paxos 防脑裂,不用你再单独搞 Witness。
把组成员分布到两个机房,单机房挂了只要多数派还在,组内自动选新主继续服务,RTO 秒级、RPO 等于 0。
比起 Otter,它省掉了 Canal、省掉了自己解析 binlog,数据库内核一次性把同步、冲突、选主都做了。
代价是节点要奇数、网络要稳、写有放大,且同样只适合同城——Paxos 一轮要跨机房往返,延迟一高就扛不住。多主模式虽支持多写,但冲突检测开销大,生产里很少用。
OceanBase,出生就是分布式的库。 如果说 Otter 是应用层补丁、MGR 是 MySQL 内核补丁,那 OceanBase 是从设计上就为多副本、跨机房生的。核心是分区(tablet)多副本 + Paxos:每个分区有一主(leader)多备(follower),leader 就是读写点;
同城双活常见做法是同城三机房部署,或「4 同城 + 1 异地」五副本。靠 Locality / Primary Zone 把 leader 固定在主机房,读写天然落本机房,正好就是前面说的「本机房闭环」;单机房故障,RootService 自动把 leader 切到同城另一机房的副本,RPO=0、RTO 秒级。
写入走多数派(quorum)确认,所以哪怕一个机房整体没了,只要多数派存活数据就不丢。一句话,双活对 OceanBase 不是 feature,是出厂设置。

三种数据库同步方式对比:MHA / MGR / OceanBase 在同步机制、防脑裂、RPO、RTO、读写模型、定位上的差异
数据同步定了调子,流量调度就得跟上。目标是同一用户的请求,尽量一辈子待在一个机房里。
接入层用 GSLB 或智能 DNS、HttpDNS,按用户地域就近把人分到最近机房。WAF、API 网关跨可用区部署,集群完全不可用时支持 bypass,DNS 直接绕过走备用链路。

就近路由与机房闭环:GSLB 入口按地域分流 + RPC 同机房服务组本机房闭环
应用层要做无状态化改造,session 存进 Redis 集群,双中心互备,哨兵跨中心。RPC 服务在注册中心按机房注册成不同服务组,客户端只订阅同机房的服务组,调用默认落在本机房。
# 接入层按机房划分 upstream,配合 GSLB 把用户就近引到本机房
upstream bj1_cluster { server 10.0.1.10; server 10.0.1.11; }
upstream bj2_cluster { server 10.0.2.10; server 10.0.2.11; }
# GSLB 按用户地域解析到对应机房的 VIP,请求入口就先就近,后面 RPC 才不跨机房
// 注册中心按机房注册不同服务组,客户端只订阅同机房分组
// 这样 RPC 调用默认落本机房,避免跨机房把延迟叠爆
registry.register(serviceName, localIp, metadata.put("zone", "bj1"));
// 客户端过滤 zone 等于本机房的节点,本机房全挂才降级跨机房调用
用户绑定单机房这一步,和方案一里 HttpDNS 绑人的思路一致,是防冲突的底层逻辑。双机房相当于镜像部署两套独立集群,数据仍单点写主机房再实时同步,读流量机房内闭环。双机房物理专线必须高可用,至少两根互备,专线故障时才有机会绕行。
一个机房挂了怎么切,是双活能不能睡安稳觉的试金石。同城双活的目标指标,RTO 秒级,小于 30 秒,RPO 等于 0,前提是同步复制。对比传统主备,RTO 通常是分钟级,这中间的差距就是业务敢不敢半夜不爬起来的底气。
切换要不要改业务代码,取决于你的同步形态。双向同步下基本不用改,流量引走就行。单主加从库形态下,得改 proxy 的主从配置,这也是它不如双向同步省心的地方。
防脑裂是同步复制绕不开的坑。主库网络分区或者宕机时,如果两边都把自己提升为主,数据就分叉了。现代做法引入独立的仲裁节点,叫 Witness,部署在第三机房或云端,只监听主备心跳,不存业务数据。主库失联时,由仲裁加上存活节点共同判定,避免两边都自认为主。
-- PostgreSQL 内核级 WAL 同步复制,主库等至少一个备库确认落盘才返回,保证 RPO=0
-- 对应 MySQL 是半同步复制,思路一致,都是用确认机制换零丢失
synchronous_standby_names = 'standby_bj2'; -- 备库在另一机房,专线<1ms时写损耗可接受
synchronous_commit = on;

故障切换与防脑裂决策流:Witness 独立仲裁节点判定主库存活,避免双主分叉
备机房有两条铁律,碰了就污染数据一致性。第一,禁用定时任务,备环境乱跑任务会写出脏数据。第二,禁用 Redis 队列消费,注册中心要做隔离,发布系统得兼容双环境,切换时自动替换环境变量,DB 连接串和服务地址一并换掉。这些做不到,备环境就是一颗不定时炸弹。
把前面所有零件拼成一套生产形态,就是各层双中心部署。从 Nginx 层、Tomcat 层,到 Redis、MySQL 层全部双中心部署,任一层故障都能灵活切换。入口流量随机进,内部 RPC 就近路由闭环在同机房。
数据层用 DTS 做双向或单向同步,或者直接上数据库内核级 WAL 同步复制拿 RPO=0。
接入层 GSLB 加智能 DNS 就近分人,网关跨可用区且支持 bypass。
应用层无状态化,session 进 Redis 双中心互备,RPC 按机房分组注册实现本机房闭环。防脑裂靠独立的 Witness 仲裁节点。

现代云原生双中心总览:接入层、应用层、数据层、仲裁层全栈双中心部署
资源利用率上,主备模式备机闲置在 30% 到 50%,双活能拉到 60% 到 90%,某银行案例一年省下 IT 成本超千万。还有一个金融案例的数据,网络延迟每增加 1ms,交易失败率上升约 0.3%,这正是死磕专线延迟的理由。
双活之外还要配合降级容灾,削峰、写降级成异步写缓存、多级缓存加 ES 兜底、本地缓存扛读热点。本文重心在双活,降级只点到为止,但它和双活是同一套容灾体系的两面。
落地的铁律就一句,能不做多机房就不做,真要做同城双活够用,异地多活是给顶级体量准备的,普通公司碰它多半是给自己挖坑。
问,双向同步怎么防止数据在机房之间无限循环给同步过去的数据打上标记,Otter 这一类工具在同步时写入特殊标识,对面机房识别到带标记的数据就不再回传,循环自然断掉。这是双向同步绕不开的设计,也是很多人的思考题标准答案。
问,读双活写单主,算不算伪双活不算。读写都双活的代价是冲突处理复杂度指数上升,绝大多数生产系统选择读双活、写单主,是用「写集中」换「一致性可控」。这是诚实的工程取舍,不是偷懒。
问,同城双活做到什么程度才该升级异地多活当且仅当你的业务对城市级灾难零容忍,且能承受 30ms 以上延迟和天文数字的成本时,才考虑异地多活。否则同城双活挡住的 99% 场景已经够用。
问,Otter 自己挂了,同步链路断了怎么办Otter 的 Manager 和 Node 都可独立部署,单点故障不影响数据库本身。链路断了,两边数据库继续各自写入,恢复后 Otter 基于 binlog 位点续传,追平即可,前提是没有在中断期间发生双向写同一条记录的冲突。
问,备机房能不能跑定时任务、消费队列不能。备机房必须禁用定时任务和 Redis 队列消费,否则会写出主库没有的数据,破坏一致性。这是备机房铁律,没有商量的余地。