【架构实战】多活架构:异地多活与单元化部署
2026/7/23 15:34:33 网站建设 项目流程

一、那场"机房断电"事故

2022年7月,某天下午3点,我们主机房所在的城市突发大规模停电

虽然机房有UPS和柴油发电机,但运营商骨干网也受到了影响。

结果

  • 主机房服务完全不可用
  • 备用机房冷备状态,切换需要4小时
  • 4小时内,业务完全中断
  • 直接经济损失:超过500万
  • 品牌损失:不可估量

CEO紧急会议上的灵魂拷问

  • “为什么备用机房没有自动切换?”
  • “为什么我们的业务不能在其他城市运行?”
  • “如果再发生一次,我们还要中断4小时吗?”

我们没办法回答。

痛定思痛,我们启动了异地多活项目,历时18个月,完成了异地多活+单元化的架构升级。

今天就分享这个项目的实战经验——异地多活不是简单的事,是系统性的架构革命


二、多活架构的本质:不止是容灾

2.1 传统容灾 vs 多活

传统容灾(同城灾备/异地灾备)

【主备模式】 主机房(Active)──> 服务用户 │ │(冷备/温备) ↓ 备机房(Standby)──> 不服务用户 特点: - 平时备机房不工作 - 故障时切换(分钟级~小时级) - 备机房资源浪费 - 数据单向同步

异地多活

【多活模式】 机房A(华东)──┐ ├──> 同时服务用户 机房B(华南)──┤ ├──> 流量分担 机房C(华北)──┘ 特点: - 多个机房同时服务 - 故障时自动切换(秒级) - 资源充分利用 - 数据双向同步

核心区别

  • 灾备:备用,平时不工作
  • 多活:多份,同时工作

2.2 多活的四种模式

模式特点RTO成本复杂度
同城双活同一城市两个机房秒级
同城多活同一城市多个机房秒级
异地多活(读写分离)不同城市,按业务分分钟级
异地多活(双向同步)不同城市,任意写入秒级极高极高

我们的选择异地多活(读写分离)—— 平衡成本和可用性。

2.3 多活的核心挑战

多活不是简单部署多份

挑战1:数据一致性 └── 多机房数据如何同步?延迟?冲突? 挑战2:流量调度 └── 请求路由到哪个机房?灰度?切量? 挑战3:业务复杂度 └── 跨机房调用?ID生成?数据归属? 挑战4:运维复杂度 └── 监控?部署?容灾? 挑战5:成本 └── 资源翻倍?网络成本?

我们用了18个月,不是因为技术难,而是因为要确保业务在多活下不出问题


三、单元化:多活的最佳实践

3.1 什么是单元化

核心思想以"用户"为单位,把用户绑定到特定机房

【传统多活】 用户A ──> 机房1 ──> 写入数据 ──> 数据同步 ──> 机房2 用户B ──> 机房2 ──> 写入数据 ──> 数据同步 ──> 机房1 问题:双向同步,数据冲突可能 【单元化】 用户A(unit=1) ──> 机房1 ──> 写入 ──> 数据留在机房1 用户B(unit=2) ──> 机房2 ──> 写入 ──> 数据留在机房2 用户C(unit=3) ──> 机房3 ──> 写入 ──> 数据留在机房3 特点: - 用户绑定机房 - 数据不跨机房流动 - 机房故障只影响本单元用户

单元化的核心

  • 用户ID决定机房归属
  • 同一用户的数据只在一个机房
  • 跨单元数据通过同步或消息

3.2 单元化 vs 普通多活

维度普通多活单元化
数据写入任意机房归属机房
数据同步双向同步单向同步
冲突解决复杂无冲突
扩容受限容易(增加单元)
成本更高
复杂度极高

3.3 单元划分策略

按用户ID分片

