參考:
https://www.cnblogs.com/CodeBear/archive/2019/03/11/10508880.html
https://www.cnblogs.com/xrq730/p/5154340.html
https://blog.csdn.net/NRlovestudy/article/details/93237547
https://blog.csdn.net/qq_29373285/article/details/88777503
淺談負載均衡算法與實現
記得,我剛工作的時候,同事說了一個故事:在他剛工作的時候,他同事有一天興沖沖的跑到公司說,你們知道嗎,公司請了個大牛。大牛?對,那人會寫AJAX!哇,真是大牛啊,跟着他,可以學不少東西啊。我聽了笑了,但有點難以理解,因為現在幾乎只要是一個開發,都會寫AJAX,怎么寫個AJAX就算大牛呢?后來我明白了,三年前高深莫測的技術到現在變得普普通通,不足為奇,就像我們今天要講的負載均衡,在幾何時,負載均衡只有大牛才能玩轉起來,但是到今天,一個小開發都可以聊上幾句。現在,就讓我們簡單的看看負載均衡把。
從負載均衡設備的角度來看,分為硬件負載均衡和軟件負載均衡:
- 硬件負載均衡:比如最常見的F5,還有Array等,這些負載均衡是商業的負載均衡器,性能比較好,畢竟他們的就是為了負載均衡而生的,背后也有非常成熟的團隊,可以提供各種解決方案,但是價格比較昂貴,所以沒有充足的理由,充足的軟妹幣是不會考慮的。
- 軟件負載均衡:包括我們耳熟能詳的Nginx,LVS,Tengine(阿里對Nginx進行的改造)等。優點就是成本比較低,但是也需要有比較專業的團隊去維護,要自己去踩坑,去DIY。
從負載均衡的技術來看,分為服務端負載均衡和客戶端負載均衡:
- 服務端負載均衡:當我們訪問一個服務,請求會先到另外一台服務器,然后這台服務器會把請求分發到提供這個服務的服務器,當然如果只有一台服務器,那好說,直接把請求給那一台服務器就可以了,但是如果有多台服務器呢?這時候,就會根據一定的算法選擇一台服務器。
- 客戶端負載均衡:客戶端服務均衡的概念貌似是有了服務治理才產生的,簡單的來說,就是在一台服務器上維護着所有服務的ip,名稱等信息,當我們在代碼中訪問一個服務,是通過一個組件訪問的,這個組件會從那台服務器上取到所有提供這個服務的服務器的信息,然后通過一定的算法,選擇一台服務器進行請求。
從負載均衡的算法來看,又分為 隨機,輪詢,哈希,最小壓力,當然可能還會加上權重的概念,負載均衡的算法就是本文的重點了。
隨機
隨機就是沒有規律的,隨便從負載中獲得一台,又分為完全隨機和加權隨機:
完全隨機
public class Servers { public List<String> list = new ArrayList<>() { { add("192.168.1.1"); add("192.168.1.2"); add("192.168.1.3"); } }; }
public class FullRandom { static Servers servers = new Servers(); static Random random = new Random(); public static String go() { var number = random.nextInt(servers.list.size()); return servers.list.get(number); } public static void main(String[] args) { for (var i = 0; i < 15; i++) { System.out.println(go()); } } }
運行結果:
雖說現在感覺並不是那么隨機,有的服務器經常被獲得到,有的服務器獲得的次數比較少,但是當有充足的請求次數,就會越來越平均,這正是隨機數的一個特性。
完全隨機是最簡單的負載均衡算法了,缺點比較明顯,因為服務器有好有壞,處理能力是不同的,我們希望性能好的服務器多處理些請求,性能差的服務器少處理一些請求,所以就有了加權隨機。
加權隨機
加權隨機,雖然還是采用的隨機算法,但是為每台服務器設置了權重,權重大的服務器獲得的概率大一些,權重小的服務器獲得的概率小一些。
關於加權隨機的算法,有兩種實現方式:
一種是網上流傳的,代碼比較簡單:構建一個服務器的List,如果A服務器的權重是2,那么往List里面Add兩次A服務器,如果B服務器的權重是7,那么我往List里面Add7次B服務器,以此類推,然后我再生成一個隨機數,隨機數的上限就是權重的總和,也就是List的Size。這樣權重越大的,被選中的概率當然越高,代碼如下:
public class Servers { public HashMap<String, Integer> map = new HashMap<>() { { put("192.168.1.1", 2); put("192.168.1.2", 7); put("192.168.1.3", 1); } }; }
public class WeightRandom { static Servers servers = new Servers(); static Random random = new Random(); public static String go() { var ipList = new ArrayList<String>(); for (var item : servers.map.entrySet()) { for (var i = 0; i < item.getValue(); i++) { ipList.add(item.getKey()); } } int allWeight = servers.map.values().stream().mapToInt(a -> a).sum(); var number = random.nextInt(allWeight); return ipList.get(number); } public static void main(String[] args) { for (var i = 0; i < 15; i++) { System.out.println(go()); } } }
運行結果:
可以很清楚的看到,權重小的服務器被選中的概率相對是比較低的。
當然我在這里僅僅是為了演示,一般來說,可以把構建服務器List的代碼移動到靜態代碼塊中,不用每次都構建。
這種實現方式相對比較簡單,很容易就能想到,但是也有缺點,如果我幾台服務器權重設置的都很大,比如上千,上萬,那么服務器List也有上萬條數據,這不是白白占用內存嗎?
所以聰明的程序員想到了第二種方式:
為了方便解釋,還是就拿上面的例子來說吧:
如果A服務器的權重是2,B服務器的權重是7,C服務器的權重是1:
- 如果我生成的隨機數是1,那么落到A服務器,因為1<=2(A服務器的權重)
- 如果我生成的隨機數是5,那么落到B服務器,因為5>2(A服務器的權重),5-2(A服務器的權重)=3,3<7(B服務器的權重)
- 如果我生成的隨機數是10,那么落到C服務器,因為10>2(A服務器的權重),10-2(A服務器的權重)=8,8>7(B服務器的權重),8-7(B服務器的權重)=1,
1<=1(C服務器的權重)
不知道博客對於大於小於符號,會不會有特殊處理,所以我再截個圖:
也許,光看文字描述還是不夠清楚,可以結合下面丑到爆炸的圖片來理解下:
- 如果生成的隨機數是5,那么落到第二塊區域
- 如果生成的隨機數是10,那么落到第三塊區域
代碼如下:
public class WeightRandom { static Servers servers = new Servers(); static Random random = new Random(); public static String go() { int allWeight = servers.map.values().stream().mapToInt(a -> a).sum(); var number = random.nextInt(allWeight); for (var item : servers.map.entrySet()) { if (item.getValue() >= number) { return item.getKey(); } number -= item.getValue(); } return ""; } public static void main(String[] args) { for (var i = 0; i < 15; i++) { System.out.println(go()); } } }
運行結果:
這種實現方式雖然相對第一種實現方式比較“繞”,但卻是一種比較好的實現方式,
對內存沒有浪費,權重大小和服務器List的Size也沒有關系。
輪詢
輪詢又分為三種,1.完全輪詢 2.加權輪詢 3.平滑加權輪詢
完全輪詢
public class FullRound { static Servers servers = new Servers(); static int index; public static String go() { if (index == servers.list.size()) { index = 0; } return servers.list.get(index++); } public static void main(String[] args) { for (var i = 0; i < 15; i++) { System.out.println(go()); } } }
運行結果:
完全輪詢,也是比較簡單的,但是問題和完全隨機是一樣的,所以出現了加權輪詢。
加權輪詢
加權輪詢還是有兩種常用的實現方式,和加權隨機是一樣的,在這里,我就演示我認為比較好的一種:
public class WeightRound { static Servers servers = new Servers(); static int index; public static String go() { int allWeight = servers.map.values().stream().mapToInt(a -> a).sum(); int number = (index++) % allWeight; for (var item : servers.map.entrySet()) { if (item.getValue() > number) { return item.getKey(); } number -= item.getValue(); } return ""; } public static void main(String[] args) { for (var i = 0; i < 15; i++) { System.out.println(go()); } } }
運行結果:
加權輪詢,看起來並沒什么問題,但是還是有一點瑕疵,其中一台服務器的壓力可能會突然上升,而另外的服務器卻很“悠閑,喝着咖啡,看着新聞”。我們希望雖然是按照輪詢,但是中間最好可以有交叉,所以出現了第三種輪詢算法:平滑加權輪詢。
平滑加權輪詢
平滑加權是一個算法,很神奇的算法,我們有必要先對這個算法進行講解。
比如A服務器的權重是5,B服務器的權重是1,C服務器的權重是1。
這個權重,我們稱之為“固定權重”,既然這個叫“固定權重”,那么肯定還有叫“非固定權重的”,沒錯,“非固定權重”每次都會根據一定的規則變動。
- 第一次訪問,ABC的“非固定權重”分別是 5 1 1(初始),因為5是其中最大的,5對應的就是A服務器,所以這次選到的服務器就是A,然后我們用當前被選中的服務器的權重-各個服務器的權重之和,也就是A服務器的權重-各個服務器的權重之和。也就是5-7=-2,沒被選中的服務器的“非固定權重”不做變化,現在三台服務器的“非固定權重”分別是-2 1 1。
- 第二次訪問,把第一次訪問最后得到的“非固定權重”+“固定權重”,現在三台服務器的“非固定權重”是3,2,2,因為3是其中最大的,3對應的就是A服務器,所以這次選到的服務器就是A,然后我們用當前被選中的服務器的權重-各個服務器的權重之和,也就是A服務器的權重-各個服務器的權重之和。也就是3-7=-4,沒被選中的服務器的“非固定權重”不做變化,現在三台服務器的“非固定權重”分別是-4 1 1。
- 第三次訪問,把第二次訪問最后得到的“非固定權重”+“固定權重”,現在三台服務器的“非固定權重”是1,3,3,這個時候3雖然是最大的,但是卻出現了兩個,我們選第一個,第一個3對應的就是B服務器,所以這次選到的服務器就是B,然后我們用當前被選中的服務器的權重-各個服務器的權重之和,也就是B服務器的權重-各個服務器的權重之和。也就是3-7=-4,沒被選中的服務器的“非固定權重”不做變化,現在三台服務器的“非固定權重”分別是1 -4 3。
...
以此類推,最終得到這樣的表格:
請求 | 獲得服務器前的非固定權重 | 選中的服務器 | 獲得服務器后的非固定權重 |
---|---|---|---|
1 | {5, 1, 1} | A | {-2, 1, 1} |
2 | {3, 2, 2} | A | {-4, 2, 2} |
3 | {1, 3, 3} | B | {1, -4, 3} |
4 | {6, -3, 4} | A | {-1, -3, 4} |
5 | {4, -2, 5} | C | {4, -2, -2} |
6 | {9, -1, -1} | A | {2, -1, -1} |
7 | {7, 0, 0} | A | {0, 0, 0} |
8 | {5, 1, 1} | A | {-2, 1, 1} |
當第8次的時候,“非固定權重“又回到了初始的5 1 1,是不是很神奇,也許算法還是比較繞的,但是代碼卻簡單多了:
public class Server { public Server(int weight, int currentWeight, String ip) { this.weight = weight; this.currentWeight = currentWeight; this.ip = ip; } private int weight; private int currentWeight; private String ip; public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } public int getCurrentWeight() { return currentWeight; } public void setCurrentWeight(int currentWeight) { this.currentWeight = currentWeight; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } }
public class Servers { public HashMap<String, Server> serverMap = new HashMap<>() { { put("192.168.1.1", new Server(5,5,"192.168.1.1")); put("192.168.1.2", new Server(1,1,"192.168.1.2")); put("192.168.1.3", new Server(1,1,"192.168.1.3")); } }; }
public class SmoothWeightRound { private static Servers servers = new Servers(); public static String go() { Server maxWeightServer = null; int allWeight = servers.serverMap.values().stream().mapToInt(Server::getWeight).sum(); for (Map.Entry<String, Server> item : servers.serverMap.entrySet()) { var currentServer = item.getValue(); if (maxWeightServer == null || currentServer.getCurrentWeight() > maxWeightServer.getCurrentWeight()) { maxWeightServer = currentServer; } } assert maxWeightServer != null; maxWeightServer.setCurrentWeight(maxWeightServer.getCurrentWeight() - allWeight); for (Map.Entry<String, Server> item : servers.serverMap.entrySet()) { var currentServer = item.getValue(); currentServer.setCurrentWeight(currentServer.getCurrentWeight() + currentServer.getWeight()); } return maxWeightServer.getIp(); } public static void main(String[] args) { for (var i = 0; i < 15; i++) { System.out.println(go()); } } }
運行結果:
這就是平滑加權輪詢,巧妙的利用了巧妙算法,既有輪詢的效果,又避免了某台服務器壓力突然升高,不可謂不妙。
哈希
負載均衡算法中的哈希算法,就是根據某個值生成一個哈希值,然后對應到某台服務器上去,當然可以根據用戶,也可以根據請求參數,或者根據其他,想怎么來就怎么來。如果根據用戶,就比較巧妙的解決了負載均衡下Session共享的問題,用戶小明走的永遠是A服務器,用戶小笨永遠走的是B服務器。
那么如何用代碼實現呢,這里又需要引出一個新的概念:哈希環。
什么?我只聽過奧運五環,還有“啊 五環 你比四環多一環,啊 五環 你比六環少一環”,這個哈希環又是什么鬼?容我慢慢道來。
哈希環,就是一個環!這...好像...有點難解釋呀,我們還是畫圖來說明把。
一個圓是由無數個點組成的,這是最簡單的數學知識,相信大家都可以理解吧,哈希環也一樣,哈希環也是有無數個“哈希點”構成的,當然並沒有“哈希點”這樣的說法,只是為了便於大家理解。
我們先計算出服務器的哈希值,比如根據IP,然后把這個哈希值放到環里,如上圖所示。
來了一個請求,我們再根據某個值進行哈希,如果計算出來的哈希值落到了A和B的中間,那么按照順時針算法,這個請求給B服務器。
理想很豐滿,現實很孤單,可能三台服務器掌管的“區域”大小相差很大很大,或者干脆其中一台服務器壞了,會出現如下的情況:
可以看出,A掌管的“區域”實在是太大,B可以說是“很悠閑,喝着咖啡,看着電影”,像這種情況,就叫“哈希傾斜”。
那么怎么避免這種情況呢?虛擬節點。
什么是虛擬節點呢,說白了,就是虛擬的節點...好像..沒解釋啊...還是上一張丑到爆炸的圖吧:
其中,正方形的是真實的節點,或者說真實的服務器,五邊形的是虛擬節點,或者說是虛擬的服務器,當一個請求過來,落到了A1和B1之間,那么按照順時針的規則,應該由B1服務器進行處理,但是B1服務器是虛擬的,它是從B服務器映射出來的,所以再交給B服務器進行處理。
要實現此種負載均衡算法,需要用到一個平時不怎么常用的Map:TreeMap,對TreeMap不了解的朋友可以先去了解下TreeMap,下面放出代碼:
private static String go(String client) { int nodeCount = 20; TreeMap<Integer, String> treeMap = new TreeMap(); for (String s : new Servers().list) { for (int i = 0; i < nodeCount; i++) treeMap.put((s + "--服務器---" + i).hashCode(), s); } int clientHash = client.hashCode(); SortedMap<Integer, String> subMap = treeMap.tailMap(clientHash); Integer firstHash; if (subMap.size() > 0) { firstHash = subMap.firstKey(); } else { firstHash = treeMap.firstKey(); } String s = treeMap.get(firstHash); return s; } public static void main(String[] args) { System.out.println(go("今天天氣不錯啊")); System.out.println(go("192.168.5.258")); System.out.println(go("0")); System.out.println(go("-110000")); System.out.println(go("風雨交加")); }
運行結果:
哈希負載均衡算法到這里就結束了。
最小壓力
所以的最小壓力負載均衡算法就是 選擇一台當前最“悠閑”的服務器,如果A服務器有100個請求,B服務器有5個請求,而C服務器只有3個請求,那么毫無疑問會選擇C服務器,這種負載均衡算法是比較科學的。但是遺憾的在當前的場景下無法模擬出來“原汁原味”的最小壓力負載均衡算法的。
當然在實際的負載均衡下,可能會將多個負載均衡算法合在一起實現,比如先根據最小壓力算法,當有幾台服務器的壓力一樣小的時候,再根據權重取出一台服務器,如果權重也一樣,再隨機取一台,等等。
圖解負載均衡算法及分類
什么是負載均衡?
百度詞條里的解釋是:負載均衡,英文叫Load Balance,意思就是將請求或者數據分攤到多個操作單元上進行執行,共同完成工作任務。它的目的就通過調度集群,達到最佳化資源使用,最大化吞吐率,最小化響應時間,避免單點過載的問題。
負載均衡分類
負載均衡可以根據網絡協議的層數進行分類,我們這里以ISO模型為准,從下到上分為:
物理層,數據鏈路層,網絡層,傳輸層,會話層,表示層,應用層。
當客戶端發起請求,會經過層層的封裝,發給服務器,服務器收到請求后經過層層的解析,獲取到對應的內容。
二層負載均衡
二層負債均衡是基於數據鏈路層的負債均衡,即讓負債均衡服務器和業務服務器綁定同一個虛擬IP(即VIP),客戶端直接通過這個VIP進行請求,那么如何區分相同IP下的不同機器呢?沒錯,通過MAC物理地址,每台機器的MAC物理地址都不一樣,當負載均衡服務器接收到請求之后,通過改寫HTTP報文中以太網首部的MAC地址,按照某種算法將請求轉發到目標機器上,實現負載均衡。
這種方式負載方式雖然控制粒度比較粗,但是優點是負載均衡服務器的壓力會比較小,負載均衡服務器只負責請求的進入,不負責請求的響應(響應是有后端業務服務器直接響應給客戶端),吞吐量會比較高。
三層負載均衡
三層負載均衡是基於網絡層的負載均衡,通俗的說就是按照不同機器不同IP地址進行轉發請求到不同的機器上。
這種方式雖然比二層負載多了一層,但從控制的顆粒度上看,並沒有比二層負載均衡更有優勢,並且,由於請求的進出都要經過負載均衡服務器,會對其造成比較大的壓力,性能也比二層負載均衡要差。
四層負載均衡
四層負載均衡是基於傳輸層的負載均衡,傳輸層的代表協議就是TCP/UDP協議,除了包含IP之外,還有區分了端口號,通俗的說就是基於IP+端口號進行請求的轉發。相對於上面兩種,控制力度縮小到了端口,可以針對同一機器上的不用服務進行負載。
這一層以LVS為代表。
七層負載均衡
七層負載均衡是基於應用層的負載均衡,應用層的代表協議有HTTP,DNS等,可以根據請求的url進行轉發負載,比起四層負載,會更加的靈活,所控制到的粒度也是最細的,使得整個網絡更"智能化"。例如訪問一個網站的用戶流量,可以通過七層的方式,將對圖片類的請求轉發到特定的圖片服務器並可以使用緩存技術;將對文字類的請求可以轉發到特定的文字服務器並可以使用壓縮技術。可以說功能是非常強大的負載。
這一層以Nginx為代表。
在普通的應用架構中,使用Nginx完全可以滿足需求,對於一些大型應用,一般會采用DNS+LVS+Nginx的方式進行多層次負載均衡,以上這些說明都是基於軟件層面的負載均衡,在一些超大型的應用中,還會在前面多加一層物理負載均衡,比如知名的F5。
負載均衡算法
負載均衡算法分為兩類:
一種是靜態負載均衡,一種是動態負載均衡。
靜態均衡算法:
1、輪詢法
將請求按順序輪流地分配到每個節點上,不關心每個節點實際的連接數和當前的系統負載。
優點:簡單高效,易於水平擴展,每個節點滿足字面意義上的均衡;
缺點:沒有考慮機器的性能問題,根據木桶最短木板理論,集群性能瓶頸更多的會受性能差的服務器影響。
2、隨機法
將請求隨機分配到各個節點。由概率統計理論得知,隨着客戶端調用服務端的次數增多,其實際效果越來越接近於平均分配,也就是輪詢的結果。
優缺點和輪詢相似。
3、源地址哈希法
源地址哈希的思想是根據客戶端的IP地址,通過哈希函數計算得到一個數值,用該數值對服務器節點數進行取模,得到的結果便是要訪問節點序號。采用源地址哈希法進行負載均衡,同一IP地址的客戶端,當后端服務器列表不變時,它每次都會落到到同一台服務器進行訪問。
優點:相同的IP每次落在同一個節點,可以人為干預客戶端請求方向,例如灰度發布;
缺點:如果某個節點出現故障,會導致這個節點上的客戶端無法使用,無法保證高可用。當某一用戶成為熱點用戶,那么會有巨大的流量涌向這個節點,導致冷熱分布不均衡,無法有效利用起集群的性能。所以當熱點事件出現時,一般會將源地址哈希法切換成輪詢法。
4、加權輪詢法
不同的后端服務器可能機器的配置和當前系統的負載並不相同,因此它們的抗壓能力也不相同。給配置高、負載低的機器配置更高的權重,讓其處理更多的請;而配置低、負載高的機器,給其分配較低的權重,降低其系統負載,加權輪詢能很好地處理這一問題,並將請求順序且按照權重分配到后端。
加權輪詢算法要生成一個服務器序列,該序列中包含n個服務器。n是所有服務器的權重之和。在該序列中,每個服務器的出現的次數,等於其權重值。並且,生成的序列中,服務器的分布應該盡可能的均勻。比如序列{a, a, a, a, a, b, c}中,前五個請求都會分配給服務器a,這就是一種不均勻的分配方法,更好的序列應該是:{a, a, b, a, c, a, a}。
優點:可以將不同機器的性能問題納入到考量范圍,集群性能最優最大化;
缺點:生產環境復雜多變,服務器抗壓能力也無法精確估算,靜態算法導致無法實時動態調整節點權重,只能粗糙優化。
5、加權隨機法
與加權輪詢法一樣,加權隨機法也根據后端機器的配置,系統的負載分配不同的權重。不同的是,它是按照權重隨機請求后端服務器,而非順序。
6、鍵值范圍法
根據鍵的范圍進行負債,比如0到10萬的用戶請求走第一個節點服務器,10萬到20萬的用戶請求走第二個節點服務器……以此類推。
優點:容易水平擴展,隨着用戶量增加,可以增加節點而不影響舊數據;
缺點:容易負債不均衡,比如新注冊的用戶活躍度高,舊用戶活躍度低,那么壓力就全在新增的服務節點上,舊服務節點性能浪費。而且也容易單點故障,無法滿足高可用。
動態均衡算法:
1、最小連接數法
根據每個節點當前的連接情況,動態地選取其中當前積壓連接數最少的一個節點處理當前請求,盡可能地提高后端服務的利用效率,將請求合理地分流到每一台服務器。俗稱閑的人不能閑着,大家一起動起來。
優點:動態,根據節點狀況實時變化;
缺點:提高了復雜度,每次連接斷開需要進行計數;
實現:將連接數的倒數當權重值。
2、最快響應速度法
根據請求的響應時間,來動態調整每個節點的權重,將響應速度快的服務節點分配更多的請求,響應速度慢的服務節點分配更少的請求,俗稱能者多勞,扶貧救弱。
優點:動態,實時變化,控制的粒度更細,跟靈敏;
缺點:復雜度更高,每次需要計算請求的響應速度;
實現:可以根據響應時間進行打分,計算權重。
3、觀察模式法
觀察者模式是綜合了最小連接數和最快響應度,同時考量這兩個指標數,進行一個權重的分配。
負載均衡的常用算法
https://www.cnblogs.com/saixing/p/6730201.html
https://blog.csdn.net/okiwilldoit/article/details/81738782
【雲服務器的負載均衡】https://cloud.tencent.com/document/product/214/5411
負載均衡的策略分為應用服務器和分布式緩存集群兩種適應場景。
為什么這么分呢?簡單的說,應用服務器只需要轉發請求就可以了。但分布式緩存集群,比如redis、Memcached等,更多的是需要再次讀取數據的。也正是因為這樣,當新加入一台機器后,要盡量對整個集群的影響小。
1、應用服務器
NO.1—— Random 隨機
這是最簡單的一種,使用隨機數來決定轉發到哪台機器上。
優點:簡單使用,不需要額外的配置和算法。
缺點:隨機數的特點是在數據量大到一定量時才能保證均衡,所以如果請求量有限的話,可能會達不到均衡負載的要求。
NO.2—— Round Robin 輪詢
這個也很簡單,請求到達后,依次轉發,不偏不向。每個服務器的請求數量很平均。
缺點:當集群中服務器硬件配置不同、性能差別大時,無法區別對待。引出下面的算法。
NO.3—— 隨機輪詢
所謂隨機輪詢,就是將隨機法和輪詢法結合起來,在輪詢節點時,隨機選擇一個節點作為開始位置index,此后每次選擇下一個節點來處理請求,即(index+1)%size。
這種方式只是在選擇第一個節點用了隨機方法,其他與輪詢法無異,缺點跟輪詢一樣。
NO.4—— Weighted Round Robin 加權輪詢
這種算法的出現就是為了解決簡單輪詢策略中的不足。在實際項目中,經常會遇到這樣的情況。
比如有5台機器,兩台新買入的性能等各方面都特別好,剩下三台老古董。這時候我們設置一個權重,讓新機器接收更多的請求。物盡其用、能者多勞嘛!
這種情況下,“均衡“就比較相對了,也沒必要做到百分百的平均。
Nginx的負載均衡默認算法是加權輪詢算法。
Nginx負載均衡算法簡介
有三個節點{a, b, c},他們的權重分別是{a=5, b=1, c=1}。發送7次請求,a會被分配5次,b會被分配1次,c會被分配1次。
一般的算法可能是:
1、輪訓所有節點,找到一個最大權重節點;
2、選中的節點權重-1;
3、直到減到0,恢復該節點原始權重,繼續輪詢;
這樣的算法看起來簡單,最終效果是:{a, a, a, a, a, b, c},即前5次可能選中的都是a,這可能造成權重大的服務器造成過大壓力的同時,小權重服務器還很閑。
Nginx的加權輪詢算法將保持選擇的平滑性,希望達到的效果可能是{a, b, a, a, c, a, a},即盡可能均勻的分攤節點,節點分配不再是連續的。
Nginx加權輪詢算法
1、概念解釋,每個節點有三個權重變量,分別是:
(1) weight: 約定權重,即在配置文件或初始化時約定好的每個節點的權重。
(2) effectiveWeight: 有效權重,初始化為weight。
在通訊過程中發現節點異常,則-1;
之后再次選取本節點,調用成功一次則+1,直達恢復到weight;
此變量的作用主要是節點異常,降低其權重。
(3) currentWeight: 節點當前權重,初始化為0。
2、算法邏輯
(1) 輪詢所有節點,計算當前狀態下所有節點的effectiveWeight之和totalWeight;
(2) currentWeight = currentWeight + effectiveWeight; 選出所有節點中currentWeight中最大的一個節點作為選中節點;
(3) 選中節點的currentWeight = currentWeight - totalWeight;
基於以上算法,我們看一個例子:
這時有三個節點{a, b, c},權重分別是{a=4, b=2, c=1},共7次請求,初始currentWeight值為{0, 0, 0},每次分配后的結果如下:
觀察到七次調用選中的節點順序為{a, b, a, c, a, b, a},a節點選中4次,b節點選中2次,c節點選中1次,算法保持了currentWeight值從初始值{c=0,b=0,a=0}到7次調用后又回到{c=0,b=0,a=0}。
參考文檔為:https://www.cnblogs.com/markcd/p/8456870.html
NO.5—— Weighted Random 加權隨機
加權隨機法跟加權輪詢法類似,根據后台服務器不同的配置和負載情況,配置不同的權重。
不同的是,它是按照權重來隨機選取服務器的,而非順序。
NO.6—— Least Connections 最少連接
這是最符合負載均衡算法的一個。需要記錄每個應用服務器正在處理的連接數,然后將新來的請求轉發到最少的那台上。
NO.7—— Latebcy-Aware
與方法6類似,該方法也是為了讓性能強的機器處理更多的請求,只不過方法6使用的指標是連接數,而該方法用的請求服務器的往返延遲(RTT),動態地選擇延遲最低的節點處理當前請求。該方法的計算延遲的具體實現可以用EWMA算法來實現,它使用滑動窗口來計算移動平均耗時。
Twitter的負載均衡算法基於這種思想,不過實現起來更加簡單,即P2C算法。首先隨機選取兩個節點,在這兩個節點中選擇延遲低,或者連接數小的節點處理請求,這樣兼顧了隨機性,又兼顧了機器的性能,實現很簡單。
具體參見:https://linkerd.io/1/features/load-balancing/
NO.7—— Source Hashing 源地址散列
根據請求的來源ip進行hash計算,然后對應到一個服務器上。之后所有來自這個ip的請求都由同一台服務器處理。
2、分布式緩存集群
好了,有了前面的基礎,再看分布式緩存集群就簡單多了。我們只需要多考慮兩點就夠了。
NO.1—— 取模
這是最簡單但最不實用的一個。
以redis為例,假設我們有5台機器,要想取模肯定先得轉換為數字,我們將一個請求的key轉成數字(比如CRC16算法那),比如現在五個請求轉換成的數字后對5取模分別為0、1、2、3、4,正好轉發到五台機器上。
這時意外來了,其中一台宕機了,現在集群中還有4台。之后再來請求只能對4取模。問題就暴露出來了,那之前按照5取模的數據命中的機率大大降低了,相當於每宕機一台,之前存入的數據幾乎都不能用了。
因此,不推薦此種做法。
NO.2—— 哈希
這種算法叫哈希有些籠統了,具體可以分為ip哈希和url哈希(類似原地址散列)。這里就不多說了。重點說下redis中的設計。
redis中引入了哈希槽來解決這一問題。16384個哈希槽,每次不再對集群中服務器的總數取模,而是16384這個固定的數字。然后將請求分發。這樣就可以避免其中一台服務器宕機,原有數據無法命中的問題。
NO.3—— 一致性哈希
終於到重頭戲了,不過有了前面的介紹,這個也就不難理解了。
一致性哈希在memcached中有使用,通過一個hash環來實現key到緩存服務器的映射。
這個環的長度為2^32,根據節點名稱的hash值將緩存服務器節點放在這個hash環上。
幾種簡單的負載均衡算法及其Java代碼實現
什么是負載均衡
負載均衡,英文名稱為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)法
前面幾種方法費盡心思來實現服務消費者請求次數分配的均衡,當然這么做是沒錯的,可以為后端的多台服務器平均分配工作量,最大程度地提高服務器的利用率,但是實際情況是否真的如此?實際情況中,請求次數的均衡真的能代表負載的均衡嗎?這是一個值得思考的問題。
上面的問題,再換一個角度來說就是:以后端服務器的視角來觀察系統的負載,而非請求發起方來觀察。最小連接數法便屬於此類。
最小連接數算法比較靈活和智能,由於后端服務器的配置不盡相同,對於請求的處理有快有慢,它正是根據后端服務器當前的連接情況,動態地選取其中當前積壓連接數最少的一台服務器來處理當前請求,盡可能地提高后端服務器的利用效率,將負載合理地分流到每一台機器。由於最小連接數設計服務器連接數的匯總和感知,設計與實現較為繁瑣,此處就不說它的實現了。