【架构实战】异地多活架构:跨地域高可用设计
一ã€ä¸€æ¬¡å‰ç¼†æŒ–æ–让我们æ–了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ä¸»ä»ŽåŒæ¥/** * 跨机房数æ®åŒæ¥æœåŠ¡ */ServiceSlf4jpublicclassDataSyncService{/** * å®žæ—¶åŒæ¥ï¼ˆCanal) */publicvoidsyncWithCanal(){// Canaléç½®CanalConnectorconnectorCanalConnectors.newSingleConnector(newInetSocketAddress(127.0.0.1,11111),example,,);connector.connect();connector.subscribe(.*\\..*);connectorrollback();while(running){Messagemessageconnector.get(100);for(CanalEntry.Entryentry:message.getEntries()){if(entry.getEntryType()CanalEntry.EntryType.ROWDATA){// 处ç†å˜æ›´æ•°æ®CanalEntry.RowChangerowChangeCanalEntry.RowChange.parseFrom(entry.getStoreValue());for(CanalEntry.RowDatarowData:rowChange.getRowDatasList()){handleChange(entry.getHeader(),rowData);}}}}}/** * 处ç†å˜æ›´æ•°æ® */privatevoidhandleChange(CanalEntry.Headerheader,CanalEntry.RowDatarowData){StringtableNameheader.getTableName();StringeventTypeheader.getEventType().name();log.info(æ•°æ®å˜æ›´: table{}, type{},tableName,eventType);switch(tableName){caseorders:syncOrder(rowData,eventType);break;caseproducts:syncProduct(rowData,eventType);break;// ... å¶ä»–表}}/** * åŒæ¥åˆ°å ¶ä»–机房 */privatevoidsyncToRemote(Stringtable,MapString,Objectdata,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åŒå†™æœåŠ¡ */ServiceSlf4jpublicclassRedisDualWriteService{AutowiredprivateRedisTemplateString,ObjectredisTemplateA;AutowiredprivateRedisTemplateString,ObjectredisTemplateB;/** * åŒå†™ */publicvoiddualWrite(Stringkey,Objectvalue){ListFutureBooleanfuturesnewArrayList();// 异æ¥å†™å¥æœºæˆ¿Afutures.add(asyncExecute(redisTemplateA,()-{redisTemplateA.opsForValue().set(key,value);returntrue;}));// 异æ¥å†™å¥æœºæˆ¿Bfutures.add(asyncExecute(redisTemplateB,()-{redisTemplateB.opsForValue().set(key,value);returntrue;}));// ç‰å¾ç»“æžœfor(FutureBooleanfuture:futures){try{if(!future.get(2,TimeUnit.SECONDS)){log.error(åŒå†™å¤±è´¥);}}catch(Exceptione){log.error(åŒå†™å¼‚常,e);}}}/** * è¯»å–æ—¶é™çº§ */publicObjectread(Stringkey){try{// 优åˆè¯»æœ¬åœ°ObjectresultredisTemplateA.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 消æ¯é˜Ÿåˆ—åŒæ¥/** * 消æ¯è·¨æœºæˆ¿åŒæ¥ */ServiceSlf4jpublicclassMQSyncService{AutowiredprivateRocketMQTemplaterocketMQTemplate;/** * å‘é€è·¨æœºæˆ¿æ¶ˆæ¯ */publicvoidsendCrossRegion(Stringtopic,Objectmessage){// å‘é€åˆ°æ‰€æœ‰æœºæˆ¿ListStringregionsArrays.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åˆ‡æ¢æœåŠ¡ */ServiceSlf4jpublicclassDnsSwitchService{/** * åˆ‡æ¢æµé‡åˆ°å¤‡ç”¨æœºæˆ¿ */publicvoidswitchTraffic(Stringregion){// æ›´æ–°DNSè§£æžUpdateDomainRequestrequestnewUpdateDomainRequest();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){Stringurlhttp://region-api.example.com/health;try{ResponseEntityStringresponserestTemplate.getForEntity(url,String.class);returnresponse.getStatusCode()HttpStatus.OK;}catch(Exceptione){log.error(å¥åº·æ£€æŸ¥å¤±è´¥: region{},region,e);returnfalse;}}/** * è‡ªåŠ¨åˆ‡æ¢ */Scheduled(fixedRate5000)publicvoidautoSwitch(){StringprimaryRegionhz;// 检查主机房å¥åº·çжæ€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_fails3 fail_timeout30s; server 10.0.1.2:8080 max_fails3 fail_timeout30s; 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é ç½®çƒæ›´æ–° */ServiceSlf4jpublicclassNginxReloadService{/** * åŠ¨æ€æ·»åŠ ä¸Šæ¸¸æœåС噍 */publicvoidaddUpstreamServer(Stringip,intport){StringconfigString.format(upstream backend {\n server %s:%d max_fails3 fail_timeout30s;\n}\n,ip,port);// 写å¥ä¸´æ—¶é ç½®writeToFile(/tmp/nginx-upstream.conf,config);// 执行reloadexecCommand(nginx -s reload);log.info(æ·»åŠ ä¸Šæ¸¸æœåС噍: {}:{},ip,port);}/** * 切æ¢ä¸»å¤‡ */publicvoidswitchMasterBackup(Stringregion){// æ›´æ–°nginxé置,切æ¢åˆ°å¤‡ç”¨æœºæˆ¿StringupstreamConfigbuildUpstreamConfig(region);writeToFile(/etc/nginx/conf.d/backend.conf,upstreamConfig);// reloadexecCommand(nginx -s reload);log.info(切æ¢ä¸»å¤‡: region{},region);}}五ã€è·¨åœ°åŸŸå»¶è¿Ÿä¼˜åŒ–5.1 读写分离/** * 跨机房读写分离 */ServiceSlf4jpublicclassCrossRegionReadWriteService{/** * 写请求 - 路由到主机房 */WriteConnectionpublicvoidwrite(Orderorder){// 强制写主库orderMapper.insert(order);// 异æ¥åŒæ¥åˆ°ä»Žåº“asyncSyncToSlave(order);}/** * 读请求 - ä¼˜å ˆæœ¬åœ°æœºæˆ¿ */ReadConnection(regionlocal)publicOrderread(LongorderId){// åˆè¯»æœ¬åœ°ä»Žåº“OrderorderorderMapper.selectById(orderId);if(ordernull){// 本地没有,跨机房读å–orderremoteRead(orderId);}returnorder;}/** * 读写分离é ç½® */BeanpublicDataSourcedataSource(){HikariDataSourceprimarycreateDataSource(jdbc:mysql://10.0.1.1:3306,primary);HikariDataSourcereplica1createDataSource(jdbc:mysql://10.0.1.2:3306,replica1);HikariDataSourcereplica2createDataSource(jdbc:mysql://10.0.2.1:3306,replica2);returnnewReplicationDataSource(primary,Arrays.asList(replica1,replica2));}}5.2 本地缓å˜/** * å¤šçº§ç¼“å˜ */ServiceSlf4jpublicclassLocalCacheService{AutowiredprivateCaffeineCacheManagercacheManager;/** * 本地缓å˜é ç½® */PostConstructpublicvoidinit(){cacheManager.setCaffeine(Caffeine.newBuilder().maximumSize(10000).expireAfterWrite(1,TimeUnit.MINUTES).recordStats());}/** * 读å–(本地缓å˜ä¼˜å ˆï¼‰ */publicObjectget(Stringkey){// 1. åˆè¯»æœ¬åœ°ç¼“å˜ObjectvaluelocalCache.getIfPresent(key);if(value!null){returnvalue;}// 2. 读Redis(本地机房)valueredisTemplate.opsForValue().get(key);if(value!null){// 写奿œ¬åœ°ç¼“å˜ localCache.put(key,value);returnvalue;}// 3. 读数æ®åº“valueloadFromDb(key);if(value!null){localCache.put(key,value);redisTemplate.opsForValue().set(key,value);}returnvalue;}}åã€å®¹ç¾åˆ‡æ¢æ¼”练6.1 演练方案/** * å®¹ç¾æ¼”练æœåŠ¡ */ServiceSlf4lpublicclassDrDrillService{/** * æ‰§è¡Œå®¹ç¾æ¼”练 */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(ListCheckPointcheckPoints){for(CheckPointcheckPoint:checkPoints){try{ObjectresultexecuteCheck(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延迟优化:读写分离 æœ¬åœ°ç¼“å˜æœ€ä½³å®žè·µï¼šåˆåšåŒåŸŽåŒæ´»ï¼Œå†æ‰©å¼‚åœ°æ ¸å¿ƒä¸šåŠ¡ä¼˜åˆå¤šæ´»å®šæœŸåšå®¹ç¾æ¼”ç»ƒä¿æŒé置一致性血的教è®ï¼šå¤šæ´»æž¶æž„䏿˜¯é“¶å¼¹ï¼Œå¤æ‚度很高。在决定åšå¤šæ´»ä¹‹å‰ï¼Œåˆè¯„估业务é‡è¦æ€§å’Œä½ æ„¿æ„ä»˜å‡ºçš„æˆæœ¬ã€‚æ€è€ƒé¢˜ï¼šä½ 的系统有没有åšå¤šæ´»ï¼Ÿé‡åˆ°äº†å“ªäº›æŒ‘战?个人观点,ä»ä¾›å‚考