前言
前不久公司有個需求是任務需要按照權重分配來選擇,當時就想到負載均衡算法里的加權隨機法,因此對常見的負載均衡算法做個總結。
一、輪詢(Round Robin)法
輪詢就是將請求按順序輪流地分配到后端服務器上,它均衡地對待后端每一臺服務器,而不關心服務器實際的連接數和當前的系統負載。
// serverWeightMap 表示服務器地址和權重的映射
Map<String, Integer> serverWeightMap = new HashMap<>();
serverWeightMap.put("192.168.1.100", 1);
serverWeightMap.put("192.168.1.101", 1);
// 權重為 4
serverWeightMap.put("192.168.1.102", 4);
serverWeightMap.put("192.168.1.103", 1);
serverWeightMap.put("192.168.1.104", 1);
// 權重為 3
serverWeightMap.put("192.168.1.105", 3);
serverWeightMap.put("192.168.1.106", 1);
// 權重為 2
serverWeightMap.put("192.168.1.107", 2);
serverWeightMap.put("192.168.1.108", 1);
serverWeightMap.put("192.168.1.109", 1);
serverWeightMap.put("192.168.1.110", 1);
public static String testRoundRobin() {
// 重新創建一個 map,避免出現由于服務器上線和下線導致的并發問題
Map<String, Integer> serverMap = new HashMap<>();
serverMap.putAll(serverWeightMap);
// 取得 IP 地址 list
Set<String> keySet = serverMap.keySet();
List<String> keyList = new ArrayList<>();
keyList.addAll(keySet);
String server = null;
Integer pos = 0;
synchronized (pos) {
if (pos >= keySet.size()) {
pos = 0;
}
server = keyList.get(pos);
pos++;
}
return server;
}
由于 serverWeightMap 中的地址列表是動態的,隨時可能有機器上線、下線或者宕機,因此為了避免可能出現的并發問題,如數組越界,通過新建方法內的局部變量 serverMap,先將域變量復制到線程本地,避免對多個線程修改。這樣會引入新的問題,復制以后 serverWeightMap 的修改將無法反應給 serverMap,也就是說,在這一輪選擇服務器的過程中,新增服務器或者下線服務器,負載均衡算法中將無法獲知。新增比較好處理,而當服務器下線或者宕機時,服務消費者將有可能訪問到不存在的地址。因此,在服務消費者的實現端需要考慮該問題,并且進行相應的容錯處理,比如重新發起一次調用。
對于當前輪詢的位置變量 pos,為了保證服務器選擇的順序性,需要在操作時對其加上 synchronized 鎖,使得在同一時刻只有一個線程能夠修改 pos 的值,否則當 pos 變量被并發修改時,則無法保證服務器選擇的順序性,甚至有可能導致 keyList 數組越界。
使用輪詢策略的目的在于,希望做到請求轉移的絕對平衡,但付出的性能代價也是相當大的。為了 pos 保證變量的互斥性,需要引入重量級的悲觀鎖 synchronized,將會導致該段輪詢代碼的并發吞吐量發生明顯的下降。
二、隨機(Random)法
通過系統隨機函數,根據后端服務器列表的大小值來隨機選取其中一臺進行訪問。由概率統計理論可以得知,隨著調用量的增大,其實際效果越來越接近于平均分配流量到每一臺后端服務器,也就是輪詢的效果。
public static String testRandom() {
// 重新創建一個 map,避免出現由于服務器上線和下線導致的并發問題
Map<String, Integer> serverMap = new HashMap<>();
serverMap.putAll(serverWeightMap);
// 取得 IP 地址 list
Set<String> keySet = serverMap.keySet();
List<String> keyList = new ArrayList<>();
keyList.addAll(keySet);
Random random = new Random();
int randomPos = random.nextInt(keyList.size());
String server = keyList.get(randomPos);
return server;
}
跟前面類似,為了避免可能的并發問題,需要將 serverWeightMap 復制到 serverMap 中。通過 Random 的 nextInt 方法,取到在 0~keyList.size() 區間的一個隨機值,從而從服務器列表中隨機獲取到一臺服務器地址,進行返回。基于概率統計的理論,吞吐量越大,隨機算法的效果越接近于輪詢算法的效果。因此,基本可以替代輪詢算法。
三、源地址哈希(Hash)法
源地址哈希的思想是獲取客戶端訪問的 IP 地址值,通過哈希函數計算得到一個數值,用該數值對服務器列表的大小進行取模運算,得到的結果邊是要訪問的服務器的序號。采用哈希法進行負載均衡,同一 IP 地址的客戶端,當后端服務器列表不變時,它每次都會映射到同一臺后端服務器進行訪問。
public static String testConsumerHash(String remoteip) {
// 重新創建一個 map,避免出現由于服務器上線和下線導致的并發問題
Map<String, Integer> serverMap = new HashMap<>();
serverMap.putAll(serverWeightMap);
// 取得 IP 地址 list
Set<String> keySet = serverMap.keySet();
List<String> keyList = new ArrayList<>();
keyList.addAll(keySet);
int hashCode = remoteip.hashCode();
int serverListSize = keyList.size();
int serverPos = hashCode % serverListSize;
return keyList.get(serverPos);
}
通過參數傳入的客戶端 remoteip 參數,取得它的哈希值,對服務器列表的大小取模,結果便是選用的服務器在服務器列表中的索引值。該算法保證了相同的客戶端 IP 地址將會被“哈希”到同一臺后端服務器,直到后端服務器列表變更。根據此特性可以在服務消費者與服務提供者之間建立有狀態的 session 會話。
四、加權輪詢(Weight Round Robin)法
不同的后端服務器可能機器的配置和當前系統的負載并不相同,因此它們的抗壓能力也不盡相同。給配置高、負載低的機器配置更高的權重,讓其處理更多的請求,而低配置、負載高的機器,則給其分配較低的權重,降低其系統負載,加權輪詢能很好地處理這一問題,并將請求順序且按照權重分配到后端。
public static String testWeightRoundRobin() {
// 重新創建一個 map,避免出現由于服務器上線和下線導致的并發問題
Map<String, Integer> serverMap = new HashMap<>();
serverMap.putAll(serverWeightMap);
// 取得 IP 地址 list
Set<String> keySet = serverMap.keySet();
Iterator<String> it = keySet.iterator();
List<String> serverList = new ArrayList<>();
while (it.hasNext()) {
String server = it.next();
Integer weight = serverMap.get(server);
for (int i = 0; i < weight; i++) {
serverList.add(server);
}
}
String server = null;
Integer pos = 0;
synchronized (pos) {
if (pos >= serverList.size()) {
pos = 0;
}
server += serverList.get(pos);
pos++;
}
return server;
}
與輪詢算法類似,只是在獲取服務器地址之前增加了一段權重計算的代碼,根據權重的大小,將地址重復地增加到服務器地址列表中,權重越大,該服務器每輪所獲得的請求數量越多。
五、加權隨機(Weight Random)法
與加權輪詢法類似,加權隨機法也根據后端服務器不同的配置和負載情況,配置不同的權重。不同的是,它是按照權重來隨機選取服務器的,而非順序。
/**
* 實現方法一
*/
public static String testWeightRandom() {
// 重新創建一個 map,避免出現由于服務器上線和下線導致的并發問題
Map<String, Integer> serverMap = new HashMap<>();
serverMap.putAll(serverWeightMap);
// 取得 IP 地址 list
Set<String> keySet = serverMap.keySet();
Iterator<String> it = keySet.iterator();
List<String> serverList = new ArrayList<>();
while (it.hasNext()) {
String server = it.next();
Integer weight = serverMap.get(server);
for (int i = 0; i < weight; i++) {
serverList.add(server);
}
}
Random random = new Random();
int randomPos = random.nextInt(serverList.size());
String server = serverList.get(randomPos);
return server;
}
/**
* 實現方法二
*/
public static String testWeightRandom() {
// 重新創建一個 map,避免出現由于服務器上線和下線導致的并發問題
Map<String, Integer> serverMap = new HashMap<>();
serverMap.putAll(serverWeightMap);
// 計算權重和
long weightSum = 0;
for (String key : serverMap.keySet()) {
weightSum += serverMap.get(key);
}
// 產生隨機數
long random = Math.round(Math.random() * weightSum);
long weight = 0;
for (String server : serverMap.keySet()) {
weight += serverMap.get(server);
if (weight >= random) {
return server;
}
}
return serverMap.keySet().iterator().next();
}
我們費盡心思來實現服務消費者請求次數分配的均衡,我們知道這樣做是沒錯的,可以為后端的多臺服務器平均分配工作量,最大程度地提高服務器的利用率,但是,實際情況真的如此嗎?在實際情況中,請求次數的均衡真的能代表負載的均衡嗎?我們必須認真地思考這個問題。從算法實施的角度來看,以后端服務器的視角來觀察系統的負載,而非請求發起方來觀察。因此,我們得有其它的算法來實現可供選擇,最小連接數法便屬于此類算法。
六、最小連接數(Least Connections)法
最小連接數算法比較靈活和智能,由于后端服務器的配置不盡相同,對于請求的處理有快有慢,它正是根據后端服務器當前的連接情況,動態地選取其中當前積壓連接數最小的一臺服務器來處理當前請求,盡可能地提高后端服務器的利用效率,將負載合理地分流到每一臺機器。由于最小連接數涉及服務器連接數的匯總和感知,設計與實現比較繁瑣。