当前位置: 首页 > news >正文

【架构实战】异地多活架构:跨地域高可用设计

一、一次å

‰ç¼†æŒ–断让我们断了3小时

2018年,我们在杭州有两个机房,有一天施工队挖断了连通两个机房的å
‰ç¼†ã€‚

那一瞬间,所有跨机房的服务调用å
¨éƒ¨å¤±è´¥ï¼Œè®¢å•系统、支付系统å
¨éƒ¨å´©æºƒã€‚

后来我们花了3个小时才恢复,但用户流失率直接飙升至30%。

从那以后,我们开始认真考虑异地多活架构,不再把鸡蛋放在一个篮子里。


二、异地多活架构概述

2.1 多活形态

多活形态对比: 1. 主备(Active-Standby) - 主机房处理所有流量 - å¤‡æœºæˆ¿ä» åšå¤‡ä»½ - 切换时需要时间恢复 2. 同城双活(Active-Active) - 两个机房同时处理流量 - 延迟<1msï¼Œç”¨æˆ·æ— æ„ŸçŸ¥ - 成本较高 3. 异地多活(Multi-Region) - 多个地域同时处理流量 - 延迟3-10ms,需要优化 - 成本最高,可用性最高 4. å ¨çƒå¤šæ´» - è¦†ç›–å ¨çƒç”¨æˆ· - 智能DNS就近访问 - 数据同步挑战大

2.2 架构设计原则

┌─────────────────────────────────────────────────────────────────┐ │ 异地多活设计原则 │ │ │ │ 1. å•å ƒåŒ– │ │ - ä¸šåŠ¡æŒ‰å•å ƒåˆ’åˆ† │ │ - å•å ƒå† é—­çŽ¯ï¼Œå‡å°‘è·¨å•å ƒä¾èµ– │ │ │ │ 2. 数据分区 │ │ - 按用户ID或地区划分数据 │ │ - æ¯ä¸ªå•å ƒè´Ÿè´£è‡ªå·±çš„æ•°æ® │ │ │ │ 3. åŒåŸŽä¼˜å ˆ │ │ - ä¼˜å ˆè®¿é—®åŒåŸŽå•å ƒ │ │ - 减少跨地域延迟 │ │ │ │ 4. 最终一致性 │ │ - å è®¸çŸ­æš‚æ•°æ®ä¸ä¸€è‡´ │ │ - 通过异步同步修复 │ │ │ │ 5. æ• éšœè‡ªæ„ˆ │ │ - è‡ªåŠ¨æ£€æµ‹æ• éšœ │ │ - 自动切换流量 │ │ │ └──────────────────────────────────────────────────────────────────┘

三、数据同步方案

3.1 MySQL主从同步

