淺談負載均衡算法與實現

記得,我剛工做的時候,同事說了一個故事:在他剛工做的時候,他同事有一天興沖沖的跑到公司說,大家知道嗎,公司請了個大牛。大牛?對,那人會寫AJAX!哇,真是大牛啊,跟着他,能夠學很多東西啊。我聽了笑了,但有點難以理解,由於如今幾乎只要是一個開發,都會寫AJAX,怎麼寫個AJAX就算大牛呢?後來我明白了,三年前高深莫測的技術到如今變得普普統統,不足爲奇,就像咱們今天要講的負載均衡,在幾什麼時候,負載均衡只有大牛才能玩轉起來,可是到今天,一個小開發均可以聊上幾句。如今,就讓咱們簡單的看看負載均衡把。node

從負載均衡設備的角度來看,分爲硬件負載均衡和軟件負載均衡:程序員

  • 硬件負載均衡:好比最多見的F5,還有Array等,這些負載均衡是商業的負載均衡器,性能比較好,畢竟他們的就是爲了負載均衡而生的,背後也有很是成熟的團隊,能夠提供各類解決方案,可是價格比較昂貴,因此沒有充足的理由,充足的軟妹幣是不會考慮的。
  • 軟件負載均衡:包括咱們耳熟能詳的Nginx,LVS,Tengine(阿里對Nginx進行的改造)等。優勢就是成本比較低,可是也須要有比較專業的團隊去維護,要本身去踩坑,去DIY。

從負載均衡的技術來看,分爲服務端負載均衡和客戶端負載均衡:算法

  • 服務端負載均衡:當咱們訪問一個服務,請求會先到另一臺服務器,而後這臺服務器會把請求分發到提供這個服務的服務器,固然若是隻有一臺服務器,那好說,直接把請求給那一臺服務器就能夠了,可是若是有多臺服務器呢?這時候,就會根據必定的算法選擇一臺服務器。
  • 客戶端負載均衡:客戶端服務均衡的概念貌似是有了服務治理才產生的,簡單的來講,就是在一臺服務器上維護着全部服務的ip,名稱等信息,當咱們在代碼中訪問一個服務,是經過一個組件訪問的,這個組件會從那臺服務器上取到全部提供這個服務的服務器的信息,而後經過必定的算法,選擇一臺服務器進行請求。

從負載均衡的算法來看,又分爲 隨機,輪詢,哈希,最小壓力,固然可能還會加上權重的概念,負載均衡的算法就是本文的重點了。bash

隨機

隨機就是沒有規律的,隨便從負載中得到一臺,又分爲徹底隨機和加權隨機:服務器

徹底隨機

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());
        }
    }
}
複製代碼

運行結果: 負載均衡

image.png
雖然說如今感受並非那麼隨機,有的服務器常常被得到到,有的服務器得到的次數比較少,可是當有充足的請求次數,就會愈來愈平均,這正是隨機數的一個特性。

徹底隨機是最簡單的負載均衡算法了,缺點比較明顯,由於服務器有好有壞,處理能力是不一樣的,咱們但願性能好的服務器多處理些請求,性能差的服務器少處理一些請求,因此就有了加權隨機。dom

加權隨機

加權隨機,雖然仍是採用的隨機算法,可是爲每臺服務器設置了權重,權重大的服務器得到的機率大一些,權重小的服務器得到的機率小一些。性能

關於加權隨機的算法,有兩種實現方式:ui

一種是網上流傳的,代碼比較簡單:構建一個服務器的List,若是A服務器的權重是2,那麼往List裏面Add兩次A服務器,若是B服務器的權重是7,那麼我往List裏面Add7次B服務器,以此類推,而後我再生成一個隨機數,隨機數的上限就是權重的總和,也就是List的Size。這樣權重越大的,被選中的機率固然越高,代碼以下:this

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());
        }
    }
}
複製代碼

運行結果:

image.png

能夠很清楚的看到,權重小的服務器被選中的機率相對是比較低的。

固然我在這裏僅僅是爲了演示,通常來講,能夠把構建服務器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服務器的權重)

不知道博客對於大於小於符號,會不會有特殊處理,因此我再截個圖:

image.png

也許,光看文字描述仍是不夠清楚,能夠結合下面醜到爆炸的圖片來理解下:

image.png

  • 若是生成的隨機數是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());
        }
    }
}
複製代碼

運行結果:

image.png

這種實現方式雖然相對第一種實現方式比較「繞」,但倒是一種比較好的實現方式, 對內存沒有浪費,權重大小和服務器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());
        }
    }
}
複製代碼

運行結果:

image.png

徹底輪詢,也是比較簡單的,可是問題和徹底隨機是同樣的,因此出現了加權輪詢。

加權輪詢

加權輪詢仍是有兩種經常使用的實現方式,和加權隨機是同樣的,在這裏,我就演示我認爲比較好的一種:

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());
        }
    }
}

複製代碼

運行結果:

image.png

