日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

java 一致性hash算法 均衡分发_Dubbo一致性哈希负载均衡的源码和Bug,了解一下?...

發(fā)布時(shí)間:2025/3/15 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java 一致性hash算法 均衡分发_Dubbo一致性哈希负载均衡的源码和Bug,了解一下?... 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

本文是對(duì)于Dubbo負(fù)載均衡策略之一的一致性哈希負(fù)載均衡的詳細(xì)分析。對(duì)源碼逐行解讀、根據(jù)實(shí)際運(yùn)行結(jié)果,配以豐富的圖片,可能是東半球講一致性哈希算法在Dubbo中的實(shí)現(xiàn)最詳細(xì)的文章了。

文中所示源碼,沒有特別標(biāo)注的地方,均為2.7.4.1版本。

在撰寫本文的過程中,發(fā)現(xiàn)了Dubbo2.7.0版本之后的一個(gè)bug。會(huì)導(dǎo)致性能問題,且目前還未解決,如果你們的負(fù)載均衡配置的是一致性哈希或者考慮使用一致性哈希的話,可以了解一下。

本文目錄

第一節(jié):哈希算法

本小節(jié)主要是為了介紹一致性哈希算法做鋪墊。簡(jiǎn)單的介紹了哈希算法解決了什么問題,帶來了什么問題。

第二節(jié):一致性哈希算法

本小節(jié)主要是通過作圖對(duì)一致性哈希進(jìn)行了簡(jiǎn)單的介紹。介紹了一致性哈希是怎么解決哈希算法帶來的問題,怎么解決數(shù)據(jù)傾斜的問題。

第三節(jié):一致性哈希算法在Dubbo中的應(yīng)用

本小節(jié)是全文重點(diǎn),通過一個(gè)"騷"操作,對(duì)Dubbo一致性哈希算法的源碼進(jìn)行了十分詳細(xì)的剖析。從整個(gè)類到類里面的每個(gè)方法進(jìn)行了詳盡的分析,打印了大量的日志,配合圖片,方便讀者理解。

第四節(jié):我又發(fā)現(xiàn)了一個(gè)Bug

本小節(jié)主要是介紹我在研究Dubbo一致性哈希負(fù)載均衡時(shí)遇到的一個(gè)問題,深入研究之后發(fā)現(xiàn)可能是一個(gè)Bug。這一小節(jié)就是比較詳盡的介紹了這個(gè)Bug現(xiàn)象、原因以及我的解決方案。

第五節(jié):加入節(jié)點(diǎn),畫圖分析

本小節(jié)對(duì)具體的案例進(jìn)行了分析,并配以圖片,相信能幫助讀者更加深刻的理解一致性哈希算法在Dubbo中的應(yīng)用。

第六節(jié):一致性哈希的應(yīng)用場(chǎng)景

本小節(jié)主要介紹幾個(gè)應(yīng)用場(chǎng)景。使用Duboo框架,在什么樣的需求可以使用一致性哈希算法做負(fù)載均衡。

PS:前一、二節(jié)主要是進(jìn)行了背景知識(shí)的簡(jiǎn)單鋪墊,如果你了解相關(guān)背景知識(shí),可以直接從第三節(jié)看起。本文的重點(diǎn)是第三到第五節(jié)。如果你只想知道Bug是什么,可以直接閱讀第四節(jié)。

另:閱讀本文需要對(duì)Dubbo有一定的了解。文章很長(zhǎng),建議收藏慢慢閱讀。一定會(huì)有收獲的。

哈希算法

在介紹一致性哈希算法之前,我們看看哈希算法,以及它解決了什么問題,帶來了什么問題。

如上圖所示,假設(shè)0,1,2號(hào)服務(wù)器都存儲(chǔ)的有用戶信息,那么當(dāng)我們需要獲取某用戶信息時(shí),因?yàn)槲覀儾恢涝撚脩粜畔⒋娣旁谀囊慌_(tái)服務(wù)器中,所以需要分別查詢0,1,2號(hào)服務(wù)器。這樣獲取數(shù)據(jù)的效率是極低的。

對(duì)于這樣的場(chǎng)景,我們可以引入哈希算法。

還是上面的場(chǎng)景,但前提是每一臺(tái)服務(wù)器存放用戶信息時(shí)是根據(jù)某一種哈希算法存放的。所以取用戶信息的時(shí)候,也按照同樣的哈希算法取即可。

