一、那场"机房断电"事故
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 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;/** * 查询用户的所有订单(用户的所有订单都在本单元) */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
- 网络工程师
- 业务开发代表
需要高层支持:
- 多活是长期项目
- 投入大、周期长
- 需要持续推动
十、总结
多活不是简单的技术升级,是系统性的架构革命。
关键要点:
- 多活 ≠ 多机房:真正的多活是流量分配 + 数据同步 + 业务适配
- 单元化是多活的关键:通过用户ID绑定机房,避免数据冲突
- 数据同步是难点:同步策略、同步延迟、数据一致性
- 流量调度是核心:GSLB、灰度切换、健康检查
- 运维复杂:监控、告警、演练、故障恢复
- 成本翻倍:资源、研发、运维成本都大幅增加
多活的哲学:
多活不是为了让系统永不故障,而是让系统在故障时还能服务大部分用户。
核心原则:
- 单元化是基础:用户绑定机房
- 数据是难点:同步策略和一致性
- 流量调度是核心:GSLB和灰度
- 演练是保障:不演练等于没做
最后的话:
多活不是"想不想做"的问题,而是"业务需不需要"的问题。
如果你的业务有以下特征,多活是必须的:
- 7×24小时服务
- 单机房故障影响巨大
- 用户分布广
- 业务连续性要求高
否则,可能灾备就够了。
多活是高成本的"奢侈品"——但对于核心业务,这奢侈品值得拥有。
今日思考:
你们的业务需要多活吗?机房级故障的影响有多大?欢迎分享你的多活经验或思考!
作者:架构实战团队
日期:2026-07-23
标签:#多活架构 #异地多活 #单元化 #容灾 #高可用 #GSLB