負載均衡,英文名稱爲Load Balance,指由多臺服務器以對稱的方式組成一個服務器集合,每臺服務器都具備等價的地位,均可以單獨對外提供服務而無須其餘服務器的輔助。經過某種負載分擔技術,將外部發送來的請求均勻分配到對稱結構中的某一臺服務器上,而接收到請求的服務器獨立地迴應客戶的請求。負載均衡可以平均分配客戶請求到服務器陣列,藉此提供快速獲取重要數據,解決大量併發訪問服務問題,這種集羣技術能夠用最少的投資得到接近於大型主機的性能。html
1、負載均衡方式 負載均衡分爲軟件負載均衡和硬件負載均衡建議沒有相關軟件使用經驗的同窗不要太糾結他們的不一樣之處,可繼續往下看。java
(1)軟件負載均衡 常見的負載均衡軟件有Nginx、LVS、HAProxy。 關於這幾個軟件的特色比較不是本文重點,感興趣同窗能夠參見博客: (總結)Nginx/LVS/HAProxy負載均衡軟件的優缺點詳解:www.ha97.com/5646.html 三大主流軟件負載均衡器對比(LVS 、 Nginx 、Haproxy):www.21yunwei.com/archives/58… (2)硬件負載均衡 常見的負載均衡硬件有Array、F5。node
2、負載均衡算法 常見的負載均衡算法有:隨機算法、加權輪詢、一致性hash、最小活躍數算法。 千萬別覺得這幾個算法看上去都特別簡單,但其實真正在生產上用到時會遠比你想的複雜。算法
算法前提條件 定義一個服務器列表,每一個負載均衡的算法會從中挑出一個服務器做爲算法的結果。 public class ServerIps { private static final List<String> LIST = Arrays.asList( "192.168.0.1", "192.168.0.2", "192.168.0.3", "192.168.0.4", "192.168.0.5", "192.168.0.6", "192.168.0.7", "192.168.0.8", "192.168.0.9", "192.168.0.10" ); }
服務器
一、隨機算法-RandomLoadBalance 先來個最簡單的實現。 `public class Random { public static String getServer() { // 生成一個隨機數做爲list的下標值併發
java.util.Random random = new java.util.Random(); int randomPos = random.nextInt(ServerIps.LIST.size());負載均衡
return ServerIps.LIST.get(randomPos); }dom
public static void main(String[] args) { // 連續調用10次 for (int i=0; i<10; i++) { System.out.println(getServer()); } } }`工具
運行結果: 192.168.0.3 192.168.0.4 192.168.0.7 192.168.0.1 192.168.0.2 192.168.0.7 192.168.0.3 192.168.0.9 192.168.0.1 192.168.0.1性能
當調用次數比較少時,Random 產生的隨機數可能會比較集中,此時多數請求會落到同一臺服務器上,只有在通過屢次請求後,才能使調用請求進行「均勻」分配。調用量少這一點並無什麼關係,負載均衡機制不正是爲了應對請求量多的狀況嗎,因此隨機算法也是用得比較多的一種算法。
可是,上面的隨機算法適用於天天機器的性能差很少的時候,實際上,生產中可能某些機器的性能更高一點,它能夠處理更多的請求,因此,咱們能夠對每臺服務器設置一個權重。
在ServerIps類中增長服務器權重對應關係MAP,權重之和爲50:
public static final Map<String, Integer> WEIGHT_LIST = new HashMap<String, Integer>(); static { // 權重之和爲50 WEIGHT_LIST.put("192.168.0.1", 1); WEIGHT_LIST.put("192.168.0.2", 8); WEIGHT_LIST.put("192.168.0.3", 3); WEIGHT_LIST.put("192.168.0.4", 6); WEIGHT_LIST.put("192.168.0.5", 5); WEIGHT_LIST.put("192.168.0.6", 5); WEIGHT_LIST.put("192.168.0.7", 4); WEIGHT_LIST.put("192.168.0.8", 7); WEIGHT_LIST.put("192.168.0.9", 2); WEIGHT_LIST.put("192.168.0.10", 9); }
那麼如今的隨機算法應該要改爲權重隨機算法,當調用量比較多的時候,服務器使用的分佈應該近似對應權重的分佈。
二、權重隨機算法 簡單的實現思路是,把每一個服務器按它所對應的服務器進行復制,具體看代碼更加容易理解 `public class WeightRandom { public static String getServer() { // 生成一個隨機數做爲list的下標值 List ips = new ArrayList();
for (String ip : ServerIps.WEIGHT_LIST.keySet()) { Integer weight = ServerIps.WEIGHT_LIST.get(ip);
// 按權重進行復制 for (int i=0; i<weight; i++) { ips.add(ip); } }
java.util.Random random = new java.util.Random(); int randomPos = random.nextInt(ips.size());
return ips.get(randomPos); }
public static void main(String[] args) { // 連續調用10次 for (int i=0; i<10; i++) { System.out.println(getServer()); } } }`
運行結果: 192.168.0.8 192.168.0.2 192.168.0.7 192.168.0.10 192.168.0.8 192.168.0.8 192.168.0.4 192.168.0.7 192.168.0.6 192.168.0.8
這種實現方法在遇到權重之和特別大的時候就會比較消耗內存,由於須要對ip地址進行復制,權重之和越大那麼上文中的ips就須要越多的內存,下面介紹另一種實現思路。
假設咱們有一組服務器 servers = [A, B, C],他們對應的權重爲 weights = [5, 3, 2],權重總和爲10。如今把這些權重值平鋪在一維座標值上,[0, 5) 區間屬於服務器 A,[5, 8) 區間屬於服務器 B,[8, 10) 區間屬於服務器 C。接下來經過隨機數生成器生成一個範圍在 [0, 10) 之間的隨機數,而後計算這個隨機數會落到哪一個區間上。好比數字3會落到服務器 A 對應的區間上,此時返回服務器 A 便可。權重越大的機器,在座標軸上對應的區間範圍就越大,所以隨機數生成器生成的數字就會有更大的機率落到此區間內。只要隨機數生成器產生的隨機數分佈性很好,在通過屢次選擇後,每一個服務器被選中的次數比例接近其權重比例。好比,通過一萬次選擇後,服務器 A 被選中的次數大約爲5000次,服務器 B 被選中的次數約爲3000次,服務器 C 被選中的次數約爲2000次。
假設如今隨機數offset=7: 一、offset<5 is false,因此不在[0, 5)區間,將offset = offset - 5(offset=2) 二、offset<3 is true,因此處於[5, 8)區間,因此應該選用B服務器
實現以下: `public class WeightRandomV2 { public static String getServer() {
int totalWeight = 0; boolean sameWeight = true; // 若是全部權重都相等,那麼隨機一個ip就行了
Object[] weights = ServerIps.WEIGHT_LIST.values().toArray();
for (int i = 0; i < weights.length; i++) { Integer weight = (Integer) weights[i]; totalWeight += weight; if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) { sameWeight = false; } }
java.util.Random random = new java.util.Random(); int randomPos = random.nextInt(totalWeight);
if (!sameWeight) { for (String ip : ServerIps.WEIGHT_LIST.keySet()) { Integer value = ServerIps.WEIGHT_LIST.get(ip); if (randomPos < value) { return ip; } randomPos = randomPos - value; } }
return (String) ServerIps.WEIGHT_LIST.keySet().toArray()[new java.util.Random().nextInt(ServerIps.WEIGHT_LIST.size())]; }
public static void main(String[] args) { // 連續調用10次 for (int i = 0; i < 10; i++) { System.out.println(getServer()); } } }` 這就是另一種權重隨機算法。
三、輪詢算法-RoundRobinLoadBalance 簡單的輪詢算法很簡單 `public class RoundRobin { // 當前循環的位置 private static Integer pos = 0;
public static String getServer() { String ip = null; // pos同步 synchronized (pos) { if (pos >= ServerIps.LIST.size()) { pos = 0; } ip = ServerIps.LIST.get(pos); pos++; }
return ip; }
public static void main(String[] args) { // 連續調用10次 for (int i = 0; i < 11; i++) { System.out.println(getServer()); } }
}`
運行結果: 192.168.0.1 192.168.0.2 192.168.0.3 192.168.0.4 192.168.0.5 192.168.0.6 192.168.0.7 192.168.0.8 192.168.0.9 192.168.0.10 192.168.0.1
這種算法很簡單,也很公平,每臺服務輪流來進行服務,可是有的機器性能好,因此能者多勞,和隨機算法一下,加上權重這個維度以後,其中一種實現方法就是複製法,這裏就不演示了,這種複製算法的缺點和隨機算法的是同樣的,比較消耗內存,那麼天然就有其餘實現方法。我下面來介紹一種算法:
這種算法須要加入一個概念:調用編號,好比第1次調用爲1, 第2次調用爲2, 第100次調用爲100,調用編號是遞增的,因此咱們能夠根據這個調用編號推算出服務器。
假設咱們有三臺服務器 servers = [A, B, C],對應的權重爲 weights = [ 2, 5, 1], 總權重爲8,咱們能夠理解爲有8臺「服務器」,這是8臺「不具備併發功能」,其中有2臺爲A,5臺爲B,1臺爲C,一次調用過來的時候,須要按順序訪問,好比有10次調用,那麼服務器調用順序爲AABBBBBCAA,調用編號會愈來愈大,而服務器是固定的,因此須要把調用編號「縮小」,這裏對調用編號進行取餘,除數爲總權重和,好比:
一、1號調用,1%8=1; 二、2號調用,2%8=2; 三、3號調用,3%8=3; 四、8號調用,8%8=0; 五、9號調用,9%8=1; 六、100號調用,100%8=4;
咱們發現調用編號能夠被縮小爲0-7之間的8個數字,問題是怎麼根據這個8個數字找到對應的服務器呢?和咱們隨機算法相似,這裏也能夠把權重想象爲一個座標軸「0-----2-----7-----8」
一、1號調用,1%8=1,offset = 1, offset <= 2 is true,取A; 二、2號調用,2%8=2;offset = 2,offset <= 2 is true, 取A; 三、3號調用,3%8=3;offset = 3, offset <= 2 is false, offset = offset - 2, offset = 1, offset <= 5,取B 四、8號調用,8%8=0;offset = 0, 特殊狀況,offset = 8,offset <= 2 is false, offset = offset - 2, offset = 6, offset <= 5 is false, offset = offset - 5, offset = 1, offset <= 1 is true, 取C; 五、9號調用,9%8=1;// ... 六、100號調用,100%8=4;//...
實現: 模擬調用編號獲取工具: `public class Sequence {
public static Integer num = 0;
public static Integer getAndIncrement() { return ++num; } }
public class WeightRoundRobin { private static Integer pos = 0;
public static String getServer() { int totalWeight = 0; boolean sameWeight = true; // 若是全部權重都相等,那麼隨機一個ip就行了
Object[] weights = ServerIps.WEIGHT_LIST.values().toArray();
for (int i = 0; i < weights.length; i++) { Integer weight = (Integer) weights[i]; totalWeight += weight; if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) { sameWeight = false; } }
Integer sequenceNum = Sequence.getAndIncrement();
Integer offset = sequenceNum % totalWeight; offset = offset == 0 ? totalWeight : offset;
if (!sameWeight) { for (String ip : ServerIps.WEIGHT_LIST.keySet()) { Integer weight = ServerIps.WEIGHT_LIST.get(ip); if (offset <= weight) { return ip; } offset = offset - weight; } }
String ip = null; synchronized (pos) { if (pos >= ServerIps.LIST.size()) { pos = 0; } ip = ServerIps.LIST.get(pos); pos++; }
return ip; }
public static void main(String[] args) { // 連續調用11次 for (int i = 0; i < 11; i++) { System.out.println(getServer()); } } } `
運行結果: 192.168.0.1 192.168.0.2 192.168.0.2 192.168.0.2 192.168.0.2 192.168.0.2 192.168.0.2 192.168.0.2 192.168.0.2 192.168.0.3 192.168.0.3 可是這種算法有一個缺點:一臺服務器的權重特別大的時候,他須要連續的的處理請求,可是實際上咱們想達到的效果是,對於100次請求,只要有100*8/50=16次就夠了,這16次不必定要連續的訪問,好比假設咱們有三臺服務器 servers = [A, B, C],對應的權重爲 weights = [5, 1, 1] , 總權重爲7,那麼上述這個算法的結果是:AAAAABC,那麼若是可以是這麼一個結果呢:AABACAA,把B和C平均插入到5個A中間,這樣是比較均衡的了。
咱們這裏能夠改爲平滑加權輪詢,這裏先講一下思路: 每一個服務器對應兩個權重,分別爲 weight 和 currentWeight。其中 weight 是固定的,currentWeight 會動態調整,初始值爲0。當有新的請求進來時,遍歷服務器列表,讓它的 currentWeight 加上自身權重。遍歷完成後,找到最大的 currentWeight,並將其減去權重總和,而後返回相應的服務器便可。
如上,通過平滑性處理後,獲得的服務器序列爲 [A, A, B, A, C, A, A],相比以前的序列 [A, A, A, A, A, B, C],分佈性要好一些。初始狀況下 currentWeight = [0, 0, 0],第7個請求處理完後,currentWeight 再次變爲 [0, 0, 0]。
實現: `// 增長一個Weight類,用來保存ip, weight(固定不變的原始權重), currentweight(當前會變化的權重) public class Weight { private String ip; private Integer weight; private Integer currentWeight;
public Weight(String ip, Integer weight, Integer currentWeight) { this.ip = ip; this.weight = weight; this.currentWeight = currentWeight; }
public String getIp() { return ip; }
public void setIp(String ip) { this.ip = ip; }
public Integer getWeight() { return weight; }
public void setWeight(Integer weight) { this.weight = weight; }
public Integer getCurrentWeight() { return currentWeight; }
public void setCurrentWeight(Integer currentWeight) { this.currentWeight = currentWeight; } }
public class WeightRoundRobinV2 { private static Map<String, Weight> weightMap = new HashMap<String, Weight>();
public static String getServer() { // java8 int totalWeight = ServerIps.WEIGHT_LIST.values().stream().reduce(0, (w1, w2) -> w1+w2);
// 初始化weightMap,初始時將currentWeight賦值爲weight if (weightMap.isEmpty()) { ServerIps.WEIGHT_LIST.forEach((key, value) -> { weightMap.put(key, new Weight(key, value, value)); }); }
// 找出currentWeight最大值 Weight maxCurrentWeight = null; for (Weight weight : weightMap.values()) { if (maxCurrentWeight == null || weight.getCurrentWeight() > maxCurrentWeight.getCurrentWeight()) { maxCurrentWeight = weight; } }
// 將maxCurrentWeight減去總權重和 maxCurrentWeight.setCurrentWeight(maxCurrentWeight.getCurrentWeight() - totalWeight);
// 全部的ip的currentWeight統一加上原始權重 for (Weight weight : weightMap.values()) { weight.setCurrentWeight(weight.getCurrentWeight() + weight.getWeight()); }
// 返回maxCurrentWeight所對應的ip return maxCurrentWeight.getIp(); }
public static void main(String[] args) { // 連續調用10次 for (int i = 0; i < 10; i++) { System.out.println(getServer()); } }
}`
講ServerIps裏的數據簡化爲:
WEIGHT_LIST.put("A", 5); WEIGHT_LIST.put("B", 1); WEIGHT_LIST.put("C", 1);
運行結果: A A B A C A A A A B 這就是輪詢算法,一個循環很簡單,可是真正在實際運用的過程當中須要思考更多。
四、一致性哈希算法-ConsistentHashLoadBalance 服務器集羣接收到一次請求調用時,能夠根據根據請求的信息,好比客戶端的ip地址,或請求路徑與請求參數等信息進行哈希,能夠得出一個哈希值,特色是對於相同的ip地址,或請求路徑和請求參數哈希出來的值是同樣的,只要能再增長一個算法,可以把這個哈希值映射成一個服務端ip地址,就可使相同的請求(相同的ip地址,或請求路徑和請求參數)落到同一服務器上。
由於客戶端發起的請求狀況是無窮無盡的(客戶端地址不一樣,請求參數不一樣等等),因此對於的哈希值也是無窮大的,因此咱們不可能把全部的哈希值都進行映射到服務端ip上,因此這裏須要用到哈希環。以下圖
(1)哈希值若是須要ip1和ip2之間的,則應該選擇ip2做爲結果; (2)哈希值若是須要ip2和ip3之間的,則應該選擇ip3做爲結果; (3)哈希值若是須要ip3和ip4之間的,則應該選擇ip4做爲結果; (4)哈希值若是須要ip4和ip1之間的,則應該選擇ip1做爲結果;
上面這狀況是比較均勻狀況,若是出現ip4服務器不存在,那就是這樣了:
會發現,ip3和ip1直接的範圍是比較大的,會有更多的請求落在ip1上,這是不「公平的」,解決這個問題須要加入虛擬節點,好比:
其中ip2-1, ip3-1就是虛擬結點,並不能處理節點,而是等同於對應的ip2和ip3服務器。 實際上,這只是處理這種不均衡性的一種思路,實際上就算哈希環自己是均衡的,你也能夠增長更多的虛擬節點來使這個環更加平滑,好比:
這個彩環也是「公平的」,而且只有ip1,2,3,4是實際的服務器ip,其餘的都是虛擬ip。 那麼咱們怎麼來實現呢? 對於咱們的服務端ip地址,咱們確定知道總共有多少個,須要多少個虛擬節點也有咱們本身控制,虛擬節點越多則流量越均衡,另外哈希算法也是很關鍵的,哈希算法越散列流量也將越均衡。
實現: `public class ConsistentHash {
private static SortedMap<Integer, String> virtualNodes = new TreeMap<>(); private static final int VIRTUAL_NODES = 160;
static { // 對每一個真實節點添加虛擬節點,虛擬節點會根據哈希算法進行散列 for (String ip : ServerIps.LIST) { for (int i = 0; i < VIRTUAL_NODES; i++) { int hash = getHash(ip+"VN"+i); virtualNodes.put(hash, ip); } } }
private static String getServer(String client) { int hash = getHash(client); // 獲得大於該Hash值的排好序的Map SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
// 大於該hash值的第一個元素的位置 Integer nodeIndex = subMap.firstKey();
// 若是不存在大於該hash值的元素,則返回根節點 if (nodeIndex == null) { nodeIndex = virtualNodes.firstKey(); }
// 返回對應的虛擬節點名稱 return subMap.get(nodeIndex); }
private static int getHash(String str) { final int p = 16777619; int hash = (int) 2166136261L; for (int i = 0; i < str.length(); i++) hash = (hash ^ str.charAt(i)) * p; hash += hash << 13; hash ^= hash >> 7; hash += hash << 3; hash ^= hash >> 17; hash += hash << 5;
// 若是算出來的值爲負數則取其絕對值 if (hash < 0) hash = Math.abs(hash); return hash; }
public static void main(String[] args) { // 連續調用10次,隨機10個client for (int i = 0; i < 10; i++) { System.out.println(getServer("client" + i)); } } } `
五、最小活躍數算法-LeastActiveLoadBalance 前面幾種方法主要目標是使服務端分配到的調用次數儘可能均衡,可是實際狀況是這樣嗎?調用次數相同,服務器的負載就均衡嗎?固然不是,這裏還要考慮每次調用的時間,而最小活躍數算法則是解決這種問題的。
活躍調用數越小,代表該服務提供者效率越高,單位時間內可處理更多的請求。此時應優先將請求分配給該服務提供者。在具體實現中,每一個服務提供者對應一個活躍數。初始狀況下,全部服務提供者活躍數均爲0。每收到一個請求,活躍數加1,完成請求後則將活躍數減1。在服務運行一段時間後,性能好的服務提供者處理請求的速度更快,所以活躍數降低的也越快,此時這樣的服務提供者可以優先獲取到新的服務請求、這就是最小活躍數負載均衡算法的基本思想。除了最小活躍數,最小活躍數算法在實現上還引入了權重值。因此準確的來講,最小活躍數算法是基於加權最小活躍數算法實現的。舉個例子說明一下,在一個服務提供者集羣中,有兩個性能優異的服務提供者。某一時刻它們的活躍數相同,則會根據它們的權重去分配請求,權重越大,獲取到新請求的機率就越大。若是兩個服務提供者權重相同,此時隨機選擇一個便可。
實現: 由於活躍數是須要服務器請求處理相關邏輯配合的,一次調用開始時活躍數+1,結束是活躍數-1,因此這裏就不對這部分邏輯進行模擬了,直接使用一個map來進行模擬。 `// 服務器當前的活躍數 public static final Map<String, Integer> ACTIVITY_LIST = new LinkedHashMap<String, Integer>(); static { ACTIVITY_LIST.put("192.168.0.1", 2); ACTIVITY_LIST.put("192.168.0.2", 0); ACTIVITY_LIST.put("192.168.0.3", 1); ACTIVITY_LIST.put("192.168.0.4", 3); ACTIVITY_LIST.put("192.168.0.5", 0); ACTIVITY_LIST.put("192.168.0.6", 1); ACTIVITY_LIST.put("192.168.0.7", 4); ACTIVITY_LIST.put("192.168.0.8", 2); ACTIVITY_LIST.put("192.168.0.9", 7); ACTIVITY_LIST.put("192.168.0.10", 3); }
public class LeastActive {
private static String getServer() {
// 找出當前活躍數最小的服務器 Optional minValue = ServerIps.ACTIVITY_LIST.values().stream().min(Comparator.naturalOrder()); if (minValue.isPresent()) { List minActivityIps = new ArrayList<>(); ServerIps.ACTIVITY_LIST.forEach((ip, activity) -> { if (activity.equals(minValue.get())) { minActivityIps.add(ip); } });
// 最小活躍數的ip有多個,則根據權重來選,權重大的優先 if (minActivityIps.size() > 1) {
// 過濾出對應的ip和權重 Map<String, Integer> weightList = new LinkedHashMap<String, Integer>(); ServerIps.WEIGHT_LIST.forEach((ip, weight) -> { if (minActivityIps.contains(ip)) { weightList.put(ip, ServerIps.WEIGHT_LIST.get(ip)); } });
int totalWeight = 0; boolean sameWeight = true; // 若是全部權重都相等,那麼隨機一個ip就行了
Object[] weights = weightList.values().toArray();
for (int i = 0; i < weights.length; i++) { Integer weight = (Integer) weights[i]; totalWeight += weight; if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) { sameWeight = false; } }
java.util.Random random = new java.util.Random(); int randomPos = random.nextInt(totalWeight);
if (!sameWeight) { for (String ip : weightList.keySet()) { Integer value = weightList.get(ip); if (randomPos < value) { return ip; } randomPos = randomPos - value; } }
return (String) weightList.keySet().toArray()[new java.util.Random().nextInt(weightList.size())];
} else { return minActivityIps.get(0); } } else { return (String) ServerIps.WEIGHT_LIST.keySet().toArray()[new java.util.Random().nextInt(ServerIps.WEIGHT_LIST.size())]; }
}
public static void main(String[] args) { // 連續調用10次,隨機10個client for (int i = 0; i < 10; i++) { System.out.println(getServer()); } } }`
這裏由於不會對活躍數進行操做,因此結果是固定的(擔任在隨機權重的時候會隨機,具體看源碼實現,以及運行結果便可理解)。
負載均衡總結