背景
Dubbo是一個分布式服務框架,能避免單點故障和支持服務的橫向擴容。一個服務通常會部署多個實例。如何從多個服務 Provider 組成的集群中挑選出一個進行調用,就涉及到一個負載均衡的策略。
幾個概念
在討論負載均衡之前,我想先解釋一下這3個概念。
- 負載均衡
- 集群容錯
- 服務路由
這3個概念容易混淆。他們都描述了怎么從多個 Provider 中選擇一個來進行調用。那他們到底有什么區別呢?下面我來舉一個簡單的例子,把這幾個概念闡述清楚吧。
有一個Dubbo的用戶服務,在北京部署了10個,在上海部署了20個。一個杭州的服務消費方發起了一次調用,然后發生了以下的事情:
- 根據配置的路由規則,如果杭州發起的調用,會路由到比較近的上海的20個 Provider。
- 根據配置的隨機負載均衡策略,在20個 Provider 中隨機選擇了一個來調用,假設隨機到了第7個 Provider。
- 結果調用第7個 Provider 失敗了。
- 根據配置的Failover集群容錯模式,重試其他服務器。
- 重試了第13個 Provider,調用成功。
上面的第1,2,4步驟就分別對應了路由,負載均衡和集群容錯。 Dubbo中,先通過路由,從多個 Provider 中按照路由規則,選出一個子集。再根據負載均衡從子集中選出一個 Provider 進行本次調用。如果調用失敗了,根據集群容錯策略,進行重試或定時重發或快速失敗等。 可以看到Dubbo中的路由,負載均衡和集群容錯發生在一次RPC調用的不同階段。最先是路由,然后是負載均衡,最后是集群容錯。 本文檔只討論負載均衡,路由和集群容錯在其他的文檔中進行說明。
Dubbo內置負載均衡策略
Dubbo內置了4種負載均衡策略:
- RandomLoadBalance:隨機負載均衡。隨機的選擇一個。是Dubbo的默認負載均衡策略。
- RoundRobinLoadBalance:輪詢負載均衡。輪詢選擇一個。
- LeastActiveLoadBalance:最少活躍調用數,相同活躍數的隨機。活躍數指調用前后計數差。使慢的 Provider 收到更少請求,因為越慢的 Provider 的調用前后計數差會越大。
- ConsistentHashLoadBalance:一致性哈希負載均衡。相同參數的請求總是落在同一台機器上。
1.隨機負載均衡
顧名思義,隨機負載均衡策略就是從多個 Provider 中隨機選擇一個。但是 Dubbo 中的隨機負載均衡有一個權重的概念,即按照權重設置隨機概率。比如說,有10個 Provider,並不是說,每個 Provider 的概率都是一樣的,而是要結合這10個 Provider 的權重來分配概率。
Dubbo中,可以對 Provider 設置權重。比如機器性能好的,可以設置大一點的權重,性能差的,可以設置小一點的權重。權重會對負載均衡產生影響。可以在Dubbo Admin中對 Provider 進行權重的設置。
基於權重的負載均衡算法
隨機策略會先判斷所有的 Invoker 的權重是不是一樣的,如果都是一樣的,那么處理就比較簡單了。使用random.nexInt(length)就可以隨機生成一個 Invoker 的序號,根據序號選擇對應的 Invoker 。如果沒有在Dubbo Admin中對服務 Provider 設置權重,那么所有的 Invoker 的權重就是一樣的,默認是100。 如果權重不一樣,那就需要結合權重來設置隨機概率了。算法大概如下: 假如有4個 Invoker。
invoker | weight |
---|---|
A | 10 |
B | 20 |
C | 20 |
D | 30 |
A,B,C和D總的權重是10 + 20 + 20 + 30 = 80。將80個數分布在如下的圖中:
+-----------------------------------------------------------------------------------+
| | | | |
+-----------------------------------------------------------------------------------+
1 10 30 50 80
|-----A----|---------B----------|----------C---------|---------------D--------------|
---------------------15
-------------------------------------------37
-----------------------------------------------------------54
上面的圖中一共有4塊區域,長度分別是A,B,C和D的權重。使用random.nextInt(10 + 20 + 20 + 30),從80個數中隨機選擇一個。然后再判斷該數分布在哪個區域。比如,如果隨機到37,37是分布在C區域的,那么就選擇 Invoker C。15是在B區域,54是在D區域。
隨機負載均衡源碼
(有權重:在權重和的范圍內生成一個隨機數,遍歷invoker,用權重和循環減去invoker的權重,結果小於0時的invoker被選中)
下面是隨機負載均衡的源碼,為了方便閱讀和理解,我把無關部分都去掉了。
public class RandomLoadBalance extends AbstractLoadBalance {
private final Random random = new Random();
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // Invoker 總數
int totalWeight = 0; // 所有 Invoker 的權重的和
// 判斷是不是所有的 Invoker 的權重都是一樣的
// 如果權重都一樣,就簡單了。直接用Random生成索引就可以了。
boolean sameWeight = true;
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
totalWeight += weight; // Sum
if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false;
}
}
if (totalWeight > 0 && !sameWeight) {
// 如果不是所有的 Invoker 權重都相同,那么基於權重來隨機選擇。權重越大的,被選中的概率越大
int offset = random.nextInt(totalWeight);
for (int i = 0; i < length; i++) {
offset -= getWeight(invokers.get(i), invocation);
if (offset < 0) {
return invokers.get(i);
}
}
}
// 如果所有 Invoker 權重相同
return invokers.get(random.nextInt(length));
}
}
2.輪詢負載均衡
輪詢負載均衡,就是依次的調用所有的 Provider。和隨機負載均衡策略一樣,輪詢負載均衡策略也有權重的概念。 輪詢負載均衡算法可以讓RPC調用嚴格按照我們設置的比例來分配。不管是少量的調用還是大量的調用。但是輪詢負載均衡算法也有不足的地方,存在慢的 Provider 累積請求的問題,比如:第二台機器很慢,但沒掛,當請求調到第二台時就卡在那,久而久之,所有請求都卡在調到第二台上,導致整個系統變慢。
3.最少活躍調用數負載均衡
官方解釋:
最少活躍調用數,相同活躍數的隨機,活躍數指調用前后計數差,使慢的機器收到更少。
這個解釋好像說的不是太明白。目的是讓更慢的機器收到更少的請求,但具體怎么實現的還是不太清楚。舉個例子:每個服務維護一個活躍數計數器。當A機器開始處理請求,該計數器加1,此時A還未處理完成。若處理完畢則計數器減1。而B機器接受到請求后很快處理完畢。那么A,B的活躍數分別是1,0。當又產生了一個新的請求,則選擇B機器去執行(B活躍數最小),這樣使慢的機器A收到少的請求。
處理一個新的請求時,Consumer 會檢查所有 Provider 的活躍數,如果具有最小活躍數的 Invoker 只有一個,直接返回該 Invoker:
if (leastCount == 1) {
// 如果只有一個最小則直接返回
return invokers.get(leastIndexs[0]);
}
如果最小活躍數的 Invoker 有多個,且權重不相等同時總權重大於0,這時隨機生成一個權重,范圍在 (0,totalWeight) 間內。最后根據隨機生成的權重,來選擇 Invoker。
if (! sameWeight && totalWeight > 0) {
// 如果權重不相同且權重大於0則按總權重數隨機
int offsetWeight = random.nextInt(totalWeight);
// 並確定隨機值落在哪個片斷上
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexs[i];
offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
if (offsetWeight <= 0)
return invokers.get(leastIndex);
}
}
4.一致性Hash算法
概述:
如果采用常用的hash(object)%N算法,那么在有機器添加或者刪除后,映射關系就變了,很多原有的緩存就無法找到了
一致性hash:添加刪除機器前后映射關系一致,當然,不是嚴格一致。實現的關鍵是環形Hash空間。將數據和機器都hash到環上,數據映射到順時針離自己最近的機器中。
一致性hash單調性體現在:
無論是新增主機還是刪除主機,被影響的都是離那台主機最近的那些節點,其他節點映射關系沒有影響
使用一致性 Hash 算法,讓相同參數的請求總是發到同一 Provider。 當某一台 Provider 崩潰時,原本發往該 Provider 的請求,基於虛擬節點,平攤到其它 Provider,不會引起劇烈變動。 參見我另一篇:https://www.cnblogs.com/twoheads/p/10135896.html
缺省只對第一個參數Hash,如果要修改,請配置:
<dubbo:parameter key="hash.arguments" value="0,1" />
缺省用160份虛擬節點,如果要修改,請配置:
<dubbo:parameter key="hash.nodes" value="320" />
優點:一致性Hash算法可以和緩存機制配合起來使用。比如有一個服務getUserInfo(String userId)。設置了Hash算法后,相同的userId的調用,都會發送到同一個 Provider。這個 Provider 上可以把用戶數據在內存中進行緩存,減少訪問數據庫或分布式緩存的次數。如果業務上允許這部分數據有一段時間的不一致,可以考慮這種做法。減少對數據庫,緩存等中間件的依賴和訪問次數,同時減少了網絡IO操作,提高系統性能。
負載均衡配置
如果不指定負載均衡,默認使用隨機負載均衡。我們也可以根據自己的需要,顯式指定一個負載均衡。 可以在多個地方類來配置負載均衡,比如 Provider 端,Consumer端,服務級別,方法級別等。
服務端服務級別
<dubbo:service interface="..." loadbalance="roundrobin" />
該服務的所有方法都使用roundrobin負載均衡。
客戶端服務級別
<dubbo:reference interface="..." loadbalance="roundrobin" />
該服務的所有方法都使用roundrobin負載均衡。
服務端方法級別
<dubbo:service interface="...">
<dubbo:method name="hello" loadbalance="roundrobin"/>
</dubbo:service>
只有該服務的hello方法使用roundrobin負載均衡。
客戶端方法級別
<dubbo:reference interface="...">
<dubbo:method name="hello" loadbalance="roundrobin"/>
</dubbo:reference>
只有該服務的hello方法使用roundrobin負載均衡。
和Dubbo其他的配置類似,多個配置是有覆蓋關系的:
- 方法級優先,接口級次之,全局配置再次之。
- 如果級別一樣,則消費方優先,提供方次之。
所以,上面4種配置的優先級是:
- 客戶端方法級別配置。
- 客戶端接口級別配置。
- 服務端方法級別配置。
- 服務端接口級別配置。
擴展負載均衡
Dubbo的4種負載均衡的實現,大多數情況下能滿足要求。有時候,因為業務的需要,我們可能需要實現自己的負載均衡策略。本章只說明如何配置負載均衡算法。關於Dubbo擴展機制的更多內容,請前往Dubbo可擴展機制實戰。
- 實現LoadBalance接口
略
轉自dubbo官網:
http://dubbo.apache.org/zh-cn/blog/dubbo-loadbalance.html