負載均衡演算法總結
參考:
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、觀察模式法
觀察者模式是綜合了最小連線數和最快響應度,同時考量這兩個指標數,進行一個權重的分配。