幾種簡單的負載均衡演算法及其Java程式碼實現

五月的倉頡發表於2016-01-30

什麼是負載均衡

負載均衡,英文名稱為Load Balance,指由多臺伺服器以對稱的方式組成一個伺服器集合,每臺伺服器都具有等價的地位,都可以單獨對外提供服務而無須其他伺服器的輔助。通過某種負載分擔技術,將外部傳送來的請求均勻分配到對稱結構中的某一臺伺服器上,而接收到請求的伺服器獨立地迴應客戶的請求。負載均衡能夠平均分配客戶請求到伺服器陣列,藉此提供快速獲取重要資料,解決大量併發訪問服務問題,這種叢集技術可以用最少的投資獲得接近於大型主機的效能。

負載均衡分為軟體負載均衡和硬體負載均衡,前者的代表是阿里章文嵩博士研發的LVS,後者則是均衡伺服器比如F5,當然這只是提一下,不是重點。

本文講述的是"將外部傳送來的請求均勻分配到對稱結構中的某一臺伺服器上"的各種演算法,並以Java程式碼演示每種演算法的具體實現,OK,下面進入正題,在進入正題前,先寫一個類來模擬Ip列表:

 1 public class IpMap
 2 {
 3     // 待路由的Ip列表,Key代表Ip,Value代表該Ip的權重
 4     public static HashMap<String, Integer> serverWeightMap = 
 5             new HashMap<String, Integer>();
 6     
 7     static
 8     {
 9         serverWeightMap.put("192.168.1.100", 1);
10         serverWeightMap.put("192.168.1.101", 1);
11         // 權重為4
12         serverWeightMap.put("192.168.1.102", 4);
13         serverWeightMap.put("192.168.1.103", 1);
14         serverWeightMap.put("192.168.1.104", 1);
15         // 權重為3
16         serverWeightMap.put("192.168.1.105", 3);
17         serverWeightMap.put("192.168.1.106", 1);
18         // 權重為2
19         serverWeightMap.put("192.168.1.107", 2);
20         serverWeightMap.put("192.168.1.108", 1);
21         serverWeightMap.put("192.168.1.109", 1);
22         serverWeightMap.put("192.168.1.110", 1);
23     }
24 }

 

輪詢(Round Robin)法

輪詢法即Round Robin法,其程式碼實現大致如下:

 1 public class RoundRobin
 2 {
 3     private static Integer pos = 0;
 4     
 5     public static String getServer()
 6     {
 7         // 重建一個Map,避免伺服器的上下線導致的併發問題
 8         Map<String, Integer> serverMap = 
 9                 new HashMap<String, Integer>();
10         serverMap.putAll(IpMap.serverWeightMap);
11         
12         // 取得Ip地址List
13         Set<String> keySet = serverMap.keySet();
14         ArrayList<String> keyList = new ArrayList<String>();
15         keyList.addAll(keySet);
16         
17         String server = null;
18         synchronized (pos)
19         {
20             if (pos > keySet.size())
21                 pos = 0;
22             server = keyList.get(pos);
23             pos ++;
24         }
25         
26         return server;
27     }
28 }

由於serverWeightMap中的地址列表是動態的,隨時可能有機器上線、下線或者當機,因此為了避免可能出現的併發問題,方法內部要新建區域性變數serverMap,現將serverMap中的內容複製到執行緒本地,以避免被多個執行緒修改。這樣可能會引入新的問題,複製以後serverWeightMap的修改無法反映給serverMap,也就是說這一輪選擇伺服器的過程中,新增伺服器或者下線伺服器,負載均衡演算法將無法獲知。新增無所謂,如果有伺服器下線或者當機,那麼可能會訪問到不存在的地址。因此,服務呼叫端需要有相應的容錯處理,比如重新發起一次server選擇並呼叫

對於當前輪詢的位置變數pos,為了保證伺服器選擇的順序性,需要在操作時對其加鎖,使得同一時刻只能有一個執行緒可以修改pos的值,否則當pos變數被併發修改,則無法保證伺服器選擇的順序性,甚至有可能導致keyList陣列越界。

