【架构实战】异地多活架构:跨地域高可用设计
一ã€ä¸€æ¬¡å
‰ç¼†æŒ–æ–让我们æ–了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
- 延迟优化:读写分离 + 本地缓å˜
最佳实践:
- å
ˆåšåŒåŸŽåŒæ´»ï¼Œå†æ‰©å¼‚地 - æ ¸å¿ƒä¸šåŠ¡ä¼˜å
ˆå¤šæ´» - 定期åšå®¹ç¾æ¼”练
- ä¿æŒé
置一致性
血的教è®ï¼š
å¤šæ´»æž¶æž„ä¸æ˜¯é“¶å¼¹ï¼Œå¤æ‚度很高。在决定åšå¤šæ´»ä¹‹å‰ï¼Œå
ˆè¯„估业务é‡è¦æ€§å’Œä½ æ„¿æ„ä»˜å‡ºçš„æˆæœ¬ã€‚
æ€è€ƒé¢˜ï¼šä½ 的系统有没有åšå¤šæ´»ï¼Ÿé‡åˆ°äº†å“ªäº›æŒ‘战?
个人观点,ä»
ä¾›å‚考
