【架构实战】多活架构:异地多活与单元化部署 一、那场机房断电事故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 问题双向同步数据冲突可能 【单元化】 用户Aunit1 ── 机房1 ── 写入 ── 数据留在机房1 用户Bunit2 ── 机房2 ── 写入 ── 数据留在机房2 用户Cunit3 ── 机房3 ── 写入 ── 数据留在机房3 特点 - 用户绑定机房 - 数据不跨机房流动 - 机房故障只影响本单元用户单元化的核心用户ID决定机房归属同一用户的数据只在一个机房跨单元数据通过同步或消息3.2 单元化 vs 普通多活维度普通多活单元化数据写入任意机房归属机房数据同步双向同步单向同步冲突解决复杂无冲突扩容受限容易增加单元成本高更高复杂度高极高3.3 单元划分策略按用户ID分片/** * 单元划分算法 */publicclassUnitRouter{// 假设我们有10个单元privatestaticfinalintUNIT_COUNT10;/** * 根据userId计算单元 */publicintgetUnitId(StringuserId){// 取userId的hash模10inthashMath.abs(userId.hashCode());returnhash%UNIT_COUNT;}/** * 根据unitId获取机房 */publicStringgetDataCenter(intunitId){// 单元0-3 → 华东机房// 单元4-6 → 华南机房// 单元7-9 → 华北机房if(unitId3)returndc-east;if(unitId6)returndc-south;returndc-north;}}按业务分片/** * 业务单元划分 */publicclassBusinessUnitRouter{publicStringgetDataCenter(StringbusinessType){switch(businessType){caseALIPAY:returndc-east;// 支付宝业务在华东caseWECHAT_PAY:returndc-south;// 微信支付在华南caseUNION_PAY:returndc-north;// 银联在华北default:returndc-east;}}}四、多活架构设计4.1 总体架构【异地多活 单元化 架构】 用户 │ ↓ ┌──────────────┐ │ DNS/GSLB │ ← 全局负载均衡 │ (流量调度) │ └──────┬───────┘ │ ┌────────────┼────────────┐ ↓ ↓ ↓ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 华东机房 │ │ 华南机房 │ │ 华北机房 │ │ (主) │ │ (备1) │ │ (备2) │ │ 单元0-3 │ │ 单元4-6 │ │ 单元7-9 │ │ │ │ │ │ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │网关 │ │ │ │网关 │ │ │ │网关 │ │ ← 单元化路由 │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │业务 │ │ │ │业务 │ │ │ │业务 │ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │数据 │ │ │ │数据 │ │ │ │数据 │ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ └─────┬────┘ └─────┬────┘ └─────┬────┘ │ │ │ └────────────┴────────────┘ ↑ 数据同步层 (单向同步 消息队列)4.2 流量调度GSLB全局负载均衡/** * GSLB流量调度 */ComponentpublicclassGSLBRouter{AutowiredprivateHealthCheckerhealthChecker;/** * 根据用户ID选择机房 */publicStringroute(StringuserId,Stringurl){// 1. 单元化路由同一用户始终到同一机房intunitIdgetUnitId(userId);StringtargetDCgetDataCenter(unitId);// 2. 健康检查目标机房不可用路由到备机房if(!healthChecker.isHealthy(targetDC)){targetDCgetBackupDataCenter(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. 解析BinlogStringtableNameentry.getHeader().getTableName();RowChangerowChangeentry.getRowChange();for(RowDatarowData:rowChange.getRowDatasList()){// 2. 检查是否需要同步按单元ID过滤StringuserIdgetUserIdFromRow(rowData);intunitIdgetUnitId(userId);intsourceUnitgetCurrentUnit();if(unitIdsourceUnit){// 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位) */privatefinallongtwepoch1288834974657L;privatefinallongdatacenterIdBits5L;privatefinallongworkerIdBits5L;privatefinallongsequenceBits12L;privatelongdatacenterId;// 数据中心IDprivatelongworkerId;// 机器IDprivatelongsequence0L;publicsynchronizedlongnextId(){longtimestamptimeGen();if(timestamplastTimestamp){thrownewRuntimeException(时钟回拨);}if(timestamplastTimestamp){sequence(sequence1)((1sequenceBits)-1);if(sequence0){timestamptilNextMillis(lastTimestamp);}}else{sequence0L;}lastTimestamptimestamp;return((timestamp-twepoch)(datacenterIdBitsworkerIdBitssequenceBits))|(datacenterId(workerIdBitssequenceBits))|(workerIdsequenceBits)|sequence;}}关键点ID中嵌入机房标识即使各机房独立生成全局也不会冲突可以从ID中反解机房五、单元化部署实践5.1 单元化路由/** * 单元化网关路由请求到正确的机房 */RestControllerpublicclassUnitizedGateway{AutowiredprivateUnitRouterunitRouter;RequestMapping(/api/{service}/**)publicResponseEntity?route(PathVariableStringservice,RequestHeader(User-Id)StringuserId,HttpServletRequestrequest){// 1. 计算用户所属单元intunitIdunitRouter.getUnitId(userId);StringtargetDCunitRouter.getDataCenter(unitId);// 2. 转发到目标机房StringtargetUrlbuildTargetUrl(targetDC,service,request);// 3. 转发HTTP请求returnforward(targetUrl,request);}}5.2 单元化数据库每个单元有独立的数据库【数据库分片】 数据库0dc-eastunit 0, 1 数据库1dc-eastunit 2, 3 数据库2dc-southunit 4, 5 数据库3dc-southunit 6 数据库4dc-northunit 7, 8 数据库5dc-northunit 9ShardingSphere配置# 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;/** * 查询用户的所有订单用户的所有订单都在本单元 */publicListOrdergetUserOrders(StringuserId){// 同单元查询returnorderRepository.findByUserId(userId);}/** * 查询商品的所有订单跨单元 */publicListOrdergetProductOrders(StringproductId){// 跨单元查询ListOrderallOrdersnewArrayList();for(intunitId0;unitId10;unitId){StringdcunitRouter.getDataCenter(unitId);ListOrderordersqueryUnitOrders(dc,productId);allOrders.addAll(orders);}returnallOrders;}}优化数据冗余把商品信息冗余到各单元搜索引擎用ES做全局索引数据仓库T1同步到数仓六、多活运维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{privatevolatileintgrayPercentage0;// 灰度比例publicStringroute(StringuserId){intunitIdunitRouter.getUnitId(userId);StringprimaryDCunitRouter.getDataCenter(unitId);StringbackupDCgetBackupDataCenter(primaryDC);// 灰度用户走备机房if(isGrayUser(userId)grayPercentage0){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网络工程师业务开发代表需要高层支持多活是长期项目投入大、周期长需要持续推动十、总结多活不是简单的技术升级是系统性的架构革命。关键要点多活 ≠ 多机房真正的多活是流量分配 数据同步 业务适配单元化是多活的关键通过用户ID绑定机房避免数据冲突数据同步是难点同步策略、同步延迟、数据一致性流量调度是核心GSLB、灰度切换、健康检查运维复杂监控、告警、演练、故障恢复成本翻倍资源、研发、运维成本都大幅增加多活的哲学多活不是为了让系统永不故障而是让系统在故障时还能服务大部分用户。核心原则单元化是基础用户绑定机房数据是难点同步策略和一致性流量调度是核心GSLB和灰度演练是保障不演练等于没做最后的话多活不是想不想做的问题而是业务需不需要的问题。如果你的业务有以下特征多活是必须的7×24小时服务单机房故障影响巨大用户分布广业务连续性要求高否则可能灾备就够了。多活是高成本的奢侈品——但对于核心业务这奢侈品值得拥有。今日思考你们的业务需要多活吗机房级故障的影响有多大欢迎分享你的多活经验或思考作者架构实战团队日期2026-07-23标签#多活架构 #异地多活 #单元化 #容灾 #高可用 #GSLB