輪詢法的優點在於:試圖做到請求轉移的絕對均衡

輪詢法的缺點在於:為了做到請求轉移的絕對均衡,必須付出相當大的代價,因為為了保證pos變數修改的互斥性,需要引入重量級的悲觀鎖synchronized,這將會導致該段輪詢程式碼的併發吞吐量發生明顯的下降

 

隨機(Random)法

通過系統隨機函式,根據後端伺服器列表的大小值來隨機選擇其中一臺進行訪問。由概率統計理論可以得知,隨著呼叫量的增大,其實際效果越來越接近於平均分配流量到每一臺後端伺服器,也就是輪詢的效果。

隨機法的程式碼實現大致如下:

 1 public class Random
 2 {
 3     public static String getServer()
 4     {
 5         // 重建一個Map,避免伺服器的上下線導致的併發問題
 6         Map<String, Integer> serverMap = 
 7                 new HashMap<String, Integer>();
 8         serverMap.putAll(IpMap.serverWeightMap);
 9         
10         // 取得Ip地址List
11         Set<String> keySet = serverMap.keySet();
12         ArrayList<String> keyList = new ArrayList<String>();
13         keyList.addAll(keySet);
14         
15         java.util.Random random = new java.util.Random();
16         int randomPos = random.nextInt(keyList.size());
17         
18         return keyList.get(randomPos);
19     }
20 }

整體程式碼思路和輪詢法一致,先重建serverMap,再獲取到server列表。在選取server的時候,通過Random的nextInt方法取0~keyList.size()區間的一個隨機值,從而從伺服器列表中隨機獲取到一臺伺服器地址進行返回。基於概率統計的理論,吞吐量越大,隨機演算法的效果越接近於輪詢演算法的效果

 

源地址雜湊(Hash)法

源地址雜湊的思想是獲取客戶端訪問的IP地址值,通過雜湊函式計算得到一個數值,用該數值對伺服器列表的大小進行取模運算,得到的結果便是要訪問的伺服器的序號。源地址雜湊演算法的程式碼實現大致如下:

 1 public class Hash
 2 {
 3     public static String getServer()
 4     {
 5         // 重建一個Map,避免伺服器的上下線導致的併發問題
 6         Map<String, Integer> serverMap = 
 7                 new HashMap<String, Integer>();
 8         serverMap.putAll(IpMap.serverWeightMap);
 9         
10         // 取得Ip地址List
11         Set<String> keySet = serverMap.keySet();
12         ArrayList<String> keyList = new ArrayList<String>();
13         keyList.addAll(keySet);
14         
15         // 在Web應用中可通過HttpServlet的getRemoteIp方法獲取
16         String remoteIp = "127.0.0.1";
17         int hashCode = remoteIp.hashCode();
18         int serverListSize = keyList.size();
19         int serverPos = hashCode % serverListSize;
20         
21         return keyList.get(serverPos);
22     }
23 }

前兩部分和輪詢法、隨機法一樣就不說了,差別在於路由選擇部分。通過客戶端的ip也就是remoteIp,取得它的Hash值,對伺服器列表的大小取模,結果便是選用的伺服器在伺服器列表中的索引值。

源地址雜湊法的優點在於:保證了相同客戶端IP地址將會被雜湊到同一臺後端伺服器,直到後端伺服器列表變更。根據此特性可以在服務消費者與服務提供者之間建立有狀態的session會話

源地址雜湊演算法的缺點在於:除非叢集中伺服器的非常穩定,基本不會上下線,否則一旦有伺服器上線、下線,那麼通過源地址雜湊演算法路由到的伺服器是伺服器上線、下線前路由到的伺服器的概率非常低,如果是session則取不到session,如果是快取則可能引發"雪崩"。如果這麼解釋不適合明白,可以看我之前的一篇文章MemCache超詳細解讀,一致性Hash演算法部分。

 