假設(shè)我們要查詢用戶號(hào)為100的用戶信息,經(jīng)過某個(gè)哈希算法,比如這里的userId mod n,即100 mod 3結(jié)果為1。所以用戶號(hào)100的這個(gè)請(qǐng)求最終會(huì)被1號(hào)服務(wù)器接收并處理。

這樣就解決了無效查詢的問題。

但是這樣的方案會(huì)帶來什么問題呢?

擴(kuò)容或者縮容時(shí),會(huì)導(dǎo)致大量的數(shù)據(jù)遷移。最少也會(huì)影響百分之50的數(shù)據(jù)。

為了說明問題,我們加入一臺(tái)服務(wù)器3。服務(wù)器的數(shù)量n就從3變成了4。還是查詢用戶號(hào)為100的用戶信息時(shí),100 mod 4結(jié)果為0。這時(shí),請(qǐng)求就被0號(hào)服務(wù)器接收了。

當(dāng)服務(wù)器數(shù)量為3時(shí),用戶號(hào)為100的請(qǐng)求會(huì)被1號(hào)服務(wù)器處理。

當(dāng)服務(wù)器數(shù)量為4時(shí),用戶號(hào)為100的請(qǐng)求會(huì)被0號(hào)服務(wù)器處理。

所以,當(dāng)服務(wù)器數(shù)量增加或者減少時(shí),一定會(huì)涉及到大量數(shù)據(jù)遷移的問題。可謂是牽一發(fā)而動(dòng)全身。

對(duì)于上述哈希算法其優(yōu)點(diǎn)是簡(jiǎn)單易用,大多數(shù)分庫(kù)分表規(guī)則就采取的這種方式。一般是提前根據(jù)數(shù)據(jù)量,預(yù)先估算好分區(qū)數(shù)。

其缺點(diǎn)是由于擴(kuò)容或收縮節(jié)點(diǎn)導(dǎo)致節(jié)點(diǎn)數(shù)量變化時(shí),節(jié)點(diǎn)的映射關(guān)系需要重新計(jì)算,會(huì)導(dǎo)致數(shù)據(jù)進(jìn)行遷移。所以擴(kuò)容時(shí)通常采用翻倍擴(kuò)容,避免數(shù)據(jù)映射全部被打亂,導(dǎo)致全量遷移的情況,這樣只會(huì)發(fā)生50%的數(shù)據(jù)遷移。

假設(shè)這是一個(gè)緩存服務(wù),數(shù)據(jù)的遷移會(huì)導(dǎo)致在遷移的時(shí)間段內(nèi),有緩存是失效的。緩存失效,可怕啊。還記得我之前的文章嗎,《當(dāng)周杰倫把QQ音樂干翻的時(shí)候,作為程序猿我看到了什么?》就是講緩存擊穿、緩存穿透、緩存雪崩的場(chǎng)景和對(duì)應(yīng)的解決方案。

一致性哈希算法

為了解決哈希算法帶來的數(shù)據(jù)遷移問題,一致性哈希算法應(yīng)運(yùn)而生。

對(duì)于一致性哈希算法,官方說法如下:

一致性哈希算法在1997年由麻省理工學(xué)院提出,是一種特殊的哈希算法,在移除或者添加一個(gè)服務(wù)器時(shí),能夠盡可能小地改變已存在的服務(wù)請(qǐng)求與處理請(qǐng)求服務(wù)器之間的映射關(guān)系。一致性哈希解決了簡(jiǎn)單哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的動(dòng)態(tài)伸縮等問題。

什么意思呢?我用大白話加畫圖的方式給你簡(jiǎn)單的介紹一下。

一致性哈希,你可以想象成一個(gè)哈希環(huán),它由0到2^32-1個(gè)點(diǎn)組成。A,B,C分別是三臺(tái)服務(wù)器,每一臺(tái)的IP加端口經(jīng)過哈希計(jì)算后的值,在哈希環(huán)上對(duì)應(yīng)如下:

當(dāng)請(qǐng)求到來時(shí),對(duì)請(qǐng)求中的某些參數(shù)進(jìn)行哈希計(jì)算后,也會(huì)得出一個(gè)哈希值,此值在哈希環(huán)上也會(huì)有對(duì)應(yīng)的位置,這個(gè)請(qǐng)求會(huì)沿著順時(shí)針的方向,尋找最近的服務(wù)器來處理它,如下圖所示:

一致性哈希就是這么個(gè)東西。那它是怎么解決服務(wù)器的擴(kuò)容或收縮導(dǎo)致大量的數(shù)據(jù)遷移的呢?

看一下當(dāng)我們使用一致性哈希算法時(shí),加入服務(wù)器會(huì)發(fā)什么事情。