/** * 跨机房数据同步服务 */@Service@Slf4jpublicclassDataSyncService{/** * 实时同步(Canal) */publicvoidsyncWithCanal(){// Canaléç½®CanalConnectorconnector=CanalConnectors.newSingleConnector(newInetSocketAddress("127.0.0.1",11111),"example","","");connector.connect();connector.subscribe(".*\\..*");connectorrollback();while(running){Messagemessage=connector.get(100);for(CanalEntry.Entryentry:message.getEntries()){if(entry.getEntryType()==CanalEntry.EntryType.ROWDATA){// 处理变更数据CanalEntry.RowChangerowChange=CanalEntry.RowChange.parseFrom(entry.getStoreValue());for(CanalEntry.RowDatarowData:rowChange.getRowDatasList()){handleChange(entry.getHeader(),rowData);}}}}}/** * 处理变更数据 */privatevoidhandleChange(CanalEntry.Headerheader,CanalEntry.RowDatarowData){StringtableName=header.getTableName();StringeventType=header.getEventType().name();log.info("数据变更: table={}, type={}",tableName,eventType);switch(tableName){case"orders":syncOrder(rowData,eventType);break;case"products":syncProduct(rowData,eventType);break;// ... å¶ä»–表}}/** * åŒæ­¥åˆ°å ¶ä»–æœºæˆ¿ */privatevoidsyncToRemote(Stringtable,Map<String,Object>data,Stringtype){// 发送到消息队列messageProducer.send("data-sync",SyncMessage.builder().table(table).type(type).data(data).timestamp(System.currentTimeMillis()).source机房("hz-primary").build());}}

3.2 Redis同步

# Redis Cluster跨机房éç½®# 方案1:主从复制cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 15000# 机房Areplicaof 10.0.1.1 6379# 机房Breplicaof 10.0.2.1 6379# 方案2:双写# 应用层同时写å¥ä¸¤ä¸ªæœºæˆ¿
/** * Redis双写服务 */@Service@Slf4jpublicclassRedisDualWriteService{@AutowiredprivateRedisTemplate<String,Object>redisTemplateA;@AutowiredprivateRedisTemplate<String,Object>redisTemplateB;/** * 双写 */publicvoiddualWrite(Stringkey,Objectvalue){List<Future<Boolean>>futures=newArrayList<>();// å¼‚æ­¥å†™å¥æœºæˆ¿Afutures.add(asyncExecute(redisTemplateA,()->{redisTemplateA.opsForValue().set(key,value);returntrue;}));// å¼‚æ­¥å†™å¥æœºæˆ¿Bfutures.add(asyncExecute(redisTemplateB,()->{redisTemplateB.opsForValue().set(key,value);returntrue;}));// ç­‰å¾ç»“æžœfor(Future<Boolean>future:futures){try{if(!future.get(2,TimeUnit.SECONDS)){log.error("双写失败");}}catch(Exceptione){log.error("双写异常",e);}}}/** * 读取时降级 */publicObjectread(Stringkey){try{// 优åˆè¯»æœ¬åœ°Objectresult=redisTemplateA.opsForValue().get(key);if(result!=null){returnresult;}}catch(Exceptione){log.warn("读机房A失败,尝试机房B",e);}try{// 降级读机房BreturnredisTemplateB.opsForValue().get(key);}catch(Exceptione){log.error("读机房B也失败",e);returnnull;}}}

3.3 消息队列同步

/** * 消息跨机房同步 */@Service@Slf4jpublicclassMQSyncService{@AutowiredprivateRocketMQTemplaterocketMQTemplate;/** * 发送跨机房消息 */publicvoidsendCrossRegion(Stringtopic,Objectmessage){// 发送到所有机房List<String>regions=Arrays.asList("hz","sh","bj");for(Stringregion:regions){try{rocketMQTemplate.asyncSend(topic+"_"+region,message,newSendCallback(){@OverridepublicvoidonSuccess(SendResultsendResult){log.info("发送成功: region={}, msgId={}",region,sendResult.getMsgId());}@OverridepublicvoidonException(Throwablee){log.error("发送失败: region={}",region,e);}});}catch(Exceptione){log.error("发送异常: region={}",region,e);}}}}

四、流量切换方案

4.1 DNS切换

/** * DNS切换服务 */@Service@Slf4jpublicclassDnsSwitchService{/** * 切换流量到备用机房 */publicvoidswitchTraffic(Stringregion){// 更新DNS解析UpdateDomainRequestrequest=newUpdateDomainRequest();request.setDomainName("api.example.com");request.setAction("UPDATE_DNS_LOAD_BALANCE");request.setRegion(region);alidnsClient.updateDomainRecord(request);// 刷新本地DNS缓存refreshLocalDns();log.info("流量切换到机房: {}",region);// 发送通知alertingService.alert("流量切换","API流量切换到"+region+"机房");}/** * 健康检查 */publicbooleanhealthCheck(Stringregion){Stringurl="http://"+region+"-api.example.com/health";try{ResponseEntity<String>response=restTemplate.getForEntity(url,String.class);returnresponse.getStatusCode()==HttpStatus.OK;}catch(Exceptione){log.error("健康检查失败: region={}",region,e);returnfalse;}}/** * 自动切换 */@Scheduled(fixedRate=5000)publicvoidautoSwitch(){StringprimaryRegion="hz";// 检查主机房健康状态if(!healthCheck(primaryRegion)){log.warn("主机房不可用,准备切换");// 切换到备用机房for(StringbackupRegion:Arrays.asList("sh","bj")){if(healthCheck(backupRegion)){switchTraffic(backupRegion);break;}}}}}

4.2 Nginx切换

# Nginx主动健康检查 upstream backend { server 10.0.1.1:8080 max_fails=3 fail_timeout=30s; server 10.0.1.2:8080 max_fails=3 fail_timeout=30s; server 10.0.2.1:8080 backup; # 杭州机房2 server 10.0.3.1:8080 backup; # 上海机房 } server { location / { proxy_pass http://backend; proxy_next_upstream error timeout http_502; proxy_connect_timeout 5s; proxy_send_timeout 30s; proxy_read_timeout 30s; } }
/** * Nginxé ç½®çƒ­æ›´æ–° */@Service@Slf4jpublicclassNginxReloadService{/** * åŠ¨æ€æ·»åŠ ä¸Šæ¸¸æœåŠ¡å™¨ */publicvoidaddUpstreamServer(Stringip,intport){Stringconfig=String.format("upstream backend {\n"+" server %s:%d max_fails=3 fail_timeout=30s;\n"+"}\n",ip,port);// 写å¥ä¸´æ—¶é ç½®writeToFile("/tmp/nginx-upstream.conf",config);// 执行reloadexecCommand("nginx -s reload");log.info("æ·»åŠ ä¸Šæ¸¸æœåŠ¡å™¨: {}:{}",ip,port);}/** * 切换主备 */publicvoidswitchMasterBackup(Stringregion){// æ›´æ–°nginxéç½®ï¼Œåˆ‡æ¢åˆ°å¤‡ç”¨æœºæˆ¿StringupstreamConfig=buildUpstreamConfig(region);writeToFile("/etc/nginx/conf.d/backend.conf",upstreamConfig);// reloadexecCommand("nginx -s reload");log.info("切换主备: region={}",region);}}

五、跨地域延迟优化

5.1 读写分离

/** * 跨机房读写分离 */@Service@Slf4jpublicclassCrossRegionReadWriteService{/** * 写请求 - 路由到主机房 */@WriteConnectionpublicvoidwrite(Orderorder){// 强制写主库orderMapper.insert(order);// 异步同步到从库asyncSyncToSlave(order);}/** * 读请求 - ä¼˜å ˆæœ¬åœ°æœºæˆ¿ */@ReadConnection(region="local")publicOrderread(LongorderId){// åˆè¯»æœ¬åœ°ä»Žåº“Orderorder=orderMapper.selectById(orderId);if(order==null){// 本地没有,跨机房读取order=remoteRead(orderId);}returnorder;}/** * è¯»å†™åˆ†ç¦»é ç½® */@BeanpublicDataSourcedataSource(){HikariDataSourceprimary=createDataSource("jdbc:mysql://10.0.1.1:3306","primary");HikariDataSourcereplica1=createDataSource("jdbc:mysql://10.0.1.2:3306","replica1");HikariDataSourcereplica2=createDataSource("jdbc:mysql://10.0.2.1:3306","replica2");returnnewReplicationDataSource(primary,Arrays.asList(replica1,replica2));}}

5.2 本地缓存

/** * 多级缓存 */@Service@Slf4jpublicclassLocalCacheService{@AutowiredprivateCaffeineCacheManagercacheManager;/** * æœ¬åœ°ç¼“å­˜é ç½® */@PostConstructpublicvoidinit(){cacheManager.setCaffeine(Caffeine.newBuilder().maximumSize(10000).expireAfterWrite(1,TimeUnit.MINUTES).recordStats());}/** * è¯»å–ï¼ˆæœ¬åœ°ç¼“å­˜ä¼˜å ˆï¼‰ */publicObjectget(Stringkey){// 1. åˆè¯»æœ¬åœ°ç¼“å­˜Objectvalue=localCache.getIfPresent(key);if(value!=null){returnvalue;}// 2. 读Redis(本地机房)value=redisTemplate.opsForValue().get(key);if(value!=null){// 写奿œ¬åœ°ç¼“å­˜ localCache.put(key,value);returnvalue;}// 3. 读数据库value=loadFromDb(key);if(value!=null){localCache.put(key,value);redisTemplate.opsForValue().set(key,value);}returnvalue;}}

å

­ã€å®¹ç¾åˆ‡æ¢æ¼”练

6.1 演练方案

/** * 容灾演练服务 */@Service@Slf4lpublicclassDrDrillService{/** * 执行容灾演练 */publicvoidexecuteDrDrill(DrDrillPlanplan){log.info("开始容灾演练: {}",plan.getName());try{// 1. 通知相å³å›¢é˜ŸnotifyTeams(plan);// 2. 记录基线状态RecordBaselineStatus();// 3. 模拟æ•éšœsimulateFailure(plan.getFailureType());// 4. ç­‰å¾åˆ‡æ¢waitForSwitch(plan.getTimeout());// 5. 验证业务verifyBusiness(plan.getCheckPoints());// 6. 恢复recover();// 7. 记录结果saveDrillResult(plan);}catch(Exceptione){log.error("容灾演练失败",e);emergencyRecover();}log.info("容灾演练完成: {}",plan.getName());}/** * 验证业务可用性 */privatevoidverifyBusiness(List<CheckPoint>checkPoints){for(CheckPointcheckPoint:checkPoints){try{Objectresult=executeCheck(checkPoint);if(!evaluate(checkPoint,result)){thrownewDrillException("检查点验证失败: "+checkPoint.getName());}}catch(Exceptione){log.error("检查点异常: {}",checkPoint.getName(),e);throwe;}}}}

七、踩坑实录

坑1:数据同步延迟

跨机房数据同步延迟太大,导致用户看到的数据不一致。

解决:优化同步链路,缩短同步时间窗口,å¿
要时强制读主库。

坑2:切换失败

æ•
障时DNS切换失败,流量切不过去。

解决:多通道切换(DNS + Nginx + 消息),提高切换成功率。

坑3:脑裂问题

两个机房都认为对方æ•
障,同时写数据,导致数据冲突。

解决:使用分布式锁或 Paxos/Raft 保证一致性。

坑4:成本失控

异地多活需要额外的机器和网络,成本太高。

è§£å†³ï¼šæŒ‰ä¸šåŠ¡é‡è¦æ€§åˆ†çº§ï¼Œæ ¸å¿ƒä¸šåŠ¡å¤šæ´»ï¼Œæ™®é€šä¸šåŠ¡å•æœºæˆ¿ã€‚

坑5:运维困难

跨机房运维复杂,容易操作失误。

解决:自动化运维工å
·ï¼Œæ ‡å‡†åŒ–操作流程。


å

«ã€æ€»ç»“

异地多活是保障业务高可用的终极方案:

  • 单å
    ƒåŒ–
    :减少跨单å
    ƒä¾èµ–
  • 数据同步:Canal + MQ + Redis
  • 流量切换:DNS + Nginx
  • 延迟优化:读写分离 + 本地缓存

最佳实践:

  1. å
    ˆåšåŒåŸŽåŒæ´»ï¼Œå†æ‰©å¼‚地
  2. æ ¸å¿ƒä¸šåŠ¡ä¼˜å
    ˆå¤šæ´»
  3. 定期做容灾演练
  4. 保持é
    ç½®ä¸€è‡´æ€§

血的教训:

多活架构不是银弹,复杂度很高。在决定做多活之前,å
ˆè¯„ä¼°ä¸šåŠ¡é‡è¦æ€§å’Œä½ æ„¿æ„ä»˜å‡ºçš„æˆæœ¬ã€‚

æ€è€ƒé¢˜ï¼šä½ çš„ç³»ç»Ÿæœ‰æ²¡æœ‰åšå¤šæ´»ï¼Ÿé‡åˆ°äº†å“ªäº›æŒ‘æˆ˜ï¼Ÿ


个人观点,ä»
供参考

http://www.jsqmd.com/news/929079/

相关文章:

  • 我用一台旧电脑跑了个 AI 模型,发现比云 API 还香(附一键部署命令)
  • 基于Arduino与Processing的RFID交互式视频播放系统实战指南
  • Windows系统深度优化架构:AtlasOS实现原理与配置机制解析
  • 如何快速修复机械键盘连击问题:免费开源防粘连工具完整指南
  • 555定时器驱动PCB艺术徽章:从经典电路到像素化耿鬼设计
  • 从零打造8x8x8 LED光立方:硬件搭建、驱动原理与Arduino编程全解析
  • 基于Arduino与TCS230的颜色识别系统:从传感器原理到实践应用
  • AI检测太高论文过不了?这4个降AI率平台2026年别再错过!
  • 如何用WeChatMsg打造你的专属数字记忆库:从数据留痕到情感永存
  • 基于Pinoo与Mblock3的倾斜传感器猜色游戏:事件驱动编程入门实践
  • 别再只盯着模型了!搞懂Unity Mesh的这3个渲染模式,性能优化和调试效率翻倍
  • 用74LS138和74LS00玩点花的:手把手教你设计一个简易的‘多数表决器’电路
  • HY-Embodied-0.5-X的长时规划能力:从任务分解到失败反思的完整循环
  • 显卡驱动清理神器:DDU深度使用终极指南
  • 树莓派四人抢答游戏机:从GPIO控制到Pygame交互的嵌入式开发实践
  • Kotlin 协程设计思想(一):CoroutineContext 到底是什么?为什么 Job 和 Dispatcher 可以直接相加?
  • 鸣潮自动化助手完整指南:如何用ok-ww解放双手,轻松完成日常任务
  • 从零制作哈利波特魔杖灯:DIY电子入门与创意电路实践
  • FinTech架构深度解析:从数据、算法到风控中台实战
  • 别死磕Ubuntu18.04了!拯救者Y9000P装双系统,直接上Ubuntu 22.04 LTS的保姆级教程(附驱动验证清单)
  • 别再死记硬背公式了!用Python手把手实现吴恩达浅层神经网络(附完整代码)
  • 南海区26年最新奢侈品名包名表专业回收权威店铺推荐 - 莘州文化
  • Arduino避障机器人:从硬件选型到代码实现的完整实践指南
  • 基于Transformer与GPT-2的惠特曼风格诗歌生成器实践
  • Veo 2分辨率配置深度解析(行业首发12K超采样白皮书):NVIDIA/AMD/Apple芯片专属优化矩阵
  • 别再死记硬背公式了!用NumPy手写一个神经元,彻底搞懂矩阵运算与并行加速
  • Django搭建的轻量级物业后台系统,含业主管理、报修工单与费用记录功能
  • 集成toxic-comment-model到现有系统:Python API调用与微调实战
  • 【Redis从入门到精通】第23篇:ZSet对象——ziplist和skiplist的完美组合
  • 从零设计电子徽章:EasyEDA实战与PCB制作全流程