加權輪詢(Weight Round Robin)法

不同的伺服器可能機器配置和當前系統的負載並不相同,因此它們的抗壓能力也不盡相同,給配置高、負載低的機器配置更高的權重,讓其處理更多的請求,而低配置、高負載的機器,則給其分配較低的權重,降低其系統負載。加權輪詢法可以很好地處理這一問題,並將請求順序按照權重分配到後端。加權輪詢法的程式碼實現大致如下:

 1 public class WeightRoundRobin
 2 {
 3     private static Integer pos;
 4     
 5     public static String getServer()
 6     {
 7         // 重建一個Map,避免伺服器的上下線導致的併發問題
 8         Map<String, Integer> serverMap = 
 9                 new HashMap<String, Integer>();
10         serverMap.putAll(IpMap.serverWeightMap);
11         
12         // 取得Ip地址List
13         Set<String> keySet = serverMap.keySet();
14         Iterator<String> iterator = keySet.iterator();
15         
16         List<String> serverList = new ArrayList<String>();
17         while (iterator.hasNext())
18         {
19             String server = iterator.next();
20             int weight = serverMap.get(server);
21             for (int i = 0; i < weight; i++)
22                 serverList.add(server);
23         }
24         
25         String server = null;
26         synchronized (pos)
27         {
28             if (pos > keySet.size())
29                 pos = 0;
30             server = serverList.get(pos);
31             pos ++;
32         }
33         
34         return server;
35     }
36 }

與輪詢法類似,只是在獲取伺服器地址之前增加了一段權重計算的程式碼,根據權重的大小,將地址重複地增加到伺服器地址列表中,權重越大,該伺服器每輪所獲得的請求數量越多。

 

加權隨機(Weight Random)法

與加權輪詢法類似,加權隨機法也是根據後端伺服器不同的配置和負載情況來配置不同的權重。不同的是,它是按照權重來隨機選擇伺服器的,而不是順序。加權隨機法的程式碼實現如下:

 1 public class WeightRandom
 2 {
 3     public static String getServer()
 4     {
 5         // 重建一個Map,避免伺服器的上下線導致的併發問題
 6         Map<String, Integer> serverMap = 
 7                 new HashMap<String, Integer>();
 8         serverMap.putAll(IpMap.serverWeightMap);
 9         
10         // 取得Ip地址List
11         Set<String> keySet = serverMap.keySet();
12         Iterator<String> iterator = keySet.iterator();
13         
14         List<String> serverList = new ArrayList<String>();
15         while (iterator.hasNext())
16         {
17             String server = iterator.next();
18             int weight = serverMap.get(server);
19             for (int i = 0; i < weight; i++)
20                 serverList.add(server);
21         }
22         
23         java.util.Random random = new java.util.Random();
24         int randomPos = random.nextInt(serverList.size());
25         
26         return serverList.get(randomPos);
27     }
28 }

這段程式碼相當於是隨機法和加權輪詢法的結合,比較好理解,就不解釋了。

 

最小連線數(Least Connections)法

前面幾種方法費盡心思來實現服務消費者請求次數分配的均衡,當然這麼做是沒錯的,可以為後端的多臺伺服器平均分配工作量,最大程度地提高伺服器的利用率,但是實際情況是否真的如此?實際情況中,請求次數的均衡真的能代表負載的均衡嗎?這是一個值得思考的問題。

上面的問題,再換一個角度來說就是:以後端伺服器的視角來觀察系統的負載,而非請求發起方來觀察。最小連線數法便屬於此類。

最小連線數演算法比較靈活和智慧,由於後端伺服器的配置不盡相同,對於請求的處理有快有慢,它正是根據後端伺服器當前的連線情況,動態地選取其中當前積壓連線數最少的一臺伺服器來處理當前請求,儘可能地提高後端伺服器的利用效率,將負載合理地分流到每一臺機器。由於最小連線數設計伺服器連線數的彙總和感知,設計與實現較為繁瑣,此處就不說它的實現了。

相關文章