當(dāng)我們加入一個(gè)D服務(wù)器后,假設(shè)其IP加端口,經(jīng)過哈希計(jì)算后落在了哈希環(huán)上圖中所示的位置。

這時(shí)影響的范圍只有圖中標(biāo)注了五角星的區(qū)間。這個(gè)區(qū)間的請(qǐng)求從原來的由C服務(wù)器處理變成了由D服務(wù)器請(qǐng)求。而D到C,C到A,A到B這個(gè)區(qū)間的請(qǐng)求沒有影響,加入D節(jié)點(diǎn)后,A、B服務(wù)器是無感知的。

所以,在一致性哈希算法中,如果增加一臺(tái)服務(wù)器,則受影響的區(qū)間僅僅是新服務(wù)器(D)在哈希環(huán)空間中,逆時(shí)針方向遇到的第一臺(tái)服務(wù)器(B)之間的區(qū)間,其它區(qū)間(D到C,C到A,A到B)不會(huì)受到影響。

在加入了D服務(wù)器的情況下,我們?cè)偌僭O(shè)一段時(shí)間后,C服務(wù)器宕機(jī)了:

當(dāng)C服務(wù)器宕機(jī)后,影響的范圍也是圖中標(biāo)注了五角星的區(qū)間。C節(jié)點(diǎn)宕機(jī)后,B、D服務(wù)器是無感知的。

所以,在一致性哈希算法中,如果宕機(jī)一臺(tái)服務(wù)器,則受影響的區(qū)間僅僅是宕機(jī)服務(wù)器(C)在哈希環(huán)空間中,逆時(shí)針方向遇到的第一臺(tái)服務(wù)器(D)之間的區(qū)間,其它區(qū)間(C到A,A到B,B到D)不會(huì)受到影響。

綜上所述,在一致性哈希算法中,不管是增加節(jié)點(diǎn),還是宕機(jī)節(jié)點(diǎn),受影響的區(qū)間僅僅是增加或者宕機(jī)服務(wù)器在哈希環(huán)空間中,逆時(shí)針方向遇到的第一臺(tái)服務(wù)器之間的區(qū)間,其它區(qū)間不會(huì)受到影響。

是不是很完美?

不是的,理想和現(xiàn)實(shí)的差距是巨大的。

一致性哈希算法帶來了什么問題?

當(dāng)節(jié)點(diǎn)很少的時(shí)候可能會(huì)出現(xiàn)這樣的分布情況,A服務(wù)會(huì)承擔(dān)大部分請(qǐng)求。這種情況就叫做數(shù)據(jù)傾斜。

怎么解決數(shù)據(jù)傾斜呢?加入虛擬節(jié)點(diǎn)。

怎么去理解這個(gè)虛擬節(jié)點(diǎn)呢?

首先一個(gè)服務(wù)器根據(jù)需要可以有多個(gè)虛擬節(jié)點(diǎn)。假設(shè)一臺(tái)服務(wù)器有n個(gè)虛擬節(jié)點(diǎn)。那么哈希計(jì)算時(shí),可以使用IP+端口+編號(hào)的形式進(jìn)行哈希值計(jì)算。其中的編號(hào)就是0到n的數(shù)字。由于IP+端口是一樣的,所以這n個(gè)節(jié)點(diǎn)都是指向的同一臺(tái)機(jī)器。

如下圖所示:

在沒有加入虛擬節(jié)點(diǎn)之前,A服務(wù)器承擔(dān)了絕大多數(shù)的請(qǐng)求。但是假設(shè)每個(gè)服務(wù)器有一個(gè)虛擬節(jié)點(diǎn)(A-1,B-1,C-1),經(jīng)過哈希計(jì)算后落在了如上圖所示的位置。那么A服務(wù)器的承擔(dān)的請(qǐng)求就在一定程度上(圖中標(biāo)注了五角星的部分)分?jǐn)偨o了B-1、C-1虛擬節(jié)點(diǎn),實(shí)際上就是分?jǐn)偨o了B、C服務(wù)器。

一致性哈希算法中,加入虛擬節(jié)點(diǎn),可以解決數(shù)據(jù)傾斜問題。

當(dāng)你在面試的過程中,如果聽到了類似于數(shù)據(jù)傾斜的字眼。那大概率是在問你一致性哈希算法和虛擬節(jié)點(diǎn)。

在介紹了相關(guān)背景后,我們可以去看看一致性哈希算法在Dubbo中的應(yīng)用了。

一致性哈希算法在Dubbo中的應(yīng)用

