一個(gè)線上問題的思考:Eureka注冊中心集群如何實(shí)現(xiàn)客戶端請求負(fù)載及故障轉(zhuǎn)移?

前言

先拋一個(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 的警告:

連接拒絕.jpg

緊接著又看到 Request execution succeeded on retry #2 的日志胧辽。

連接重試.jpg

看到這里,表明我們的服務(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)移?

問題思考梳理.png

注冊中心集群負(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ù)的端口號來模擬注冊中心集群部署枣抱,分別以87618762兩個(gè)端口進(jìn)行啟動
2熔吗、啟動客戶端SeviceA,配置注冊中心地址為:http://localhost:8761/eureka,http://localhost:8762/eureka

EurekaClient端配置.png

3佳晶、啟動SeviceA時(shí)在發(fā)送注冊請求的地方打斷點(diǎn):AbstractJerseyEurekaHttpClient.register()桅狠,如下圖所示:

8761在前.png

這里看到請求注冊中心時(shí),連接的是8761這個(gè)端口的服務(wù)轿秧。

4中跌、更改ServiceA中注冊中心的配置:http://localhost:8762/eureka,http://localhost:8761/eureka
5、重新啟動SeviceA然后查看端口菇篡,如下圖所示:

8762在前.png

此時(shí)看到請求注冊中心是拔创,連接的是8762這個(gè)端口的服務(wù)尾序。

注冊中心故障轉(zhuǎn)移測試

以兩個(gè)端口分別啟動EurekaServer服務(wù),再啟動一個(gè)客戶端ServiceA。啟動成功后君珠,關(guān)閉一個(gè)8761端口對應(yīng)的服務(wù),查看此時(shí)客戶端是否會自動遷移請求到8762端口對應(yīng)的服務(wù):

1婚温、以87618762兩個(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ù)路狮,第一次請求的EurekaServer8761端口的服務(wù),因?yàn)樵摲?wù)已經(jīng)關(guān)閉蔚约,所以返回的responsenull

8761故障.png

6览祖、第二次會重新請求8762端口的服務(wù),返回的response為狀態(tài)為200炊琉,故障轉(zhuǎn)移成功展蒂,如下圖:
8762故障轉(zhuǎn)移.png

思考

通過這兩個(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ù)載原理如圖所示:

負(fù)載原理.png

這里會以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() 中:

代碼實(shí)現(xiàn).png

這里面random 是通過我們EurekaClient端的ipv4做為隨機(jī)的種子榕栏,生成一個(gè)重新排序的serverList畔勤,也就是對應(yīng)代碼中的randomList,所以每個(gè)EurekaClient獲取到的serverList順序可能不同扒磁,在使用過程中庆揪,取列表的第一個(gè)元素作為serverhost,從而達(dá)到負(fù)載的目的妨托。

負(fù)載均衡代碼實(shí)現(xiàn).png

思考

原來代碼是通過EurekaClientIP進(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é)果如下:


負(fù)載測試結(jié)果.png

可以看到第二個(gè)機(jī)器會有50%的請求沃缘,最后一臺機(jī)器只有17%的請求躯枢,負(fù)載的情況并不是很均勻,我認(rèn)為通過IP負(fù)載并不是一個(gè)好的方案槐臀。

還記得我們之前講過Ribbon默認(rèn)的輪詢算法RoundRobinRule锄蹂,【一起學(xué)源碼-微服務(wù)】Ribbon 源碼四:進(jìn)一步探究Ribbon的IRule和IPing

這種算法就是一個(gè)很好的散列算法水慨,可以保證每次請求都很均勻得糜,原理如下圖:

Ribbon輪詢算法.png

故障轉(zhuǎn)移原理

原理圖解

還是先上結(jié)論,如下圖:

故障轉(zhuǎn)移原理.png

我們的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() 中:

重試代碼.png

我們來分析下這個(gè)代碼:

  1. 第101行贝润,獲取client上次成功server端的host绊茧,如果有值則直接使用這個(gè)host
  2. 第105行,getHostCandidates()是獲取client端配置的serverList數(shù)據(jù)打掘,且通過ip進(jìn)行重排序的列表
  3. 第114行华畏,candidateHosts.get(endpointIdx++),初始endpointIdx=0尊蚁,獲取列表中第1個(gè)元素作為host請求
  4. 第120行唯绍,獲取返回的response結(jié)果,如果返回的狀態(tài)碼是200枝誊,則將此次請求的host設(shè)置到全局的delegate變量中
  5. 第133行况芒,執(zhí)行到這里說明第120行執(zhí)行的response返回的狀態(tài)碼不是200,也就是執(zhí)行失敗,將全局變量delegate中的數(shù)據(jù)清空
  6. 再次循環(huán)第一步绝骚,此時(shí)endpointIdx=1耐版,獲取列表中的第二個(gè)元素作為host請求
  7. 依次執(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ù)即可洒宝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末购公,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子雁歌,更是在濱河造成了極大的恐慌宏浩,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件靠瞎,死亡現(xiàn)場離奇詭異比庄,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)较坛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門印蔗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丑勤,你說我怎么就攤上這事华嘹。” “怎么了法竞?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵耙厚,是天一觀的道長。 經(jīng)常有香客問我岔霸,道長薛躬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任呆细,我火速辦了婚禮型宝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己趴酣,他們只是感情好梨树,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著岖寞,像睡著了一般抡四。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仗谆,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天指巡,我揣著相機(jī)與錄音,去河邊找鬼隶垮。 笑死藻雪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的岁疼。 我是一名探鬼主播阔涉,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼缆娃,長吁一口氣:“原來是場噩夢啊……” “哼捷绒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起贯要,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤暖侨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后崇渗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體字逗,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年宅广,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了葫掉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡跟狱,死狀恐怖俭厚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情驶臊,我是刑警寧澤挪挤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站关翎,受9級特大地震影響扛门,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜纵寝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一论寨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦葬凳、人聲如沸贞铣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辕坝。三九已至,卻和暖如春荐健,著一層夾襖步出監(jiān)牢的瞬間酱畅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工江场, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纺酸,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓址否,卻偏偏與公主長得像餐蔬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子佑附,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345