加權輪詢,看起來並沒什麼問題,可是仍是有一點瑕疵,其中一臺服務器的壓力可能會忽然上升,而另外的服務器卻很「清閒,喝着咖啡,看着新聞」。咱們但願雖然是按照輪詢,可是中間最好能夠有交叉,因此出現了第三種輪詢算法:平滑加權輪詢。

平滑加權輪詢

平滑加權是一個算法,很神奇的算法,咱們有必要先對這個算法進行講解。 好比A服務器的權重是5,B服務器的權重是1,C服務器的權重是1。 這個權重,咱們稱之爲「固定權重」,既然這個叫「固定權重」,那麼確定還有叫「非固定權重的」,沒錯,「非固定權重」每次都會根據必定的規則變更。

  1. 第一次訪問,ABC的「非固定權重」分別是 5 1 1(初始),由於5是其中最大的,5對應的就是A服務器,因此此次選到的服務器就是A,而後咱們用當前被選中的服務器的權重-各個服務器的權重之和,也就是A服務器的權重-各個服務器的權重之和。也就是5-7=-2,沒被選中的服務器的「非固定權重」不作變化,如今三臺服務器的「非固定權重」分別是-2 1 1。
  2. 第二次訪問,把第一次訪問最後獲得的「非固定權重」+「固定權重」,如今三臺服務器的「非固定權重」是3,2,2,由於3是其中最大的,3對應的就是A服務器,因此此次選到的服務器就是A,而後咱們用當前被選中的服務器的權重-各個服務器的權重之和,也就是A服務器的權重-各個服務器的權重之和。也就是3-7=-4,沒被選中的服務器的「非固定權重」不作變化,如今三臺服務器的「非固定權重」分別是-4 1 1。
  3. 第三次訪問,把第二次訪問最後獲得的「非固定權重」+「固定權重」,如今三臺服務器的「非固定權重」是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());
        }
    }
}
複製代碼

運行結果:

image.png

這就是平滑加權輪詢,巧妙的利用了巧妙算法,既有輪詢的效果,又避免了某臺服務器壓力忽然升高,不可謂不妙。

哈希

負載均衡算法中的哈希算法,就是根據某個值生成一個哈希值,而後對應到某臺服務器上去,固然能夠根據用戶,也能夠根據請求參數,或者根據其餘,想怎麼來就怎麼來。若是根據用戶,就比較巧妙的解決了負載均衡下Session共享的問題,用戶小明走的永遠是A服務器,用戶小笨永遠走的是B服務器。

那麼如何用代碼實現呢,這裏又須要引出一個新的概念:哈希環

什麼?我只聽過奧運五環,還有「啊 五環 你比四環多一環,啊 五環 你比六環少一環」,這個哈希環又是什麼鬼?容我慢慢道來。

哈希環,就是一個環!這...好像...有點難解釋呀,咱們仍是畫圖來講明把。

image.png

一個圓是由無數個點組成的,這是最簡單的數學知識,相信你們均可以理解吧,哈希環也同樣,哈希環也是有無數個「哈希點」構成的,固然並無「哈希點」這樣的說法,只是爲了便於你們理解。

咱們先計算出服務器的哈希值,好比根據IP,而後把這個哈希值放到環裏,如上圖所示。

來了一個請求,咱們再根據某個值進行哈希,若是計算出來的哈希值落到了A和B的中間,那麼按照順時針算法,這個請求給B服務器。

理想很豐滿,現實很孤單,可能三臺服務器掌管的「區域」大小相差很大很大,或者乾脆其中一臺服務器壞了,會出現以下的狀況:

image.png

能夠看出,A掌管的「區域」實在是太大,B能夠說是「很清閒,喝着咖啡,看着電影」,像這種狀況,就叫「哈希傾斜」。

那麼怎麼避免這種狀況呢?虛擬節點

什麼是虛擬節點呢,說白了,就是虛擬的節點...好像..沒解釋啊...仍是上一張醜到爆炸的圖吧:

image.png
其中,正方形的是真實的節點,或者說真實的服務器,五邊形的是虛擬節點,或者說是虛擬的服務器,當一個請求過來,落到了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("風雨交加"));
    }
複製代碼

運行結果:

image.png

哈希負載均衡算法到這裏就結束了。

最小壓力

因此的最小壓力負載均衡算法就是 選擇一臺當前最「清閒」的服務器,若是A服務器有100個請求,B服務器有5個請求,而C服務器只有3個請求,那麼毫無疑問會選擇C服務器,這種負載均衡算法是比較科學的。可是遺憾的在當前的場景下沒法模擬出來「原汁原味」的最小壓力負載均衡算法的。

固然在實際的負載均衡下,可能會將多個負載均衡算法合在一塊兒實現,好比先根據最小壓力算法,當有幾臺服務器的壓力同樣小的時候,再根據權重取出一臺服務器,若是權重也同樣,再隨機取一臺,等等。

今天的內容到這裏就結束了,謝謝你們。

相關文章
相關標籤/搜索