前言
先拋一個(gè)問題給我聰明的讀者桑谍,如果你們使用微服務(wù)SpringCloud-Netflix
進(jìn)行業(yè)務(wù)開發(fā),那么線上注冊中心肯定也是用了集群部署祸挪,問題來了:
你了解Eureka注冊中心集群如何實(shí)現(xiàn)客戶端請求負(fù)載及故障轉(zhuǎn)移嗎锣披?
可以先思考一分鐘,我希望你能夠帶著問題來閱讀此篇文章贿条,也希望你看完文章后會有所收獲雹仿!
背景
前段時(shí)間線上Sentry
平臺報(bào)警,多個(gè)業(yè)務(wù)服務(wù)在和注冊中心交互時(shí)整以,例如續(xù)約和注冊表增量拉取等都報(bào)了Request execution failed with message : Connection refused
的警告:
緊接著又看到 Request execution succeeded on retry #2
的日志胧辽。
看到這里,表明我們的服務(wù)在嘗試兩次重連后和注冊中心交互正常了公黑。
一切都顯得那么有驚無險(xiǎn)邑商,這里報(bào)Connection refused 是注冊中心網(wǎng)絡(luò)抖動導(dǎo)致的,接著觸發(fā)了我們服務(wù)的重連凡蚜,重連成功后一切又恢復(fù)正常人断。
這次的報(bào)警雖然沒有對我們線上業(yè)務(wù)造成影響,并且也在第一時(shí)間恢復(fù)了正常朝蜘,但作為一個(gè)愛思考的小火雞恶迈,我很好奇這背后的一系列邏輯:Eureka注冊中心集群如何實(shí)現(xiàn)客戶端請求負(fù)載及故障轉(zhuǎn)移?
注冊中心集群負(fù)載測試
線上注冊中心是由三臺機(jī)器組成的集群芹务,都是4c8g
的配置蝉绷,業(yè)務(wù)端配置注冊中心地址如下(這里的peer來代替具體的ip地址
):
eureka.client.serviceUrl.defaultZone=http://peer1:8080/eureka/,http://peer2:8080/eureka/,http://peer3:8080/eureka/
我們可以寫了一個(gè)Demo
進(jìn)行測試:
注冊中心集群負(fù)載測試
1、本地通過修改EurekaServer
服務(wù)的端口號來模擬注冊中心集群部署枣抱,分別以8761
和8762
兩個(gè)端口進(jìn)行啟動
2熔吗、啟動客戶端SeviceA
,配置注冊中心地址為:http://localhost:8761/eureka,http://localhost:8762/eureka
3佳晶、啟動SeviceA
時(shí)在發(fā)送注冊請求的地方打斷點(diǎn):AbstractJerseyEurekaHttpClient.register()
桅狠,如下圖所示:
這里看到請求注冊中心時(shí),連接的是8761
這個(gè)端口的服務(wù)轿秧。
4中跌、更改ServiceA
中注冊中心的配置:http://localhost:8762/eureka,http://localhost:8761/eureka
5、重新啟動SeviceA
然后查看端口菇篡,如下圖所示:
此時(shí)看到請求注冊中心是拔创,連接的是
8762
這個(gè)端口的服務(wù)尾序。
注冊中心故障轉(zhuǎn)移測試
以兩個(gè)端口分別啟動EurekaServer
服務(wù),再啟動一個(gè)客戶端ServiceA
。啟動成功后君珠,關(guān)閉一個(gè)8761
端口對應(yīng)的服務(wù),查看此時(shí)客戶端是否會自動遷移請求到8762
端口對應(yīng)的服務(wù):
1婚温、以8761
和8762
兩個(gè)端口號啟動EurekaServer
2歹篓、啟動ServiceA
,配置注冊中心地址為:http://localhost:8761/eureka,http://localhost:8762/eureka
3咐容、啟動成功后舆逃,關(guān)閉8761
端口的EurekaServer
4、在EurekaClient
端發(fā)送心跳請求
的地方打上斷點(diǎn):AbstractJerseyEurekaHttpClient.sendHeartBeat()
5戳粒、查看斷點(diǎn)處數(shù)據(jù)路狮,第一次請求的EurekaServer
是8761
端口的服務(wù),因?yàn)樵摲?wù)已經(jīng)關(guān)閉蔚约,所以返回的response
是null
6览祖、第二次會重新請求
8762
端口的服務(wù),返回的response
為狀態(tài)為200
炊琉,故障轉(zhuǎn)移成功展蒂,如下圖:思考
通過這兩個(gè)測試Demo
,我以為EurekaClient
每次都會取defaultZone
配置的第一個(gè)host
作為請求EurekaServer
的請求的地址苔咪,如果該節(jié)點(diǎn)故障時(shí)锰悼,會自動切換配置中的下一個(gè)EurekaServer
進(jìn)行重新請求。
那么疑問來了团赏,EurekaClient
每次請求真的是以配置的defaultZone
配置的第一個(gè)服務(wù)節(jié)點(diǎn)作為請求的嗎箕般?這似乎也太弱了!L蚯濉丝里?
EurekaServer
集群不就成了偽集群
G酢!杯聚?除了客戶端配置的第一個(gè)節(jié)點(diǎn)臼婆,其它注冊中心的節(jié)點(diǎn)都只能作為備份和故障轉(zhuǎn)移來使用!;仙堋颁褂?
真相是這樣嗎?NO傀广!我們眼見也不一定為實(shí)颁独,源碼面前毫無秘密!
翠花伪冰,上干貨誓酒!
客戶端請求負(fù)載原理
原理圖解
還是先上結(jié)論,負(fù)載原理如圖所示:
這里會以EurekaClient
端的IP
作為隨機(jī)的種子贮聂,然后隨機(jī)打亂serverList
丰捷,例如我們在商品服務(wù)(192.168.10.56)中配置的注冊中心集群地址為:peer1,peer2,peer3
,打亂后的地址可能變成peer3,peer2,peer1
寂汇。
用戶服務(wù)(192.168.22.31)中配置的注冊中心集群地址為:peer1,peer2,peer3
病往,打亂后的地址可能變成peer2,peer1,peer3
。
EurekaClient
每次請求serverList
中的第一個(gè)服務(wù)骄瓣,從而達(dá)到負(fù)載的目的停巷。
代碼實(shí)現(xiàn)
我們直接看最底層負(fù)載代碼的實(shí)現(xiàn),具體代碼在
com.netflix.discovery.shared.resolver.ResolverUtils.randomize()
中:
這里面random
是通過我們EurekaClient
端的ipv4
做為隨機(jī)的種子榕栏,生成一個(gè)重新排序的serverList
畔勤,也就是對應(yīng)代碼中的randomList
,所以每個(gè)EurekaClient
獲取到的serverList
順序可能不同扒磁,在使用過程中庆揪,取列表的第一個(gè)元素作為server
端host
,從而達(dá)到負(fù)載的目的妨托。
思考
原來代碼是通過EurekaClient
的IP
進(jìn)行負(fù)載的缸榛,所以剛才通過DEMO
程序結(jié)果就能解釋的通了,因?yàn)槲覀冏鰧?shí)驗(yàn)都是用的同一個(gè)IP
兰伤,所以每次都是會訪問同一個(gè)Server
節(jié)點(diǎn)内颗。
既然說到了負(fù)載,這里肯定會有另一個(gè)疑問:
通過IP進(jìn)行的負(fù)載均衡敦腔,每次請求都會均勻分散到每一個(gè)Server
節(jié)點(diǎn)嗎均澳?
比如第一次訪問Peer1
,第二次訪問Peer2
,第三次訪問Peer3
找前,第四次繼續(xù)訪問Peer1
等糟袁,循環(huán)往復(fù)......
我們可以繼續(xù)做個(gè)試驗(yàn),假如我們有10000個(gè)EurekaClient
節(jié)點(diǎn)躺盛,3個(gè)EurekaServer
節(jié)點(diǎn)项戴。
Client
節(jié)點(diǎn)的IP
區(qū)間為:192.168.0.0 ~ 192.168.255.255
,這里面共覆蓋6w多個(gè)ip
段颗品,測試代碼如下:
/**
* 模擬注冊中心集群負(fù)載肯尺,驗(yàn)證負(fù)載散列算法
*
* @author 一枝花算不算浪漫
* @date 2020/6/21 23:36
*/
public class EurekaClusterLoadBalanceTest {
public static void main(String[] args) {
testEurekaClusterBalance();
}
/**
* 模擬ip段測試注冊中心負(fù)載集群
*/
private static void testEurekaClusterBalance() {
int ipLoopSize = 65000;
String ipFormat = "192.168.%s.%s";
TreeMap<String, Integer> ipMap = Maps.newTreeMap();
int netIndex = 0;
int lastIndex = 0;
for (int i = 0; i < ipLoopSize; i++) {
if (lastIndex == 256) {
netIndex += 1;
lastIndex = 0;
}
String ip = String.format(ipFormat, netIndex, lastIndex);
randomize(ip, ipMap);
System.out.println("IP: " + ip);
lastIndex += 1;
}
printIpResult(ipMap, ipLoopSize);
}
/**
* 模擬指定ip地址獲取對應(yīng)注冊中心負(fù)載
*/
private static void randomize(String eurekaClientIp, TreeMap<String, Integer> ipMap) {
List<String> eurekaServerUrlList = Lists.newArrayList();
eurekaServerUrlList.add("http://peer1:8080/eureka/");
eurekaServerUrlList.add("http://peer2:8080/eureka/");
eurekaServerUrlList.add("http://peer3:8080/eureka/");
List<String> randomList = new ArrayList<>(eurekaServerUrlList);
Random random = new Random(eurekaClientIp.hashCode());
int last = randomList.size() - 1;
for (int i = 0; i < last; i++) {
int pos = random.nextInt(randomList.size() - i);
if (pos != i) {
Collections.swap(randomList, i, pos);
}
}
for (String eurekaHost : randomList) {
int ipCount = ipMap.get(eurekaHost) == null ? 0 : ipMap.get(eurekaHost);
ipMap.put(eurekaHost, ipCount + 1);
break;
}
}
private static void printIpResult(TreeMap<String, Integer> ipMap, int totalCount) {
for (Map.Entry<String, Integer> entry : ipMap.entrySet()) {
Integer count = entry.getValue();
BigDecimal rate = new BigDecimal(count).divide(new BigDecimal(totalCount), 2, BigDecimal.ROUND_HALF_UP);
System.out.println(entry.getKey() + ":" + count + ":" + rate.multiply(new BigDecimal(100)).setScale(0, BigDecimal.ROUND_HALF_UP) + "%");
}
}
}
負(fù)載測試結(jié)果如下:
可以看到第二個(gè)機(jī)器會有50%的請求沃缘,最后一臺機(jī)器只有17%的請求躯枢,負(fù)載的情況并不是很均勻,我認(rèn)為通過IP
負(fù)載并不是一個(gè)好的方案槐臀。
還記得我們之前講過Ribbon
默認(rèn)的輪詢算法RoundRobinRule
锄蹂,【一起學(xué)源碼-微服務(wù)】Ribbon 源碼四:進(jìn)一步探究Ribbon的IRule和IPing 。
這種算法就是一個(gè)很好的散列算法水慨,可以保證每次請求都很均勻得糜,原理如下圖:
故障轉(zhuǎn)移原理
原理圖解
還是先上結(jié)論,如下圖:
我們的serverList
按照client
端的ip
進(jìn)行重排序后晰洒,每次都會請求第一個(gè)元素作為和Server
端交互的host
朝抖,如果請求失敗,會嘗試請求serverList
列表中的第二個(gè)元素繼續(xù)請求谍珊,這次請求成功后治宣,會將此次請求的host
放到全局的一個(gè)變量中保存起來,下次client
端再次請求 就會直接使用這個(gè)host
砌滞。
這里最多會重試請求兩次侮邀。
代碼實(shí)現(xiàn)
直接看底層交互的代碼,位置在
com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute()
中:
我們來分析下這個(gè)代碼:
- 第101行贝润,獲取
client
上次成功server
端的host
绊茧,如果有值則直接使用這個(gè)host
- 第105行,
getHostCandidates()
是獲取client
端配置的serverList
數(shù)據(jù)打掘,且通過ip
進(jìn)行重排序的列表 - 第114行华畏,
candidateHosts.get(endpointIdx++)
,初始endpointIdx=0
尊蚁,獲取列表中第1個(gè)元素作為host
請求 - 第120行唯绍,獲取返回的
response
結(jié)果,如果返回的狀態(tài)碼是200
枝誊,則將此次請求的host
設(shè)置到全局的delegate
變量中 - 第133行况芒,執(zhí)行到這里說明第120行執(zhí)行的
response
返回的狀態(tài)碼不是200
,也就是執(zhí)行失敗,將全局變量delegate
中的數(shù)據(jù)清空 - 再次循環(huán)第一步绝骚,此時(shí)
endpointIdx=1
耐版,獲取列表中的第二個(gè)元素作為host
請求 - 依次執(zhí)行,第100行的循環(huán)條件
numberOfRetries=3
压汪,最多重試2次就會跳出循環(huán)
我們還可以第123和129行粪牲,這也正是我們業(yè)務(wù)拋出來的日志信息,所有的一切都對應(yīng)上了止剖。
總結(jié)
感謝你看到這里腺阳,相信你已經(jīng)清楚了開頭提問的問題。
上面已經(jīng)分析完了Eureka
集群下Client
端請求時(shí)負(fù)載均衡的選擇以及集群故障時(shí)自動重試請求的實(shí)現(xiàn)原理穿香。
如果還有不懂的問題亭引,可以添加我的微信或者給我公眾號留言,我會單獨(dú)和你討論交流皮获。
本文首發(fā)自:一枝花算不算浪漫
公眾號焙蚓,如若轉(zhuǎn)載請?jiān)谖恼麻_頭標(biāo)明出處,如需開白可直接公眾號回復(fù)即可洒宝。