經(jīng)過《一文講透Dubbo負(fù)載均衡之最小活躍數(shù)算法》這篇文章我們知道Dubbo中負(fù)載均衡的實(shí)現(xiàn)是通過org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance中的doSelect抽象方法實(shí)現(xiàn)的,一致性哈希負(fù)載均衡的實(shí)現(xiàn)類如下所示:

org.apache.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance

由于一致性哈希實(shí)現(xiàn)類看起來稍微有點(diǎn)抽象,不太好演示,所以我想到了一個(gè)"騷"操作。前面的文章說過LoadBalance是一個(gè)SPI接口:

既然是一個(gè)SPI接口,那我們可以自己擴(kuò)展一個(gè)一模一樣的算法,只是在算法里面加入一點(diǎn)輸出語(yǔ)句方便我們觀察情況。怎么擴(kuò)展SPI接口就不描述了,只要記住代碼里面的輸出語(yǔ)句都是額外加的,此外沒有任何改動(dòng)即可,如下:

整個(gè)類如下圖片所示,請(qǐng)先看完整個(gè)類,有一個(gè)整體的概念后,我會(huì)進(jìn)行方法級(jí)別的分析。

圖片很長(zhǎng),其中我加了很多注釋和輸出語(yǔ)句,可以點(diǎn)開大圖查看,一定會(huì)幫你更加好的理解一致性哈希在Dubbo中的應(yīng)用:

把代碼也貼在這里

public class WhyConsistentHashLoadBalance extends AbstractLoadBalance {

public static final String NAME = "consistenthash";

/**

* Hash nodes name

*/

public static final String HASH_NODES = "hash.nodes";

/**

* Hash arguments name

*/

public static final String HASH_ARGUMENTS = "hash.arguments";

private final ConcurrentMap> selectors =

new ConcurrentHashMap>();

@SuppressWarnings("unchecked")

@Override

protected Invoker doSelect(List> invokers, URL url, Invocation invocation) {

String methodName = RpcUtils.getMethodName(invocation);

String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;

System.out.println("從selectors中獲取value的key=" + key);

//獲取invokers的hashcode

int identityHashCode = System.identityHashCode(invokers);

WhyConsistentHashLoadBalance.ConsistentHashSelector selector =

(WhyConsistentHashLoadBalance.ConsistentHashSelector) selectors.get(key);

//如果invokers是一個(gè)新的List對(duì)象,意味著服務(wù)提供者數(shù)量發(fā)生了變化,可能新增也可能減少了。

//此時(shí)selector.identityHashCode!=identityHashCode條件成立

//如果是第一次調(diào)用此時(shí)selector == null條件成立

if (selector == null || selector.identityHashCode != identityHashCode) {

System.out.println("是新的invokers:" + identityHashCode + ",原:" + (selector == null ? "null" : selector.identityHashCode));

//創(chuàng)建新的ConsistentHashSelector

selectors.put(key, new WhyConsistentHashLoadBalance.ConsistentHashSelector(invokers, methodName, identityHashCode));

selector = (WhyConsistentHashLoadBalance.ConsistentHashSelector) selectors.get(key);

System.out.println("哈希環(huán)構(gòu)建完成,詳情如下:");

for (Map.Entry> entry : selector.virtualInvokers.entrySet()) {

System.out.println("key(哈希值)=" + entry.getKey() + ",value(虛擬節(jié)點(diǎn))=" + entry.getValue());

}

}

//調(diào)用ConsistentHashSelector的select方法選擇Invoker

System.out.println("開始調(diào)用ConsistentHashSelector的select方法選擇Invoker");

return selector.select(invocation);

}

private static final class ConsistentHashSelector {

//使用TreeMap存儲(chǔ)Invoker的虛擬節(jié)點(diǎn)

private final TreeMap> virtualInvokers;

//虛擬節(jié)點(diǎn)數(shù)

private final int replicaNumber;

//hashCode

private final int identityHashCode;

//請(qǐng)求中的參數(shù)下標(biāo)。

//需要對(duì)請(qǐng)求中對(duì)應(yīng)下標(biāo)的參數(shù)進(jìn)行哈希計(jì)算

private final int[] argumentIndex;

ConsistentHashSelector(List> invokers, String methodName, int identityHashCode) {

this.virtualInvokers = new TreeMap>();

this.identityHashCode = identityHashCode;

URL url = invokers.get(0).getUrl();

System.out.println("CHS中url為=" + url);

//即使啟動(dòng)多個(gè)invoker,每個(gè)invoker對(duì)應(yīng)的url上的虛擬節(jié)點(diǎn)數(shù)配置的都是一樣的

//這里默認(rèn)是160個(gè)。本文中的示例代碼設(shè)置為4個(gè)。

this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);

//所有輸出語(yǔ)句都是我加的,CHS是ConsistentHashSelector的縮寫

System.out.println("CHS中url上的【hash.nodes】為=" + replicaNumber);

//獲取參與哈希計(jì)算的參數(shù)下標(biāo)值,默認(rèn)對(duì)第一個(gè)參數(shù)進(jìn)行哈希運(yùn)算

//本文中的示例代碼使用默認(rèn)配置,所以這里的index長(zhǎng)度為1。

String[] index = COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, HASH_ARGUMENTS, "0"));

