先拋一個問題給我聰明的讀者,若是大家使用微服務SpringCloud-Netflix
進行業務開發,那麼線上註冊中心確定也是用了集羣部署,問題來了:java
你瞭解Eureka註冊中心集羣如何實現客戶端請求負載及故障轉移嗎?算法
能夠先思考一分鐘,我但願你可以帶着問題來閱讀此篇文章,也但願你看完文章後會有所收穫!微信
前段時間線上Sentry
平臺報警,多個業務服務在和註冊中心交互時,例如續約和註冊表增量拉取等都報了Request execution failed with message : Connection refused
的警告:markdown
緊接着又看到 Request execution succeeded on retry #2
的日誌。網絡
看到這裏,代表咱們的服務在嘗試兩次重連後和註冊中心交互正常了。負載均衡
一切都顯得那麼有驚無險,這裏報Connection refused 是註冊中心網絡抖動致使的,接着觸發了咱們服務的重連,重連成功後一切又恢復正常。dom
此次的報警雖然沒有對咱們線上業務形成影響,而且也在第一時間恢復了正常,但做爲一個愛思考的小火雞,我很好奇這背後的一系列邏輯:Eureka註冊中心集羣如何實現客戶端請求負載及故障轉移?
ide
線上註冊中心是由三臺機器組成的集羣,都是4c8g
的配置,業務端配置註冊中心地址以下(這裏的peer來代替具體的ip地址
):微服務
eureka.client.serviceUrl.defaultZone=http://peer1:8080/eureka/,http://peer2:8080/eureka/,http://peer3:8080/eureka/ 複製代碼
咱們能夠寫了一個Demo
進行測試:oop
一、本地經過修改EurekaServer
服務的端口號來模擬註冊中心集羣部署,分別以8761
和8762
兩個端口進行啓動 二、啓動客戶端SeviceA
,配置註冊中心地址爲:http://localhost:8761/eureka,http://localhost:8762/eureka
三、啓動SeviceA
時在發送註冊請求的地方打斷點:AbstractJerseyEurekaHttpClient.register()
,以下圖所示:
這裏看到請求註冊中心時,鏈接的是8761
這個端口的服務。
四、更改ServiceA
中註冊中心的配置:http://localhost:8762/eureka,http://localhost:8761/eureka
五、從新啓動SeviceA
而後查看端口,以下圖所示:
8762
這個端口的服務。
以兩個端口分別啓動EurekaServer
服務,再啓動一個客戶端ServiceA
。啓動成功後,關閉一個8761
端口對應的服務,查看此時客戶端是否會自動遷移請求到8762
端口對應的服務:
一、以8761
和8762
兩個端口號啓動EurekaServer
二、啓動ServiceA
,配置註冊中心地址爲:http://localhost:8761/eureka,http://localhost:8762/eureka
三、啓動成功後,關閉8761
端口的EurekaServer
四、在EurekaClient
端發送心跳請求
的地方打上斷點:AbstractJerseyEurekaHttpClient.sendHeartBeat()
五、查看斷點處數據,第一次請求的EurekaServer
是8761
端口的服務,由於該服務已經關閉,因此返回的response
是null
8762
端口的服務,返回的
response
爲狀態爲
200
,故障轉移成功,以下圖:
經過這兩個測試Demo
,我覺得EurekaClient
每次都會取defaultZone
配置的第一個host
做爲請求EurekaServer
的請求的地址,若是該節點故障時,會自動切換配置中的下一個EurekaServer
進行從新請求。
那麼疑問來了,EurekaClient
每次請求真的是以配置的defaultZone
配置的第一個服務節點做爲請求的嗎?這彷佛也太弱了!!?
EurekaServer
集羣不就成了僞集羣
!!?除了客戶端配置的第一個節點,其它註冊中心的節點都只能做爲備份和故障轉移來使用!!?
真相是這樣嗎?NO!咱們眼見也不必定爲實,源碼面前毫無祕密!
翠花,上乾貨!
仍是先上結論,負載原理如圖所示:
這裏會以EurekaClient
端的IP
做爲隨機的種子,而後隨機打亂serverList
,例如咱們在**商品服務(192.168.10.56)**中配置的註冊中心集羣地址爲:peer1,peer2,peer3
,打亂後的地址可能變成peer3,peer2,peer1
。
**用戶服務(192.168.22.31)**中配置的註冊中心集羣地址爲:peer1,peer2,peer3
,打亂後的地址可能變成peer2,peer1,peer3
。
EurekaClient
每次請求serverList
中的第一個服務,從而達到負載的目的。
咱們直接看最底層負載代碼的實現,具體代碼在 com.netflix.discovery.shared.resolver.ResolverUtils.randomize()
中:
這裏面random
是經過咱們EurekaClient
端的ipv4
作爲隨機的種子,生成一個從新排序的serverList
,也就是對應代碼中的randomList
,因此每一個EurekaClient
獲取到的serverList
順序可能不一樣,在使用過程當中,取列表的第一個元素做爲server
端host
,從而達到負載的目的。
原來代碼是經過EurekaClient
的IP
進行負載的,因此剛纔經過DEMO
程序結果就能解釋的通了,由於咱們作實驗都是用的同一個IP
,因此每次都是會訪問同一個Server
節點。
既然說到了負載,這裏確定會有另外一個疑問:
經過IP進行的負載均衡,每次請求都會均勻分散到每個Server
節點嗎?
好比第一次訪問Peer1
,第二次訪問Peer2
,第三次訪問Peer3
,第四次繼續訪問Peer1
等,循環往復......
咱們能夠繼續作個試驗,假如咱們有10000個EurekaClient
節點,3個EurekaServer
節點。
Client
節點的IP
區間爲:192.168.0.0 ~ 192.168.255.255
,這裏面共覆蓋6w多個ip
段,測試代碼以下:
/** * 模擬註冊中心集羣負載,驗證負載散列算法 * * @author 一枝花算不算浪漫 * @date 2020/6/21 23:36 */ public class EurekaClusterLoadBalanceTest { public static void main(String[] args) { testEurekaClusterBalance(); } /** * 模擬ip段測試註冊中心負載集羣 */ 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地址獲取對應註冊中心負載 */ 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) + "%"); } } } 複製代碼
負載測試結果以下:
能夠看到第二個機器會有**50%的請求,最後一臺機器只有17%**的請求,負載的狀況並非很均勻,我認爲經過IP
負載並非一個好的方案。
還記得咱們以前講過Ribbon
默認的輪詢算法RoundRobinRule
,【一塊兒學源碼-微服務】Ribbon 源碼四:進一步探究Ribbon的IRule和IPing 。
這種算法就是一個很好的散列算法,能夠保證每次請求都很均勻,原理以下圖:
仍是先上結論,以下圖:
咱們的serverList
按照client
端的ip
進行重排序後,每次都會請求第一個元素做爲和Server
端交互的host
,若是請求失敗,會嘗試請求serverList
列表中的第二個元素繼續請求,此次請求成功後,會將這次請求的host
放到全局的一個變量中保存起來,下次client
端再次請求 就會直接使用這個host
。
這裏最多會重試請求兩次。
直接看底層交互的代碼,位置在 com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute()
中:
咱們來分析下這個代碼:
client
上次成功server
端的host
,若是有值則直接使用這個host
getHostCandidates()
是獲取client
端配置的serverList
數據,且經過ip
進行重排序的列表candidateHosts.get(endpointIdx++)
,初始endpointIdx=0
,獲取列表中第1個元素做爲host
請求response
結果,若是返回的狀態碼是200
,則將這次請求的host
設置到全局的delegate
變量中response
返回的狀態碼不是200
,也就是執行失敗,將全局變量delegate
中的數據清空endpointIdx=1
,獲取列表中的第二個元素做爲host
請求numberOfRetries=3
,最多重試2次就會跳出循環咱們還能夠第123和129行,這也正是咱們業務拋出來的日誌信息,全部的一切都對應上了。
感謝你看到這裏,相信你已經清楚了開頭提問的問題。
上面已經分析完了Eureka
集羣下Client
端請求時負載均衡的選擇以及集羣故障時自動重試請求的實現原理。
若是還有不懂的問題,能夠添加個人微信或者給我公衆號留言,我會單獨和你討論交流。
本文首發自:一枝花算不算浪漫
公衆號,如若轉載請在文章開頭標明出處,如需開白可直接公衆號回覆便可。