本文是對於Dubbo負載均衡策略之一的一致性哈希負載均衡的詳細分析。對源碼逐行解讀、根據實際運行結果,配以豐富的圖片,多是東半球講一致性哈希算法在Dubbo中的實現最詳細的文章了。java
文中所示源碼,沒有特別標註的地方,均爲2.7.4.1版本。node
在撰寫本文的過程當中,發現了Dubbo2.7.0版本以後的一個bug。會致使性能問題,且目前還未解決,若是大家的負載均衡配置的是一致性哈希或者考慮使用一致性哈希的話,能夠了解一下。git
第一節:哈希算法github
本小節主要是爲了介紹一致性哈希算法作鋪墊。簡單的介紹了哈希算法解決了什麼問題,帶來了什麼問題。面試
第二節:一致性哈希算法算法
本小節主要是經過做圖對一致性哈希進行了簡單的介紹。介紹了一致性哈希是怎麼解決哈希算法帶來的問題,怎麼解決數據傾斜的問題。apache
第三節:一致性哈希算法在Dubbo中的應用數組
本小節是全文重點,經過一個"騷"操做,對Dubbo一致性哈希算法的源碼進行了十分詳細的剖析。從整個類到類裏面的每一個方法進行了詳盡的分析,打印了大量的日誌,配合圖片,方便讀者理解。緩存
第四節:我又發現了一個Bug服務器
本小節主要是介紹我在研究Dubbo一致性哈希負載均衡時遇到的一個問題,深刻研究以後發現多是一個Bug。這一小節就是比較詳盡的介紹了這個Bug現象、緣由以及個人解決方案。
第五節:加入節點,畫圖分析
本小節對具體的案例進行了分析,並配以圖片,相信能幫助讀者更加深入的理解一致性哈希算法在Dubbo中的應用。
第六節:一致性哈希的應用場景
本小節主要介紹幾個應用場景。使用Duboo框架,在什麼樣的需求可使用一致性哈希算法作負載均衡。
PS:前1、二節主要是進行了背景知識的簡單鋪墊,若是你瞭解相關背景知識,能夠直接從第三節看起。本文的重點是第三到第五節。若是你只想知道Bug是什麼,能夠直接閱讀第四節。
另:閱讀本文須要對Dubbo有必定的瞭解。文章很長,建議收藏慢慢閱讀。必定會有收穫的。
在介紹一致性哈希算法以前,咱們看看哈希算法,以及它解決了什麼問題,帶來了什麼問題。
如上圖所示,假設0,1,2號服務器都存儲的有用戶信息,那麼當咱們須要獲取某用戶信息時,由於咱們不知道該用戶信息存放在哪一臺服務器中,因此須要分別查詢0,1,2號服務器。這樣獲取數據的效率是極低的。
對於這樣的場景,咱們能夠引入哈希算法。
仍是上面的場景,但前提是每一臺服務器存放用戶信息時是根據某一種哈希算法存放的。因此取用戶信息的時候,也按照一樣的哈希算法取便可。
假設咱們要查詢用戶號爲100的用戶信息,通過某個哈希算法,好比這裏的userId mod n,即100 mod 3結果爲1。因此用戶號100的這個請求最終會被1號服務器接收並處理。
這樣就解決了無效查詢的問題。
可是這樣的方案會帶來什麼問題呢?
擴容或者縮容時,會致使大量的數據遷移。最少也會影響百分之50的數據。
爲了說明問題,咱們加入一臺服務器3。服務器的數量n就從3變成了4。仍是查詢用戶號爲100的用戶信息時,100 mod 4結果爲0。這時,請求就被0號服務器接收了。
當服務器數量爲3時,用戶號爲100的請求會被1號服務器處理。
當服務器數量爲4時,用戶號爲100的請求會被0號服務器處理。
因此,當服務器數量增長或者減小時,必定會涉及到大量數據遷移的問題。可謂是牽一髮而動全身。
對於上述哈希算法其優勢是簡單易用,大多數分庫分表規則就採起的這種方式。通常是提早根據數據量,預先估算好分區數。
其缺點是因爲擴容或收縮節點致使節點數量變化時,節點的映射關係須要從新計算,會致使數據進行遷移。因此擴容時一般採用翻倍擴容,避免數據映射所有被打亂,致使全量遷移的狀況,這樣只會發生50%的數據遷移。
假設這是一個緩存服務,數據的遷移會致使在遷移的時間段內,有緩存是失效的。緩存失效,可怕啊。還記得我以前的文章嗎,《當週杰倫把QQ音樂幹翻的時候,做爲程序猿我看到了什麼?》就是講緩存擊穿、緩存穿透、緩存雪崩的場景和對應的解決方案。
爲了解決哈希算法帶來的數據遷移問題,一致性哈希算法應運而生。
對於一致性哈希算法,官方說法以下:
一致性哈希算法在1997年由麻省理工學院提出,是一種特殊的哈希算法,在移除或者添加一個服務器時,可以儘量小地改變已存在的服務請求與處理請求服務器之間的映射關係。一致性哈希解決了簡單哈希算法在分佈式哈希表( Distributed Hash Table,DHT) 中存在的動態伸縮等問題。
什麼意思呢?我用大白話加畫圖的方式給你簡單的介紹一下。
一致性哈希,你能夠想象成一個哈希環,它由0到2^32-1個點組成。A,B,C分別是三臺服務器,每一臺的IP加端口通過哈希計算後的值,在哈希環上對應以下:
當請求到來時,對請求中的某些參數進行哈希計算後,也會得出一個哈希值,此值在哈希環上也會有對應的位置,這個請求會沿着順時針的方向,尋找最近的服務器來處理它,以下圖所示:
一致性哈希就是這麼個東西。那它是怎麼解決服務器的擴容或收縮致使大量的數據遷移的呢?
看一下當咱們使用一致性哈希算法時,加入服務器會發什麼事情。
當咱們加入一個D服務器後,假設其IP加端口,通過哈希計算後落在了哈希環上圖中所示的位置。
這時影響的範圍只有圖中標註了五角星的區間。這個區間的請求從原來的由C服務器處理變成了由D服務器請求。而D到C,C到A,A到B這個區間的請求沒有影響,加入D節點後,A、B服務器是無感知的。
因此,在一致性哈希算法中,若是增長一臺服務器,則受影響的區間僅僅是新服務器(D)在哈希環空間中,逆時針方向遇到的第一臺服務器(B)之間的區間,其它區間(D到C,C到A,A到B)不會受到影響。
在加入了D服務器的狀況下,咱們再假設一段時間後,C服務器宕機了:
當C服務器宕機後,影響的範圍也是圖中標註了五角星的區間。C節點宕機後,B、D服務器是無感知的。
因此,在一致性哈希算法中,若是宕機一臺服務器,則受影響的區間僅僅是宕機服務器(C)在哈希環空間中,逆時針方向遇到的第一臺服務器(D)之間的區間,其它區間(C到A,A到B,B到D)不會受到影響。
綜上所述,在一致性哈希算法中,無論是增長節點,仍是宕機節點,受影響的區間僅僅是增長或者宕機服務器在哈希環空間中,逆時針方向遇到的第一臺服務器之間的區間,其它區間不會受到影響。
是否是很完美?
不是的,理想和現實的差距是巨大的。
一致性哈希算法帶來了什麼問題?
當節點不多的時候可能會出現這樣的分佈狀況,A服務會承擔大部分請求。這種狀況就叫作數據傾斜。
怎麼解決數據傾斜呢?加入虛擬節點。
怎麼去理解這個虛擬節點呢?
首先一個服務器根據須要能夠有多個虛擬節點。假設一臺服務器有n個虛擬節點。那麼哈希計算時,可使用IP+端口+編號的形式進行哈希值計算。其中的編號就是0到n的數字。因爲IP+端口是同樣的,因此這n個節點都是指向的同一臺機器。
以下圖所示:
在沒有加入虛擬節點以前,A服務器承擔了絕大多數的請求。可是假設每一個服務器有一個虛擬節點(A-1,B-1,C-1),通過哈希計算後落在瞭如上圖所示的位置。那麼A服務器的承擔的請求就在必定程度上(圖中標註了五角星的部分)分攤給了B-一、C-1虛擬節點,實際上就是分攤給了B、C服務器。
一致性哈希算法中,加入虛擬節點,能夠解決數據傾斜問題。
當你在面試的過程當中,若是聽到了相似於數據傾斜的字眼。那大機率是在問你一致性哈希算法和虛擬節點。
在介紹了相關背景後,咱們能夠去看看一致性哈希算法在Dubbo中的應用了。
通過《一文講透Dubbo負載均衡之最小活躍數算法》這篇文章咱們知道Dubbo中負載均衡的實現是經過org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance中的doSelect抽象方法實現的,一致性哈希負載均衡的實現類以下所示: org.apache.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
因爲一致性哈希實現類看起來稍微有點抽象,不太好演示,因此我想到了一個"騷"操做。前面的文章說過LoadBalance是一個SPI接口:
既然是一個SPI接口,那咱們能夠本身擴展一個如出一轍的算法,只是在算法裏面加入一點輸出語句方便咱們觀察狀況。怎麼擴展SPI接口就不描述了,只要記住代碼裏面的輸出語句都是額外加的,此外沒有任何改動便可,以下:
整個類以下圖片所示,請先看完整個類,有一個總體的概念後,我會進行方法級別的分析。
圖片很長,其中我加了不少註釋和輸出語句,能夠點開大圖查看,必定會幫你更加好的理解一致性哈希在Dubbo中的應用:
把代碼也貼在這裏
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<String, WhyConsistentHashLoadBalance.ConsistentHashSelector<?>> selectors =
new ConcurrentHashMap<String, WhyConsistentHashLoadBalance.ConsistentHashSelector<?>>();
@SuppressWarnings("unchecked")
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> 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<T> selector =
(WhyConsistentHashLoadBalance.ConsistentHashSelector<T>) selectors.get(key);
//若是invokers是一個新的List對象,意味着服務提供者數量發生了變化,可能新增也可能減小了。
//此時selector.identityHashCode!=identityHashCode條件成立
//若是是第一次調用此時selector == null條件成立
if (selector == null || selector.identityHashCode != identityHashCode) {
System.out.println("是新的invokers:" + identityHashCode + ",原:" + (selector == null ? "null" : selector.identityHashCode));
//建立新的ConsistentHashSelector
selectors.put(key, new WhyConsistentHashLoadBalance.ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
selector = (WhyConsistentHashLoadBalance.ConsistentHashSelector<T>) selectors.get(key);
System.out.println("哈希環構建完成,詳情以下:");
for (Map.Entry<Long, Invoker<T>> entry : selector.virtualInvokers.entrySet()) {
System.out.println("key(哈希值)=" + entry.getKey() + ",value(虛擬節點)=" + entry.getValue());
}
}
//調用ConsistentHashSelector的select方法選擇Invoker
System.out.println("開始調用ConsistentHashSelector的select方法選擇Invoker");
return selector.select(invocation);
}
private static final class ConsistentHashSelector<T> {
//使用TreeMap存儲Invoker的虛擬節點
private final TreeMap<Long, Invoker<T>> virtualInvokers;
//虛擬節點數
private final int replicaNumber;
//hashCode
private final int identityHashCode;
//請求中的參數下標。
//須要對請求中對應下標的參數進行哈希計算
private final int[] argumentIndex;
ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
this.identityHashCode = identityHashCode;
URL url = invokers.get(0).getUrl();
System.out.println("CHS中url爲=" + url);
//即便啓動多個invoker,每一個invoker對應的url上的虛擬節點數配置的都是同樣的
//這裏默認是160個。本文中的示例代碼設置爲4個。
this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
//全部輸出語句都是我加的,CHS是ConsistentHashSelector的縮寫
System.out.println("CHS中url上的【hash.nodes】爲=" + replicaNumber);
//獲取參與哈希計算的參數下標值,默認對第一個參數進行哈希運算
//本文中的示例代碼使用默認配置,因此這裏的index長度爲1。
String[] index = COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, HASH_ARGUMENTS, "0"));
System.out.println("CHS中url上的【hash.arguments】爲=" + Arrays.toString(index));
//for循環,對argumentIndex進行賦值操做。
argumentIndex = new int[index.length];
for (int i = 0; i < index.length; i++) {
argumentIndex[i] = Integer.parseInt(index[i]);
}
System.out.println("CHS中argumentIndex數組爲=" + Arrays.toString(argumentIndex));
//本文中啓動了2個服務提供者,因此invokers=2
for (Invoker<T> invoker : invokers) {
//獲取每一個invoker的地址
String address = invoker.getUrl().getAddress();
System.out.println("CHS中invoker的地址爲=" + address);
for (int i = 0; i < replicaNumber / 4; i++) {
//對address+i進行md5運算獲得一個長度爲16的字節數組
byte[] digest = md5(address + i);
System.out.println("CHS中對" + address + i + "進行md5計算");
//對digest部分字節進行4次hash運算獲得四個不一樣的long型正整數
for (int h = 0; h < 4; h++) {
//h=0時,取digest中下標爲0~3的4個字節進行位運算
//h=1時,取digest中下標爲4~7的4個字節進行位運算
//h=2,h=3時過程同上
long m = hash(digest, h);
System.out.println("CHS中對digest進行第"+h+"次hash計算後的值:"+m+",當前invoker="+invoker);
//將hash到invoker的映射關係存儲到virtualInvokers中,
//virtualInvokers須要提供高效的查詢操做,所以選用TreeMap做爲存儲結構
virtualInvokers.put(m, invoker);
}
}
}
}
public Invoker<T> select(Invocation invocation) {
String key = toKey(invocation.getArguments());
System.out.println("CHS的select方法根據argumentIndex取出invocation中參與hash計算的key="+key);
byte[] digest = md5(key);
//取digest數組的前四個字節進行hash運算,再將hash值傳給selectForKey方法,
//尋找合適的Invoker
long hash = hash(digest, 0);
System.out.println("CHS的select方法中key=" + key + "通過哈希計算後hash=" + hash);
return selectForKey(hash);
}
//根據argumentIndex將參數轉化爲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<T> selectForKey(long hash) {
//到TreeMap中查找第一個節點值大於或等於當前hash的Invoker
Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
//若是hash大於Invoker在圓環上最大的位置,此時entry=null,
//須要將TreeMap的頭節點賦值給entry
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
System.out.println("CHS的selectForKey方法根據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();
}
}
}
複製代碼
改造以後,咱們先把程序跑起來,有了輸出就好分析了。
服務端代碼以下:
其中的端口是須要手動修改的,我分別啓動服務在20881和20882端口。
項目中provider.xml配置以下:
consumer.xml配置以下:
而後,啓動在20881和20882端口分別啓動兩個服務端。客戶端消費以下:
運行結果輸出以下,能夠先看個大概的輸出,下面會對每一部分輸出進行逐一的解讀。
好了,用例也跑起來了,日誌也有了。接下來開始結合代碼和日誌進行方法級別的分析。
首先是doSelect方法的入口:
從上圖咱們知道了,第一次調用須要對selectors進行put操做,selectors的key是接口中定義的方法,value是ConsistentHashSelector內部類。
ConsistentHashSelector經過調用其構造函數進行初始化的。invokers(服務端)做爲參數傳遞到了構造函數中,構造函數裏面的邏輯,就是把服務端映射到哈希環上的過程,請看下圖,結合代碼,仔細分析輸出數據:
從上圖能夠看出,當ConsistentHashSelector的構造方法調用完成後,8個虛擬節點在哈希環上已經映射完成。兩臺服務器,每一臺4個虛擬節點組成了這8個虛擬節點。
doSelect方法繼續執行,並打印出每一個虛擬節點的哈希值和對應的服務端,請仔細品讀下圖:
說明一下:上面圖中的哈希環是沒有考慮比例的,僅僅是展示了兩個服務器在哈希環上的相對位置。並且爲了演示說明方便,僅僅只有8個節點。假設咱們有4臺服務器,每臺服務器的虛擬節點是默認值(160),這個狀況下哈希環上一共有160*4=640個節點。
哈希環映射完成後,接下來的邏輯是把此次請求通過哈希計算後,映射到哈希環上,並順時針方向尋找遇到的第一個節點,讓該節點處理該請求:
還記得地址爲468e8565的A服務器是什麼端口嗎?前面的圖片中有哦,該服務對應的端口是20882。
最後咱們看看輸出結果:
和咱們預期的一致。整個調用就算是完成了。
再對兩個方法進行一個補充說明。
第一個方法是selectForKey,這個方法裏面邏輯以下圖所示:
虛擬節點都存儲在TreeMap中。順時針查詢的邏輯由TreeMap保證。看一下下面的Demo你就明白了。
第二個方法是hash方法,其中的& 0xFFFFFFFFL的目的以下:
&是位運算符,而0xFFFFFFFFL轉換爲四字節表現後,其低32位全是1,因此保證了哈希環的範圍是[0,Integer.MAX_VALUE]:
因此這裏咱們能夠改造這個哈希環的範圍,假設咱們改成100000。十進制的100000對於的16進製爲186A0。因此咱們改造後的哈希算法爲:
再次調用後能夠看到,計算後的哈希值都在10萬之內。可是分佈極不均勻,說明修改數據後這個哈希算法不是一個優秀的哈希算法:
以上,就是對一致性哈希算法在Dubbo中的實現的解讀。須要特殊說明一下的是,和上週分享的最小活躍數負載均衡算法不一樣,一致性哈希負載均衡策略和權重沒有任何關係。
在上篇文章中,我介紹了Dubbo 2.6.5版本以前,最小活躍數算法的兩個bug。很不幸,此次我又發現了Dubbo 2.7.4.1版本,一致性哈希負載均衡策略的一個bug,我提交了issue,截止目前還未解決。
issue地址以下: github.com/apache/dubb…
我在這裏詳細說一下這個Bug現象、緣由和個人解決方案。
現象以下,咱們調用三次服務端:
輸出日誌以下(有部分刪減):
能夠看到,在三次調用的過程當中並無發生服務的上下線操做,可是每一次調用都從新進行了哈希環的映射。而咱們預期的結果是應該只有在第一次調用的時候進行哈希環的映射,若是沒有服務上下線的操做,後續請求根據已經映射好的哈希環進行處理。
上面輸出的緣由是因爲每次調用的invokers的identityHashCode發生了變化:
咱們看一下三次調用invokers的狀況:
通過debug咱們能夠看出由於每次調用的invokers地址值不是同一個,因此System.identityHashCode(invokers)方法返回的值都不同。
接下來的問題就是爲何每次調用的invokers地址值都不同呢?
通過Debug以後,能夠找到這個地方: org.apache.dubbo.rpc.cluster.RouterChain#route
問題就出在這個TagRouter中: org.apache.dubbo.rpc.cluster.router.tag.TagRouter#filterInvoker
因此,在TagRouter中的stream操做,改變了invokers,致使每次調用時其 System.identityHashCode(invokers)返回的值不同。因此每次調用都會進行哈希環的映射操做,在服務節點多,虛擬節點多的狀況下會有必定的性能問題。
到這一步,問題又發生了變化。這個TagRouter怎麼來的呢?
若是瞭解Dubbo 2.7.x版本新特性的朋友可能知道,標籤路由是Dubbo2.7引入的新功能。
經過加載下面的配置加載了RouterFactrory:
META-INF\dubbo\internal\org.apache.dubbo.rpc.cluster.RouterFactory(Dubbo 2.7.0版本以前)
META-INF\dubbo\internal\com.alibaba.dubbo.rpc.cluster.RouterFactory(Dubbo 2.7.0以前)
下面是Dubbo 2.6.7(2.6.x的最後一個版本)和Dubbo 2.7.0版本該文件的對比:
能夠看到確實是在Dubbo2.7.0以後引入了TagRouter。
至此,Dubbo 2.7.0版本以後,一致性哈希負載均衡算法的Bug的前因後果也介紹清楚了。目前該Bug還未解決。
解決方案是什麼呢?特別簡單,把獲取identityHashCode的方法從System.identityHashCode(invokers)修改成invokers.hashCode()便可。
此方案是我提的issue裏面的評論,這裏System.identityHashCode和 hashCode之間的聯繫和區別就不進行展開講述了,不清楚的你們能夠自行了解一下。
改完以後,咱們再看看運行效果:
能夠看到第二次調用的時候並無進行哈希環的映射操做,而是直接取到了值,進行調用。
最後,我再分析一種狀況。在A、B、C三個服務器(2088一、2088二、20883端口)都在正常運行,哈希映射已經完成的狀況下,咱們再啓動一個D節點(20884端口),這時的日誌輸出和對應的哈希環變化狀況以下:
根據日誌做圖以下:
根據輸出日誌和上圖再加上源碼,你再細細回味一下。我我的以爲仍是講的很是詳細了,多是東半球講一致性哈希算法在Dubbo中的實現最詳細的文章了。
當你們談到一致性哈希算法的時候,首先的第一印象應該是在緩存場景下的使用,由於在一個優秀的哈希算法加持下,其上下線節點對總體數據的影響(遷移)都是比較友好的。
可是想一下爲何Dubbo在負載均衡策略裏面提供了基於一致性哈希的負載均衡策略?它的實際使用場景是什麼?
我最開始也想不明白。我想的是在Dubbo的場景下,假設需求是想要一個用戶的請求一直讓一臺服務器處理,那咱們能夠採用一致性哈希負載均衡策略,把用戶號進行哈希計算,能夠實現這樣的需求。可是這樣的需求未免有點太牽強了,適用場景略小。
直到有天晚上,我睡覺以前,電光火石之間忽然想到了一個稍微適用的場景了。
若是需求是須要保證某一類請求必須順序處理呢?
若是你用其餘負載均衡策略,請求分發到了不一樣的機器上去,就很難保證請求的順序處理了。好比A,B請求要求順序處理,如今A請求先發送,被負載到了A服務器上,B請求後發送,被負載到了B服務器上。而B服務器因爲性能好或者當前沒有其餘請求或者其餘緣由極有可能在A服務器還在處理A請求以前就把B請求處理完成了。這樣不符合咱們的要求。
這時,一致性哈希負載均衡策略就上場了,它幫咱們保證了某一類請求都發送到固定的機器上去執行。好比把同一個用戶的請求發送到同一臺機器上去執行,就意味着把某一類請求發送到同一臺機器上去執行。因此咱們只須要在該機器上運行的程序中保證順序執行就好了,好比你加一個隊列。
一致性哈希算法+隊列,能夠實現順序處理的需求。
這是Dubbo負載均衡算法的第二篇文章,上週寫了一篇《一文講透Dubbo負載均衡之最小活躍數算法》,也是很是詳細,能夠看看哦。
才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。
若是你以爲文章還不錯,你的轉發、分享、讚揚、點贊、留言就是對我最大的鼓勵。
感謝您的閱讀,個人訂閱號裏全是原創,十分歡迎並感謝您的關注。
以上。
原創不易,歡迎轉發,求個關注,賞個"在看"吧。