System.out.println("CHS中url上的【hash.arguments】為=" + Arrays.toString(index));

//for循環(huán),對(duì)argumentIndex進(jìn)行賦值操作。

argumentIndex = new int[index.length];

for (int i = 0; i < index.length; i++) {

argumentIndex[i] = Integer.parseInt(index[i]);

}

System.out.println("CHS中argumentIndex數(shù)組為=" + Arrays.toString(argumentIndex));

//本文中啟動(dòng)了2個(gè)服務(wù)提供者,所以invokers=2

for (Invoker invoker : invokers) {

//獲取每個(gè)invoker的地址

String address = invoker.getUrl().getAddress();

System.out.println("CHS中invoker的地址為=" + address);

for (int i = 0; i < replicaNumber / 4; i++) {

//對(duì)address+i進(jìn)行md5運(yùn)算得到一個(gè)長(zhǎng)度為16的字節(jié)數(shù)組

byte[] digest = md5(address + i);

System.out.println("CHS中對(duì)" + address + i + "進(jìn)行md5計(jì)算");

//對(duì)digest部分字節(jié)進(jìn)行4次hash運(yùn)算得到四個(gè)不同的long型正整數(shù)

for (int h = 0; h < 4; h++) {

//h=0時(shí),取digest中下標(biāo)為0~3的4個(gè)字節(jié)進(jìn)行位運(yùn)算

//h=1時(shí),取digest中下標(biāo)為4~7的4個(gè)字節(jié)進(jìn)行位運(yùn)算

//h=2,h=3時(shí)過程同上

long m = hash(digest, h);

System.out.println("CHS中對(duì)digest進(jìn)行第"+h+"次hash計(jì)算后的值:"+m+",當(dāng)前invoker="+invoker);

//將hash到invoker的映射關(guān)系存儲(chǔ)到virtualInvokers中,

//virtualInvokers需要提供高效的查詢操作,因此選用TreeMap作為存儲(chǔ)結(jié)構(gòu)

virtualInvokers.put(m, invoker);

}

}

}

}

public Invoker select(Invocation invocation) {

String key = toKey(invocation.getArguments());

System.out.println("CHS的select方法根據(jù)argumentIndex取出invocation中參與hash計(jì)算的key="+key);

byte[] digest = md5(key);

//取digest數(shù)組的前四個(gè)字節(jié)進(jìn)行hash運(yùn)算,再將hash值傳給selectForKey方法,

//尋找合適的Invoker

long hash = hash(digest, 0);

System.out.println("CHS的select方法中key=" + key + "經(jīng)過哈希計(jì)算后hash=" + hash);

return selectForKey(hash);

}

//根據(jù)argumentIndex將參數(shù)轉(zhuǎn)化為key。

private String toKey(Object[] args) {

StringBuilder buf = new StringBuilder();

for (int i : argumentIndex) {

if (i >= 0 && i < args.length) {

buf.append(args[i]);

}

}

return buf.toString();

}

private Invoker selectForKey(long hash) {

//到TreeMap中查找第一個(gè)節(jié)點(diǎn)值大于或等于當(dāng)前hash的Invoker

Map.Entry> entry = virtualInvokers.ceilingEntry(hash);

//如果hash大于Invoker在圓環(huán)上最大的位置,此時(shí)entry=null,

//需要將TreeMap的頭節(jié)點(diǎn)賦值給entry

if (entry == null) {

entry = virtualInvokers.firstEntry();

}

System.out.println("CHS的selectForKey方法根據(jù)key="+hash+"選擇出來的invoker="+entry.getValue());

return entry.getValue();

}

private long hash(byte[] digest, int number) {

return (((long) (digest[3 + number * 4] & 0xFF) << 24)

| ((long) (digest[2 + number * 4] & 0xFF) << 16)

| ((long) (digest[1 + number * 4] & 0xFF) << 8)

| (digest[number * 4] & 0xFF))

& 0xFFFFFFFFL;

}

