dubbo負載均衡策略(面試問題:dubbo負載均衡是怎麼配置的)
負載均衡演算法
在叢集負載均衡時,Dubbo提供了4種均衡策略,如:Random LoadBalance(隨機均衡演算法)、;RoundRobin LoadBalance(權重輪循均衡演算法)、LeastAction LoadBalance(最少活躍呼叫數均衡演算法)、ConsistentHash LoadBalance(一致性Hash均衡演算法)。預設時為Random隨機呼叫。這四種演算法的原理簡要介紹如下:
1、RoundRobin LoadBalance
Round-Robin既是輪詢演算法,是按照公約後的權重設定輪詢比率,即權重輪詢演算法(Weighted Round-Robin)
輪詢排程演算法的原理是:每一次把來自使用者的請求輪流分配給內部中的伺服器。如:從1開始,一直到N(其中,N是內部伺服器的總個數),然後重新開始迴圈。
該演算法的優點:
其簡潔性,它無需記錄當前所有連線的狀態,所以它是一種無狀態排程。
該演算法的缺點:
輪詢排程演算法假設所有伺服器的處理效能都相同,不關心每臺伺服器的當前連線數和響應速度。當請求服務間隔時間變化比較大時,輪詢排程演算法容易導致伺服器間的負載不平衡。
所以此種均衡演算法適合於伺服器組中的所有伺服器都有相同的軟硬體配置並且平均服務請求相對均衡的情況。但是,在實際情況中,可能並不是這種情況。由於每臺伺服器的配置、安裝的業務應用等不同,其處理能力會不一樣。所以,我們根據伺服器的不同處理能力,給每個伺服器分配不同的權值,使其能夠接受相應權值數的服務請求。
權重輪詢排程演算法流程
假設有一組伺服器S = {S0, S1, …, Sn-1},W(Si)表示伺服器Si的權值,一個指示變數i表示上一次選擇的伺服器,指示變數cw表示當前排程的權值,max(S)表示集合S中所有伺服器的最大權值,gcd(S)表示集合S中所有伺服器權值的最大公約數。變數i初始化為-1,cw初始化為零。其演算法如下:
while (true) { i = (i + 1) mod n; if (i == 0) { cw = cw - gcd(S); if (cw <= 0) { cw = max(S); if (cw == 0) return NULL; } } if (W(Si) >= cw) return Si; } |
這種演算法的邏輯實現如圖2所示,圖中我們假定四臺伺服器的處理能力為3:1:1:1。
圖1 權重輪詢排程實現邏輯圖示
由於權重輪詢排程演算法考慮到了不同伺服器的處理能力,所以這種均衡演算法能確保高效能的伺服器得到更多的使用率,避免低效能的伺服器負載過重。所以,在實際應用中比較常見。
2、ConsistentHash LoadBalance
一致性Hash,相同引數的請求總是發到同一個提供者。一:一致性Hash演算法可以解決服務提供者的增加、移除及掛掉時的情況,能儘可能小的改變已存在key對映關係,儘可能的滿足單調性的要求。二:一致性Hash通過構建虛擬節點,能儘可能避免分配失衡,具有很好的平衡性。
一致性Hash下面就來按照5個步驟簡單講講consistent hash演算法的基本原理。因為以下資料來自於網際網路,現說明幾點:一、下面例子中的物件就相當於Client發的請求,cache相當於服務提供者。
環形hash 空間
考慮通常的hash演算法都是將value對映到一個32為的key值,也即是0~2^32-1次方的數值空間;我們可以將這個空間想象成一個首(0)尾(2^32-1)相接的圓環,如下面圖2所示的那樣。
圖2環形hash空間
把物件對映到hash 空間
接下來考慮4個物件object1~object4,通過hash函式計算出的hash值key在環上的分佈如圖3所示。
hash(object1) = key1;
… …
hash(object4) = key4;
圖3 4個物件的key值分佈
把cache 對映到hash空間
Consistent hashing的基本思想就是將物件和cache都對映到同一個hash數值空間中,並且使用相同的hash演算法。
假設當前有A,B和C共3臺cache,那麼其對映結果將如圖4所示,他們在hash空間中,以對應的hash值排列。
hash(cache A) = key A;
… …
hash(cache C) = key C;
圖4 cache和物件的key值分佈
說到這裡,順便提一下cache的hash計算,一般的方法可以使用cache機器的IP地址或者機器名作為hash輸入。
把物件對映到cache
現在cache和物件都已經通過同一個hash演算法對映到hash數值空間中了,接下來要考慮的就是如何將物件對映到cache上面了。
在這個環形空間中,如果沿著順時針方向從物件的key值出發,直到遇見一個cache,那麼就將該物件儲存在這個cache上,因為物件和cache的hash值是固定的,因此這個cache必然是唯一和確定的。這樣不就找到了物件和cache的對映方法了嗎!
依然繼續上面的例子(參見圖4),那麼根據上面的方法,物件object1將被儲存到cache A上;object2和object3對應到cache C;object4對應到cache B;
考察cache 的變動
前面講過,一致性Hash演算法可以解決服務提供者的增加、移除及掛掉時的情況,能儘可能小的改變已存在key對映關係,儘可能的滿足單調性的要求。
移除cache
考慮假設cache B掛掉了,根據上面講到的對映方法,這時受影響的將僅是那些沿cache B逆時針遍歷直到下一個cache(cache C)之間的物件,也即是本來對映到cache B上的那些物件。
因此這裡僅需要變動物件object4,將其重新對映到cache C上即可;參見圖5。
圖5 Cache B被移除後的cache對映
新增cache
再考慮新增一臺新的cache D的情況,假設在這個環形hash空間中,cache D被對映在物件object2和object3之間。這時受影響的將僅是那些沿cache D逆時針遍歷直到下一個cache(cache B)之間的物件(它們是也本來對映到cache C上物件的一部分),將這些物件重新對映到cache D上即可。
因此這裡僅需要變動物件object2,將其重新對映到cache D上;參見圖6。
圖6 新增cache D後的對映關係
虛擬節點
考慮Hash演算法的另一個指標是平衡性(Balance),定義如下:
平衡性是指雜湊的結果能夠儘可能分佈到所有的緩衝中去,這樣可以使得所有的緩衝空間都得到利用。
hash演算法並不是保證絕對的平衡,如果cache較少的話,物件並不能被均勻的對映到cache上,比如在上面的例子中,僅部署cache A和cache C的情況下,在4個物件中,cache A僅儲存了object1,而cache C則儲存了object2、object3和object4;分佈是很不均衡的。
為了解決這種情況,consistent hashing引入了“虛擬節點”的概念,它可以如下定義:
“虛擬節點”(virtual node)是實際節點在hash空間的複製品(replica),一實際個節點對應了若干個“虛擬節點”,這個對應個數也成為“複製個數”,“虛擬節點”在hash空間中以hash值排列。
仍以僅部署cache A和cache C的情況為例,在圖5中我們已經看到,cache分佈並不均勻。現在我們引入虛擬節點,並設定“複製個數”為2,這就意味著一共會存在4個“虛擬節點”,cache A1, cache A2代表了cache A;cache C1, cache C2代表了cache C;假設一種比較理想的情況,參見圖7。
圖7 引入“虛擬節點”後的對映關係
此時,物件到“虛擬節點”的對映關係為:
objec1->cache A2;objec2->cache A1;objec3->cache C1;objec4->cache C2;
因此物件object1和object2都被對映到了cache A上,而object3和object4對映到了cache C上;平衡性有了很大提高。
引入“虛擬節點”後,對映關係就從{物件->節點}轉換到了{物件->虛擬節點}。查詢物體所在cache時的對映關係如圖8所示。
圖8查詢物件所在cache
“虛擬節點”的hash計算可以採用對應節點的IP地址加數字字尾的方式。例如假設cache A的IP地址為202.168.14.241。
引入“虛擬節點”前,計算cache A的hash值:
Hash(“202.168.14.241”);
引入“虛擬節點”後,計算“虛擬節”點cache A1和cache A2的hash值:
Hash(“202.168.14.241#1”); // cache A1
Hash(“202.168.14.241#2”); // cache A2
3、Random LoadBalance與LeastAction LoadBalance
Random LoadBalance與LeastAction LoadBalance演算法比較簡單,可以參照Dubbo文件中的給的描述及後面程式碼附錄。Dubbo文件截圖如下圖9所示:
圖9 負載均衡演算法
4、附錄
1、RandomLoadBalance演算法
public class RandomLoadBalance extends AbstractLoadBalance {
public static final String NAME = "random";
private final Random random = new Random();
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // 總個數
int totalWeight = 0; // 總權重
boolean sameWeight = true; // 權重是否都一樣
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
totalWeight += weight; // 累計總權重
if (sameWeight && i > 0
&& weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false; // 計算所有權重是否一樣
}
}
if (totalWeight > 0 && ! sameWeight) {
// 如果權重不相同且權重大於0則按總權重數隨機
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);
}
}
}
// 如果權重相同或權重為0則均等隨機
return invokers.get(random.nextInt(length));
}
}
2、RoundRobinLoadBalance演算法
public class RoundRobinLoadBalance extends AbstractLoadBalance {
public static final String NAME = "roundrobin";
private final ConcurrentMap<String, AtomicPositiveInteger> sequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();
private final ConcurrentMap<String, AtomicPositiveInteger> weightSequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
int length = invokers.size(); // 總個數
int maxWeight = 0; // 最大權重
int minWeight = Integer.MAX_VALUE; // 最小權重
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
maxWeight = Math.max(maxWeight, weight); // 累計最大權重
minWeight = Math.min(minWeight, weight); // 累計最小權重
}
if (maxWeight > 0 && minWeight < maxWeight) { // 權重不一樣
AtomicPositiveInteger weightSequence = weightSequences.get(key);
if (weightSequence == null) {
weightSequences.putIfAbsent(key, new AtomicPositiveInteger());
weightSequence = weightSequences.get(key);
}
int currentWeight = weightSequence.getAndIncrement() % maxWeight;
List<Invoker<T>> weightInvokers = new ArrayList<Invoker<T>>();
for (Invoker<T> invoker : invokers) { // 篩選權重大於當前權重基數的Invoker
if (getWeight(invoker, invocation) > currentWeight) {
weightInvokers.add(invoker);
}
}
int weightLength = weightInvokers.size();
if (weightLength == 1) {
return weightInvokers.get(0);
} else if (weightLength > 1) {
invokers = weightInvokers;
length = invokers.size();
}
}
AtomicPositiveInteger sequence = sequences.get(key);
if (sequence == null) {
sequences.putIfAbsent(key, new AtomicPositiveInteger());
sequence = sequences.get(key);
}
// 取模輪循
return invokers.get(sequence.getAndIncrement() % length);
}
}
3、LeastActionLoadBalance演算法
public class LeastActiveLoadBalance extends AbstractLoadBalance {
public static final String NAME = "leastactive";
private final Random random = new Random();
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // 總個數
int leastActive = -1; // 最小的活躍數
int leastCount = 0; // 相同最小活躍數的個數
int[] leastIndexs = new int[length]; // 相同最小活躍數的下標
int totalWeight = 0; // 總權重
int firstWeight = 0; // 第一個權重,用於於計算是否相同
boolean sameWeight = true; // 是否所有權重相同
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); // 活躍數
int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT); // 權重
if (leastActive == -1 || active < leastActive) { // 發現更小的活躍數,重新開始
leastActive = active; // 記錄最小活躍數
leastCount = 1; // 重新統計相同最小活躍數的個數
leastIndexs[0] = i; // 重新記錄最小活躍數下標
totalWeight = weight; // 重新累計總權重
firstWeight = weight; // 記錄第一個權重
sameWeight = true; // 還原權重相同標識
} else if (active == leastActive) { // 累計相同最小的活躍數
leastIndexs[leastCount ++] = i; // 累計相同最小活躍數下標
totalWeight += weight; // 累計總權重
// 判斷所有權重是否一樣
if (sameWeight && i > 0
&& weight != firstWeight) {
sameWeight = false;
}
}
}
// assert(leastCount > 0)
if (leastCount == 1) {
// 如果只有一個最小則直接返回
return invokers.get(leastIndexs[0]);
}
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(