/** * 单元划分算法 */publicclassUnitRouter{// 假设我们有10个单元privatestaticfinalintUNIT_COUNT=10;/** * 根据userId计算单元 */publicintgetUnitId(StringuserId){// 取userId的hash,模10inthash=Math.abs(userId.hashCode());returnhash%UNIT_COUNT;}/** * 根据unitId获取机房 */publicStringgetDataCenter(intunitId){// 单元0-3 → 华东机房// 单元4-6 → 华南机房// 单元7-9 → 华北机房if(unitId<=3)return"dc-east";if(unitId<=6)return"dc-south";return"dc-north";}}

按业务分片

/** * 业务单元划分 */publicclassBusinessUnitRouter{publicStringgetDataCenter(StringbusinessType){switch(businessType){case"ALIPAY":return"dc-east";// 支付宝业务在华东case"WECHAT_PAY":return"dc-south";// 微信支付在华南case"UNION_PAY":return"dc-north";// 银联在华北default:return"dc-east";}}}

四、多活架构设计

4.1 总体架构

【异地多活 + 单元化 架构】 用户 │ ↓ ┌──────────────┐ │ DNS/GSLB │ ← 全局负载均衡 │ (流量调度) │ └──────┬───────┘ │ ┌────────────┼────────────┐ ↓ ↓ ↓ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 华东机房 │ │ 华南机房 │ │ 华北机房 │ │ (主) │ │ (备1) │ │ (备2) │ │ 单元0-3 │ │ 单元4-6 │ │ 单元7-9 │ │ │ │ │ │ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │网关 │ │ │ │网关 │ │ │ │网关 │ │ ← 单元化路由 │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │业务 │ │ │ │业务 │ │ │ │业务 │ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │数据 │ │ │ │数据 │ │ │ │数据 │ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ └─────┬────┘ └─────┬────┘ └─────┬────┘ │ │ │ └────────────┴────────────┘ ↑ 数据同步层 (单向同步 + 消息队列)

4.2 流量调度:GSLB全局负载均衡

/** * GSLB流量调度 */@ComponentpublicclassGSLBRouter{@AutowiredprivateHealthCheckerhealthChecker;/** * 根据用户ID选择机房 */publicStringroute(StringuserId,Stringurl){// 1. 单元化路由:同一用户始终到同一机房intunitId=getUnitId(userId);StringtargetDC=getDataCenter(unitId);// 2. 健康检查:目标机房不可用,路由到备机房if(!healthChecker.isHealthy(targetDC)){targetDC=getBackupDataCenter(targetDC);log.warn("机房{}不可用,用户{}路由到备机房{}",getDataCenter(unitId),userId,targetDC);}returntargetDC;}/** * 健康检查 */publicbooleanisHealthy(StringdataCenter){// 检查机房健康状态returnhealthChecker.check(dataCenter);}}

GSLB的几种实现

  • DNS级别:通过DNS解析返回不同IP(延迟高)
  • HTTP级别:通过302/重定向(用户感知)
  • IP级别:直接路由(性能最好)

4.3 数据同步:单元化多活的关键

同步策略

/** * 基于Binlog的数据同步 */@ComponentpublicclassDataSyncManager{@AutowiredprivateCanalClientcanalClient;// 监听MySQL Binlog/** * 数据同步:从华东同步到华南 */@CanalEventListenerpublicvoidonChange(CanalEntryentry){// 1. 解析BinlogStringtableName=entry.getHeader().getTableName();RowChangerowChange=entry.getRowChange();for(RowDatarowData:rowChange.getRowDatasList()){// 2. 检查是否需要同步(按单元ID过滤)StringuserId=getUserIdFromRow(rowData);intunitId=getUnitId(userId);intsourceUnit=getCurrentUnit();if(unitId==sourceUnit){// 3. 同步到其他机房syncToOtherDCs(tableName,rowData);}}}/** * 异步消息同步 */publicvoidsyncToOtherDCs(StringtableName,RowDatarowData){// 通过消息队列异步同步rocketMQTemplate.asyncSend("data-sync-topic",newDataSyncMessage(tableName,rowData),newSendCallback(){@OverridepublicvoidonSuccess(SendResultsendResult){log.info("数据同步成功: table={}, row={}",tableName,rowData);}@OverridepublicvoidonException(Exceptione){log.error("数据同步失败",e);// 重试或人工处理}});}}

同步策略对比

策略延迟一致性适用
同步双写关键数据
异步同步最终一般数据
消息队列最终事件型数据
定时同步最终非实时数据

我们用的组合

  • 关键数据(订单、支付):同步双写 + 异步校验
  • 一般数据(用户信息):异步同步
  • 日志型数据:消息队列

4.4 唯一ID生成

分布式ID的挑战

/** * 单元化ID生成(基于Snowflake) */@ComponentpublicclassUnitizedIdGenerator{/** * Snowflake ID结构: * 0 | 00000000 00000000 00000000 00000000 00000000 0 | 00000 | 00000 | 000000000000 * 1 | 时间戳(41位) | 数据中心(5位) | 机器(5位) | 序列(12位) */privatefinallongtwepoch=1288834974657L;privatefinallongdatacenterIdBits=5L;privatefinallongworkerIdBits=5L;privatefinallongsequenceBits=12L;privatelongdatacenterId;// 数据中心IDprivatelongworkerId;// 机器IDprivatelongsequence=0L;publicsynchronizedlongnextId(){longtimestamp=timeGen();if(timestamp<lastTimestamp){thrownewRuntimeException("时钟回拨");}if(timestamp==lastTimestamp){sequence=(sequence+1)&((1<<sequenceBits)-1);if(sequence==0){timestamp=tilNextMillis(lastTimestamp);}}else{sequence=0L;}lastTimestamp=timestamp;return((timestamp-twepoch)<<(datacenterIdBits+workerIdBits+sequenceBits))|(datacenterId<<(workerIdBits+sequenceBits))|(workerId<<sequenceBits)|sequence;}}

关键点

  • ID中嵌入机房标识
  • 即使各机房独立生成,全局也不会冲突
  • 可以从ID中反解机房

五、单元化部署实践

5.1 单元化路由

/** * 单元化网关:路由请求到正确的机房 */@RestControllerpublicclassUnitizedGateway{@AutowiredprivateUnitRouterunitRouter;@RequestMapping("/api/{service}/**")publicResponseEntity<?>route(@PathVariableStringservice,@RequestHeader("User-Id")StringuserId,HttpServletRequestrequest){// 1. 计算用户所属单元intunitId=unitRouter.getUnitId(userId);StringtargetDC=unitRouter.getDataCenter(unitId);// 2. 转发到目标机房StringtargetUrl=buildTargetUrl(targetDC,service,request);// 3. 转发HTTP请求returnforward(targetUrl,request);}}

5.2 单元化数据库

每个单元有独立的数据库

【数据库分片】 数据库0(dc-east):unit 0, 1 数据库1(dc-east):unit 2, 3 数据库2(dc-south):unit 4, 5 数据库3(dc-south):unit 6 数据库4(dc-north):unit 7, 8 数据库5(dc-north):unit 9

ShardingSphere配置

# sharding-rule.yamlrules:-!SHARDINGtables:t_order:actualDataNodes:ds_${0..5}.t_order_${0..7}databaseStrategy:standard:shardingColumn:user_idshardingAlgorithmName:db-inlinetableStrategy:standard:shardingColumn:user_idshardingAlgorithmName:t-order-inlineshardingAlgorithms:db-inline:type:INLINEprops:algorithm-expression:ds_${(user_id.hashCode() % 10 / 2).intValue()}t-order-inline:type:INLINEprops:algorithm-expression:t_order_${user_id.hashCode() % 8}

5.3 跨单元查询

业务上避免跨单元查询

必须跨单元查询时

/** * 跨单元查询服务 */@ServicepublicclassCrossUnitQueryService{@AutowiredprivateUnitRouterunitRouter;/** * 查询用户的所有订单(用户的所有订单都在本单元) */publicList<Order>getUserOrders(StringuserId){// 同单元查询returnorderRepository.findByUserId(userId);}/** * 查询商品的所有订单(跨单元) */publicList<Order>getProductOrders(StringproductId){// 跨单元查询List<Order>allOrders=newArrayList<>();for(intunitId=0;unitId<10;unitId++){Stringdc=unitRouter.getDataCenter(unitId);List<Order>orders=queryUnitOrders(dc,productId);allOrders.addAll(orders);}returnallOrders;}}

优化

  • 数据冗余:把商品信息冗余到各单元
  • 搜索引擎:用ES做全局索引
  • 数据仓库:T+1同步到数仓

六、多活运维

6.1 多活监控

多活的特殊监控

# 多活专用监控指标-机房健康度-CPU使用率-内存使用率-磁盘空间-网络连通性-数据同步状态-同步延迟(秒)-同步积压(条数)-同步错误率-流量分配-各机房QPS-单元分布-异常流量-业务可用性-各机房业务成功率-跨机房调用成功率-用户体验指标

6.2 多活演练

演练场景

【演练场景设计】 场景1:单机房故障 操作:停止华东机房 验证: - 流量自动切到华南、华北 - 单元0-3的用户自动路由到备机房 - 业务成功率保持>99% 场景2:数据库主从切换 操作:手动切换数据库主从 验证: - 业务不中断 - 同步无积压 场景3:网络抖动 操作:注入网络延迟 验证: - 跨机房调用有超时和重试 - 用户体验无明显下降 场景4:数据不一致 操作:人为制造数据冲突 验证: - 监控告警触发 - 修复机制有效

6.3 多活容灾切换

切换流程

【多活切换流程】 1. 故障发现(0-2分钟) └── 监控告警 2. 故障确认(2-5分钟) └── 运维确认 3. 切换决策(5-10分钟) └── 值班SRE、技术总监 4. 流量切换(10-15分钟) └── GSLB调整权重 └── DNS切换 └── 单元重新分配 5. 业务验证(15-30分钟) └── 核心业务验证 └── 监控持续观察 6. 故障恢复(30分钟后) └── 故障机房修复 └── 数据同步补偿 └── 流量回切

灰度切流

/** * 灰度切流 */@ComponentpublicclassGrayscaleRouter{privatevolatileintgrayPercentage=0;// 灰度比例publicStringroute(StringuserId){intunitId=unitRouter.getUnitId(userId);StringprimaryDC=unitRouter.getDataCenter(unitId);StringbackupDC=getBackupDataCenter(primaryDC);// 灰度用户走备机房if(isGrayUser(userId)&&grayPercentage>0){returnbackupDC;}returnprimaryDC;}}

七、踩坑总结

7.1 坑1:数据双向同步导致冲突

症状:两个机房同时写同一条数据,冲突。

解决

  • 单元化:用户绑定机房,不双向写
  • 明确写入策略:每个用户只在固定机房写入
  • 冲突检测:监控和告警

7.2 坑2:流量切换不均匀

症状:切流量时,部分用户请求失败。

解决

  • 灰度切流:先切1% → 10% → 50% → 100%
  • 健康检查:备机房健康才切
  • 回滚预案:出问题立即回切

7.3 坑3:跨机房调用延迟

症状:跨机房调用一次增加50ms延迟。

解决

  • 业务上避免跨机房调用
  • 数据冗余:把数据冗余到本机房
  • 缓存:热点数据全机房缓存

7.4 坑4:时钟不同步

症状:多机房时间不一致,分布式锁失效。

解决

  • NTP时间同步
  • 逻辑时钟:使用Sequence代替时间戳
  • 时钟监控:监控机房时间差

7.5 坑5:多活改造不彻底

症状:核心服务多活了,但依赖服务没有。

解决

  • 全链路多活:所有依赖都支持多活
  • 降级方案:依赖未多活时有兜底
  • 分阶段改造:先核心后边缘

八、多活成本与收益

8.1 成本分析

多活的成本

项目成本说明
服务器翻倍至少2倍资源
网络3-5倍跨机房专线流量
存储翻倍多份数据
运维翻倍复杂度增加
改造成本一次性1-2年研发投入

我们项目的成本

  • 服务器增加:120%
  • 网络成本:增加300%
  • 研发投入:18人月
  • 运维增加:50%

8.2 收益分析

直接收益

  • 避免业务中断:每年节省潜在损失1000万+
  • 资源利用率提升:从30%提升到70%
  • 弹性伸缩:应对流量波峰波谷

间接收益

  • 用户体验提升
  • 品牌信任度提升
  • 业务连续性保障

8.3 ROI评估

投入:18个月研发 + 50%资源增加 收益:避免一次机房级故障即可收回成本 结论:对于核心业务,多活是值得的

但不是所有业务都需要

  • 核心业务(订单、支付):必须多活
  • 重要业务(用户、商品):应该多活
  • 边缘业务(评论、日志):可以多活
  • 内部系统:不必多活

九、实战经验总结

9.1 多活的核心原则

1. 业务优先:先想清楚业务怎么用,再设计架构

2. 单元化是核心:通过用户ID绑定机房,避免数据冲突

3. 渐进式实施:先核心后边缘,先双活后多活

4. 灰度切流:流量切换要可灰度、可回滚

5. 全链路多活:所有依赖都要支持多活

6. 演练验证:不演练等于没做多活

9.2 多活的实施步骤

Step 1:业务梳理(1-2个月)

  • 哪些业务支持多活
  • 哪些需要单元化
  • 数据归属分析

Step 2:基础设施准备(2-3个月)

  • 机房建设/租用
  • 网络专线
  • GSLB配置

Step 3:核心服务多活(6-8个月)

  • 订单、支付等核心服务
  • 数据库分片
  • 数据同步

Step 4:业务系统迁移(3-4个月)

  • 业务系统适配多活
  • 单元化路由
  • 跨单元查询

Step 5:演练验证(2-3个月)

  • 各种故障演练
  • 性能压测
  • 优化调优

总周期:18个月

9.3 多活的组织保障

需要专门的多活团队

  • SRE工程师
  • 数据库DBA
  • 网络工程师
  • 业务开发代表

需要高层支持

  • 多活是长期项目
  • 投入大、周期长
  • 需要持续推动

十、总结

多活不是简单的技术升级,是系统性的架构革命

关键要点

  1. 多活 ≠ 多机房:真正的多活是流量分配 + 数据同步 + 业务适配
  2. 单元化是多活的关键:通过用户ID绑定机房,避免数据冲突
  3. 数据同步是难点:同步策略、同步延迟、数据一致性
  4. 流量调度是核心:GSLB、灰度切换、健康检查
  5. 运维复杂:监控、告警、演练、故障恢复
  6. 成本翻倍:资源、研发、运维成本都大幅增加

多活的哲学

多活不是为了让系统永不故障,而是让系统在故障时还能服务大部分用户。

核心原则

  • 单元化是基础:用户绑定机房
  • 数据是难点:同步策略和一致性
  • 流量调度是核心:GSLB和灰度
  • 演练是保障:不演练等于没做

最后的话

多活不是"想不想做"的问题,而是"业务需不需要"的问题

如果你的业务有以下特征,多活是必须的

  • 7×24小时服务
  • 单机房故障影响巨大
  • 用户分布广
  • 业务连续性要求高

否则,可能灾备就够了

多活是高成本的"奢侈品"——但对于核心业务,这奢侈品值得拥有


今日思考
你们的业务需要多活吗?机房级故障的影响有多大?欢迎分享你的多活经验或思考!


作者:架构实战团队
日期:2026-07-23
标签:#多活架构 #异地多活 #单元化 #容灾 #高可用 #GSLB

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询