private byte[] md5(String value) {

MessageDigest md5;

try {

md5 = MessageDigest.getInstance("MD5");

} catch (NoSuchAlgorithmException e) {

throw new IllegalStateException(e.getMessage(), e);

}

md5.reset();

byte[] bytes = value.getBytes(StandardCharsets.UTF_8);

md5.update(bytes);

return md5.digest();

}

}

}

改造之后,我們先把程序跑起來,有了輸出就好分析了。

服務(wù)端代碼如下:

其中的端口是需要手動(dòng)修改的,我分別啟動(dòng)服務(wù)在20881和20882端口。

項(xiàng)目中provider.xml配置如下:

consumer.xml配置如下:

然后,啟動(dòng)在20881和20882端口分別啟動(dòng)兩個(gè)服務(wù)端。客戶端消費(fèi)如下:

運(yùn)行結(jié)果輸出如下,可以先看個(gè)大概的輸出,下面會(huì)對(duì)每一部分輸出進(jìn)行逐一的解讀。

好了,用例也跑起來了,日志也有了。接下來開始結(jié)合代碼和日志進(jìn)行方法級(jí)別的分析。

首先是doSelect方法的入口:

從上圖我們知道了,第一次調(diào)用需要對(duì)selectors進(jìn)行put操作,selectors的key是接口中定義的方法,value是ConsistentHashSelector內(nèi)部類。

ConsistentHashSelector通過調(diào)用其構(gòu)造函數(shù)進(jìn)行初始化的。invokers(服務(wù)端)作為參數(shù)傳遞到了構(gòu)造函數(shù)中,構(gòu)造函數(shù)里面的邏輯,就是把服務(wù)端映射到哈希環(huán)上的過程,請(qǐng)看下圖,結(jié)合代碼,仔細(xì)分析輸出數(shù)據(jù):

從上圖可以看出,當(dāng)ConsistentHashSelector的構(gòu)造方法調(diào)用完成后,8個(gè)虛擬節(jié)點(diǎn)在哈希環(huán)上已經(jīng)映射完成。兩臺(tái)服務(wù)器,每一臺(tái)4個(gè)虛擬節(jié)點(diǎn)組成了這8個(gè)虛擬節(jié)點(diǎn)。

doSelect方法繼續(xù)執(zhí)行,并打印出每個(gè)虛擬節(jié)點(diǎn)的哈希值和對(duì)應(yīng)的服務(wù)端,請(qǐng)仔細(xì)品讀下圖:

說明一下:上面圖中的哈希環(huán)是沒有考慮比例的,僅僅是展現(xiàn)了兩個(gè)服務(wù)器在哈希環(huán)上的相對(duì)位置。而且為了演示說明方便,僅僅只有8個(gè)節(jié)點(diǎn)。假設(shè)我們有4臺(tái)服務(wù)器,每臺(tái)服務(wù)器的虛擬節(jié)點(diǎn)是默認(rèn)值(160),這個(gè)情況下哈希環(huán)上一共有160*4=640個(gè)節(jié)點(diǎn)。

哈希環(huán)映射完成后,接下來的邏輯是把這次請(qǐng)求經(jīng)過哈希計(jì)算后,映射到哈希環(huán)上,并順時(shí)針方向?qū)ふ矣龅降牡谝粋€(gè)節(jié)點(diǎn),讓該節(jié)點(diǎn)處理該請(qǐng)求:

還記得地址為468e8565的A服務(wù)器是什么端口嗎?前面的圖片中有哦,該服務(wù)對(duì)應(yīng)的端口是20882。

最后我們看看輸出結(jié)果:

和我們預(yù)期的一致。整個(gè)調(diào)用就算是完成了。

再對(duì)兩個(gè)方法進(jìn)行一個(gè)補(bǔ)充說明。

第一個(gè)方法是selectForKey,這個(gè)方法里面邏輯如下圖所示:

虛擬節(jié)點(diǎn)都存儲(chǔ)在TreeMap中。順時(shí)針查詢的邏輯由TreeMap保證。看一下下面的Demo你就明白了。

第二個(gè)方法是hash方法,其中的& 0xFFFFFFFFL的目的如下:

&是位運(yùn)算符,而0xFFFFFFFFL轉(zhuǎn)換為四字節(jié)表現(xiàn)后,其低32位全是1,所以保證了哈希環(huán)的范圍是[0,Integer.MAX_VALUE]:

