隨着系統日益龐大、邏輯業務愈來愈複雜,系統架構由原來的單一系統到垂直系統,發展到如今的分佈式系統。分佈式系統中,能夠作到公共業務模塊的高可用,高容錯性,高擴展性,然而,當系統愈來愈複雜時,須要考慮的東西天然也愈來愈多,要求也愈來愈高,好比服務路由、負載均衡等。此文將針對負載均衡算法進行講解,不涉及具體的實現。java
在分佈式系統中,多臺服務器同時提供一個服務,並統一到服務配置中心進行管理,如圖1-1。消費者經過查詢服務配置中心,獲取到服務到地址列表,須要選取其中一臺來發起RPC遠程調用。如何選擇,則取決於具體的負載均衡算法,對應於不一樣的場景,選擇的負載均衡算法也不盡相同。負載均衡算法的種類有不少種,常見的負載均衡算法包括輪詢法、隨機法、源地址哈希法、加權輪詢法、加權隨機法、最小鏈接法等,應根據具體的使用場景選取對應的算法。算法
圖1-1後端
輪詢很容易實現,將請求按順序輪流分配到後臺服務器上,均衡的對待每一臺服務器,而不關心服務器實際的鏈接數和當前的系統負載。數組
這裏經過實例化一個serviceWeightMap的Map變量來服務器地址和權重的映射,以此來模擬輪詢算法的實現,其中設置的權重值在之後的加權算法中會使用到,這裏先不作過多介紹,該變量初始化以下:服務器
private static Map<String, Integer> serviceWeightMap = new HashMap<String, Integer>(); static { serviceWeightMap.put("192.168.1.100", 1); serviceWeightMap.put("192.168.1.101", 1);
//權重爲4 serviceWeightMap.put("192.168.1.102", 4); serviceWeightMap.put("192.168.1.103", 1); serviceWeightMap.put("192.168.1.104", 1);
//權重爲3 serviceWeightMap.put("192.168.1.105", 3); serviceWeightMap.put("192.168.1.106", 1);
//權重爲2 serviceWeightMap.put("192.168.1.107", 2); serviceWeightMap.put("192.168.1.108", 1); serviceWeightMap.put("192.168.1.109", 1); serviceWeightMap.put("192.168.1.110", 1); }
經過該地址列表,實現的輪詢算法的部分關鍵代碼以下架構
private static Integer pos = 0; public static String testRoundRobin() { // 從新建立一個map,避免出現因爲服務器上線和下線致使的併發問題 Map<String, Integer> serverMap = new HashMap<String, Integer>(); serverMap.putAll(serviceWeightMap); //取得IP地址list Set<String> keySet = serverMap.keySet(); ArrayList<String> keyList = new ArrayList<String>(); keyList.addAll(keySet); String server = null; synchronized (pos) { if (pos > keySet.size()) { pos = 0; } server = keyList.get(pos); pos++; } return server; }
因爲serviceWeightMap中的地址列表是動態的,隨時可能由機器上線、下線或者宕機,所以,爲了不可能出現的併發問題,好比數組越界,經過在方法內新建局部變量serverMap,先將域變量拷貝到線程本地,避免被其餘線程修改。這樣可能會引入新的問題,當被拷貝以後,serviceWeightMap的修改將沒法被serverMap感知,也就是說,在這一輪的選擇服務器中,新增服務器或者下線服務器,負載均衡算法中將沒法獲知。新增比較好處理,而當服務器下線或者宕機時,服務消費者將有可能訪問不到不存在的地址。所以,在服務消費者服務端須要考慮該問題,而且進行相應的容錯處理,好比從新發起一次調用。 併發
對於當前輪詢的位置變量pos,爲了保證服務器選擇的順序性,須要對其在操做時加上synchronized鎖,使得同一時刻只有一個線程可以修改pos的值,不然當pos變量被併發修改,將沒法保證服務器選擇的順序性,甚至有可能致使keyList數組越界。負載均衡
使用輪詢策略的目的是,但願作到請求轉移的絕對均衡,但付出的代價性能也是至關大的。爲了保證pos變量的併發互斥,引入了重量級悲觀鎖synchronized,將會致使該輪詢代碼的併發吞吐量明顯降低。dom
經過系統隨機函數,根據後臺服務器列表的大小值來隨機選取其中一臺進行訪問。由機率機率統計理論能夠得知,隨着調用量的增大,其實際效果愈來愈接近於平均分配流量到後臺的每一臺服務器,也就是輪詢法的效果。分佈式
隨機算法的部分關鍵代碼以下:
public static String testRandom() { // 從新建立一個map,避免出現因爲服務器上線和下線致使的併發問題 Map<String, Integer> serverMap = new HashMap<String, Integer>(); serverMap.putAll(serviceWeightMap); //取得IP地址list Set<String> keySet = serverMap.keySet(); ArrayList<String> keyList = new ArrayList<String>(); keyList.addAll(keySet); Random random = new Random(); int randomPos = random.nextInt(keyList.size()); String server = keyList.get(randomPos); return server; }
跟前面相似,爲了不併發的問題,須要將serviceWeightMap拷貝到serverMap中。經過Random的nextInt函數,取到0~keyList.size之間的隨機值, 從而從服務器列表中隨機取到一臺服務器的地址,進行返回。根據機率統計理論,吞吐量越大,隨機算法的效果越接近於輪詢算法的效果。
源地址哈希法的思想是根據服務消費者請求客戶端的IP地址,經過哈希函數計算獲得一個哈希值,將此哈希值和服務器列表的大小進行取模運算,獲得的結果即是要訪問的服務器地址的序號。採用源地址哈希法進行負載均衡,相同的IP客戶端,若是服務器列表不變,將映射到同一個後臺服務器進行訪問。
源地址哈希法部分關鍵代碼以下:
public static String testConsumerHash(String remoteIp) { // 從新建立一個map,避免出現因爲服務器上線和下線致使的併發問題 Map<String, Integer> serverMap = new HashMap<String, Integer>(); serverMap.putAll(serviceWeightMap); //取得IP地址list Set<String> keySet = serverMap.keySet(); ArrayList<String> keyList = new ArrayList<String>(); keyList.addAll(keySet); int hashCode = remoteIp.hashCode(); int pos = hashCode % keyList.size(); return keyList.get(pos); }
不一樣的後臺服務器可能機器的配置和當前系統的負載並不相同,所以它們的抗壓能力也不同。跟配置高、負載低的機器分配更高的權重,使其能處理更多的請求,而配置低、負載高的機器,則給其分配較低的權重,下降其系統負載,加權輪詢很好的處理了這一問題,並將請求按照順序且根據權重分配給後端。
加權輪詢法部分關鍵代碼以下:
public static String testWeightRoundRobin() { // 從新建立一個map,避免出現因爲服務器上線和下線致使的併發問題 Map<String, Integer> serverMap = new HashMap<String, Integer>(); serverMap.putAll(serviceWeightMap); //取得IP地址list Set<String> keySet = serverMap.keySet(); Iterator<String> it = keySet.iterator(); List<String> serverList = new ArrayList<String>(); 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; synchronized (pos) { if (pos > serverList.size()) { pos = 0; } server = serverList.get(pos); pos++; } return server; }
與輪詢算法相似,只是在獲取服務器地址以前增長了一段權重計算代碼,根據權重的大小,將地址重複增長到服務器地址列表中,權重越大,該服務器每輪所得到的請求數量越多。
加權隨機法跟加權輪詢法相似,根據後臺服務器不一樣的配置和負載狀況,配置不一樣的權重。不一樣的是,它是按照權重來隨機選取服務器的,而非順序。
部分關鍵代碼以下:
public static String testWeightRandom() { // 從新建立一個map,避免出現因爲服務器上線和下線致使的併發問題 Map<String, Integer> serverMap = new HashMap<String, Integer>(); serverMap.putAll(serviceWeightMap); //取得IP地址list Set<String> keySet = serverMap.keySet(); List<String> serverList = new ArrayList<String>(); Iterator<String> it = keySet.iterator(); 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; }
前面咱們費盡心思來實現服務消費者請求次數分配的均衡,咱們知道這樣作是沒錯的,能夠爲後端的多臺服務器平均分配工做量,最大程度地提升服務器的利用率,可是,實際上,請求次數的均衡並不表明負載的均衡。所以咱們須要介紹最小鏈接數法,最小鏈接數法比較靈活和智能,因爲後臺服務器的配置不盡相同,對請求的處理有快有慢,它正是根據後端服務器當前的鏈接狀況,動態的選取其中當前積壓鏈接數最少的一臺服務器來處理當前請求,儘量的提升後臺服務器利用率,將負載合理的分流到每一臺服務器。