所以這里我們可以改造這個(gè)哈希環(huán)的范圍,假設(shè)我們改為100000。十進(jìn)制的100000對(duì)于的16進(jìn)制為186A0。所以我們改造后的哈希算法為:

再次調(diào)用后可以看到,計(jì)算后的哈希值都在10萬(wàn)以內(nèi)。但是分布極不均勻,說明修改數(shù)據(jù)后這個(gè)哈希算法不是一個(gè)優(yōu)秀的哈希算法:

以上,就是對(duì)一致性哈希算法在Dubbo中的實(shí)現(xiàn)的解讀。需要特殊說明一下的是,和上周分享的最小活躍數(shù)負(fù)載均衡算法不同,一致性哈希負(fù)載均衡策略和權(quán)重沒有任何關(guān)系。

我又發(fā)現(xiàn)了一個(gè)BUG

在上篇文章中,我介紹了Dubbo 2.6.5版本之前,最小活躍數(shù)算法的兩個(gè)bug。很不幸,這次我又發(fā)現(xiàn)了Dubbo 2.7.4.1版本,一致性哈希負(fù)載均衡策略的一個(gè)bug,我提交了issue,截止目前還未解決。

我在這里詳細(xì)說一下這個(gè)Bug現(xiàn)象、原因和我的解決方案。

現(xiàn)象如下,我們調(diào)用三次服務(wù)端:

輸出日志如下(有部分刪減):

可以看到,在三次調(diào)用的過程中并沒有發(fā)生服務(wù)的上下線操作,但是每一次調(diào)用都重新進(jìn)行了哈希環(huán)的映射。而我們預(yù)期的結(jié)果是應(yīng)該只有在第一次調(diào)用的時(shí)候進(jìn)行哈希環(huán)的映射,如果沒有服務(wù)上下線的操作,后續(xù)請(qǐng)求根據(jù)已經(jīng)映射好的哈希環(huán)進(jìn)行處理。

上面輸出的原因是由于每次調(diào)用的invokers的identityHashCode發(fā)生了變化:

我們看一下三次調(diào)用invokers的情況:

經(jīng)過debug我們可以看出因?yàn)槊看握{(diào)用的invokers地址值不是同一個(gè),所以System.identityHashCode(invokers)方法返回的值都不一樣。

接下來的問題就是為什么每次調(diào)用的invokers地址值都不一樣呢?

經(jīng)過Debug之后,可以找到這個(gè)地方:

org.apache.dubbo.rpc.cluster.RouterChain#route

問題就出在這個(gè)TagRouter中:

org.apache.dubbo.rpc.cluster.router.tag.TagRouter#filterInvoker

所以,在TagRouter中的stream操作,改變了invokers,導(dǎo)致每次調(diào)用時(shí)其

System.identityHashCode(invokers)返回的值不一樣。所以每次調(diào)用都會(huì)進(jìn)行哈希環(huán)的映射操作,在服務(wù)節(jié)點(diǎn)多,虛擬節(jié)點(diǎn)多的情況下會(huì)有一定的性能問題。

到這一步,問題又發(fā)生了變化。這個(gè)TagRouter怎么來的呢?

如果了解Dubbo 2.7.x版本新特性的朋友可能知道,標(biāo)簽路由是Dubbo2.7引入的新功能。

通過加載下面的配置加載了RouterFactrory:

META-INFdubbointernalorg.apache.dubbo.rpc.cluster.RouterFactory(Dubbo 2.7.0版本之前)

META-INFdubbointernalcom.alibaba.dubbo.rpc.cluster.RouterFactory(Dubbo 2.7.0之前)

下面是Dubbo 2.6.7(2.6.x的最后一個(gè)版本)和Dubbo 2.7.0版本該文件的對(duì)比:

可以看到確實(shí)是在Dubbo2.7.0之后引入了TagRouter。

至此,Dubbo 2.7.0版本之后,一致性哈希負(fù)載均衡算法的Bug的來龍去脈也介紹清楚了。目前該Bug還未解決。

解決方案是什么呢?特別簡(jiǎn)單,把獲取identityHashCode的方法從System.identityHashCode(invokers)修改為invokers.hashCode()即可。

此方案是我提的issue里面的評(píng)論,這里System.identityHashCode和 hashCode之間的聯(lián)系和區(qū)別就不進(jìn)行展開講述了,不清楚的大家可以自行了解一下。

改完之后,我們?cè)倏纯催\(yùn)行效果:

可以看到第二次調(diào)用的時(shí)候并沒有進(jìn)行哈希環(huán)的映射操作,而是直接取到了值,進(jìn)行調(diào)用。

加入節(jié)點(diǎn),畫圖分析

最后,我再分析一種情況。在A、B、C三個(gè)服務(wù)器(20881、20882、20883端口)都在正常運(yùn)行,哈希映射已經(jīng)完成的情況下,我們?cè)賳?dòng)一個(gè)D節(jié)點(diǎn)(20884端口),這時(shí)的日志輸出和對(duì)應(yīng)的哈希環(huán)變化情況如下:

根據(jù)日志作圖如下:

根據(jù)輸出日志和上圖再加上源碼,你再細(xì)細(xì)回味一下。我個(gè)人覺得還是講的非常詳細(xì)了,可能是東半球講一致性哈希算法在Dubbo中的實(shí)現(xiàn)最詳細(xì)的文章了。

一致性哈希的應(yīng)用場(chǎng)景

當(dāng)大家談到一致性哈希算法的時(shí)候,首先的第一印象應(yīng)該是在緩存場(chǎng)景下的使用,因?yàn)樵谝粋€(gè)優(yōu)秀的哈希算法加持下,其上下線節(jié)點(diǎn)對(duì)整體數(shù)據(jù)的影響(遷移)都是比較友好的。

但是想一下為什么Dubbo在負(fù)載均衡策略里面提供了基于一致性哈希的負(fù)載均衡策略?它的實(shí)際使用場(chǎng)景是什么?

我最開始也想不明白。我想的是在Dubbo的場(chǎng)景下,假設(shè)需求是想要一個(gè)用戶的請(qǐng)求一直讓一臺(tái)服務(wù)器處理,那我們可以采用一致性哈希負(fù)載均衡策略,把用戶號(hào)進(jìn)行哈希計(jì)算,可以實(shí)現(xiàn)這樣的需求。但是這樣的需求未免有點(diǎn)太牽強(qiáng)了,適用場(chǎng)景略小。

直到有天晚上,我睡覺之前,電光火石之間突然想到了一個(gè)稍微適用的場(chǎng)景了。

如果需求是需要保證某一類請(qǐng)求必須順序處理呢?

如果你用其他負(fù)載均衡策略,請(qǐng)求分發(fā)到了不同的機(jī)器上去,就很難保證請(qǐng)求的順序處理了。比如A,B請(qǐng)求要求順序處理,現(xiàn)在A請(qǐng)求先發(fā)送,被負(fù)載到了A服務(wù)器上,B請(qǐng)求后發(fā)送,被負(fù)載到了B服務(wù)器上。而B服務(wù)器由于性能好或者當(dāng)前沒有其他請(qǐng)求或者其他原因極有可能在A服務(wù)器還在處理A請(qǐng)求之前就把B請(qǐng)求處理完成了。這樣不符合我們的要求。

這時(shí),一致性哈希負(fù)載均衡策略就上場(chǎng)了,它幫我們保證了某一類請(qǐng)求都發(fā)送到固定的機(jī)器上去執(zhí)行。比如把同一個(gè)用戶的請(qǐng)求發(fā)送到同一臺(tái)機(jī)器上去執(zhí)行,就意味著把某一類請(qǐng)求發(fā)送到同一臺(tái)機(jī)器上去執(zhí)行。所以我們只需要在該機(jī)器上運(yùn)行的程序中保證順序執(zhí)行就行了,比如你加一個(gè)隊(duì)列。

一致性哈希算法+隊(duì)列,可以實(shí)現(xiàn)順序處理的需求。

最后說一句

這是Dubbo負(fù)載均衡算法的第二篇文章,上周寫了一篇《一文講透Dubbo負(fù)載均衡之最小活躍數(shù)算法》,也是非常詳細(xì),可以看看哦。

才疏學(xué)淺,難免會(huì)有紕漏,如果你發(fā)現(xiàn)了錯(cuò)誤的地方,還請(qǐng)你留言給我指出來,我對(duì)其加以修改。

如果你覺得文章還不錯(cuò),你的轉(zhuǎn)發(fā)、分享、贊賞、點(diǎn)贊、留言就是對(duì)我最大的鼓勵(lì)。

感謝您的閱讀,我的訂閱號(hào)里全是原創(chuàng),十分歡迎并感謝您的關(guān)注。

以上。

原創(chuàng)不易,歡迎轉(zhuǎn)發(fā),求個(gè)關(guān)注,賞個(gè)"在看"吧。

總結(jié)

以上是生活随笔為你收集整理的java 一致性hash算法 均衡分发_Dubbo一致性哈希负载均衡的源码和Bug,了解一下?...的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。