負載均衡之隨機、輪詢、一致性哈希


1、什么是負載均衡

  • 負載均衡指多台服務器以對稱的方式組成一個服務器集合,每台服務器都具有等價的地位,都可以單獨對外提供服務而無須其他服務器的輔助。
  • 通過某種負載分擔任務,將外部發送來的請求均勻分配到對稱結構中的某一台服務器上,而接受到的請求的服務器獨立地回應客戶的請求。
  • 負載均衡能夠平均分配客戶請求到服務器陣列,借此提供快速獲取重要數據,解決大量並發訪問服務問題,這種集群技術可以用最少的投資獲得接近於大型主機的性能。

負載均衡方式:

  1. 軟件負載均衡:Nginx、LVS、HAProxy
  2. 硬件負載均衡:Array、F5

2 隨機算法實現

2.1 最簡單版本:

隨機平均選擇服務器

import java.util.Arrays;
import java.util.List;

public class ServerIps {

    public static final List<String> LIST = (List<String>) 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"
            );

}


import java.util.Random;

public class RandomChoose{

    public static String getServer() {
        Random random = new Random();
        return ServerIps.LIST.get(random.nextInt(ServerIps.LIST.size()));
    }

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            System.out.println(getServer());
        }
    }

}

2.2 考慮權重的影響

但是每台機器的性能可能不同,性能強的機器能夠處理更多的業務量,這種情況平均輪詢不是很合理。
這樣的話,需要給不同的機器設置不同的權重。

public class ServerIps {

    public static final Map<String, Integer> WEIGHT_MAP = new HashMap<>();
    static {
        WEIGHT_MAP.put("192.168.0.1", 2);
        WEIGHT_MAP.put("192.168.0.2", 8);
        WEIGHT_MAP.put("192.168.0.3", 1);
        WEIGHT_MAP.put("192.168.0.4", 9);
        WEIGHT_MAP.put("192.168.0.5", 4);
        WEIGHT_MAP.put("192.168.0.6", 6);
    }   
}

2.2.1 暴力解法

創建一個新的List將所有的IP地址裝入List,注意根據權重來選擇裝入幾次IP地址,比如“198.168.0.1”這個IP地址的權重是2,那么就裝入2次該IP地址

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class WeightRandom{

    public static String getServer() {
        List<String> ips = new ArrayList<>();

        for(String ip: ServerIps.WEIGHT_MAP.keySet()) {
            Integer weight = ServerIps.WEIGHT_MAP.get(ip);
            for(int i=0; i<weight; i++) {
                ips.add(ip);
            }
        }
        Random random = new Random();
        return ips.get(random.nextInt(ips.size()));
    }

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            System.out.println(getServer());
        }
    }

}

2.2.2 優化

當權重很大的時候,將會存入很多次IP地址,耗費很大的空間
可以采用坐標映射的想法,具體如下:

其中A、B、C分別代表3個IP地址,權重分別為5、3、2 A: 5 B: 3 C: 2

映射到坐標軸為
0-----5---8--10

隨意在這個坐標軸取整數就可以確定其在哪個IP地址上
如:
offset = 7
7在5---8這個區間里面,那么對應的就是B這台服務器

具體實現思路: 
offset > 5; offset - 5; offset = 2;
offset < 3;
對應B

代碼實現:

import java.util.Random;

public class WeightRandom{

    public static String getServer() {

        int totalWeight = 0;
        for (Integer weight: ServerIps.WEIGHT_MAP.values()) {
            totalWeight += weight;
        }

        Random random = new Random();
        int offset = random.nextInt(totalWeight);

        for(String ip: ServerIps.WEIGHT_MAP.keySet()) {
            Integer weight = ServerIps.WEIGHT_MAP.get(ip);
            if (offset < weight) {
                return ip;
            }

            offset -= weight;
        }
        return null;

    }

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            System.out.println(getServer());
        }
    }

}

3.輪詢

主要思想是服務器一個接一個的服務

3.1 簡單實現

public class RoundRobin {
    private static Integer pos = 0;

    public static String getServer() {
        if(pos >= ServerIps.LIST.size()) {
            pos = 0;
        }

        String ip = ServerIps.LIST.get(pos);
        pos++;
        return ip;
    }

    public static void main(String[] args) {
        for(int i = 0; i < 20; i++) {
            System.out.println(getServer());
        }
    }

}

3.2 優化:考慮權重

考慮權重的時候

  1. 暴力解決,將=創建一個LIST,將IP地址按照權重的大小放入LIST里面
  2. 還是按照隨機優化的思想

其中A、B、C分別代表3個IP地址,權重分別為5、3、2 A: 5 B: 3 C: 2

映射到坐標軸為
0-----5---8--10

offset的取值再不是一個隨機數,而是0,1,2,3,4,5,6,7,8,9

1 ——> A
2 ——> A 
...
6 ——> B
...

代碼實現

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;


public class ServerIps {

    public static final Map<String, Integer> WEIGHT_MAP = new LinkedHashMap<>();
    static {
        WEIGHT_MAP.put("192.168.0.1", 2);
        WEIGHT_MAP.put("192.168.0.2", 8);
        WEIGHT_MAP.put("192.168.0.3", 1);
        WEIGHT_MAP.put("192.168.0.4", 9);
        WEIGHT_MAP.put("192.168.0.5", 4);
        WEIGHT_MAP.put("192.168.0.6", 6);
    }

}

注意其中采用的是LinkedHashMap,這樣可以保證輸出的順序和輸入的順序一致

public class RequestId {
    public static Integer num = 0;

    public static Integer getAndIncrement() {
        return num++;
    }

}

上面代碼模仿的是客戶的ID號,根據客戶的ID號來控制服務器的輪詢。

public class RoundRobin2 {
public static String getServer() {

        int totalWeight = 0;
        for (Integer weight: ServerIps.WEIGHT_MAP.values()) {
            totalWeight += weight;
        }

        int requestId = RequestId.getAndIncrement();
        int offset = requestId % totalWeight;

        for(String ip: ServerIps.WEIGHT_MAP.keySet()) {
            Integer weight = ServerIps.WEIGHT_MAP.get(ip);
            if (offset < weight) {
                return ip;
            }

            offset -= weight;
        }
        return null;

    }

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            System.out.println(getServer());
        }
    }

}

3.3 優化:平滑加權輪詢算法

Nginx默認采用這種算法

A: 5
B: 1
C: 1 
這樣的話,優化1中的訪問順序為AAAAABC,這樣的話對服務器A的壓力比較大

如果按照離散的話,就不會有這樣的問題,如下面這種順序
AABACAA
這樣不僅能使服務比較分散,也能保證權重,還能達到輪詢的目的

具體過程如下:

ip
weight: 靜態,5,1,1 currentWeight:動態

currentWeight: 0,0,0

 

currentWeight+=weight max(currentWeight) result max(currentWeight)-=sum(weight)7
5,1,1 5 A -2,1,1
3,2,2 3 A -4,2,2
1,3,3 3 B 1,-4,3
6,-3,4 6 A -1,-3,4
4,-2,5 5 C 4,-2,-2
9,-1,-1 9 A 2,-1,-1
7,0,0 7 A 0,0,0
5,1,1 ... ... ...

 

代碼實現

public class Weight {
    private String ip;
    private Integer weight;
    private Integer currentWeight;

    public Weight(String ip, Integer weight, Integer currentWeight) {
        super();
        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;
    }

}

=============================

import java.util.LinkedHashMap;
import java.util.Map;

public class WeightRoundRobin {
    private static Map<String, Weight> weightMap = new LinkedHashMap<>();

    public static String getServer() {
        if(weightMap.isEmpty()) {
            for(String ip: ServerIps.WEIGHT_MAP.keySet()) {
                Integer weight = ServerIps.WEIGHT_MAP.get(ip);          
                weightMap.put(ip, new Weight(ip, weight, 0));

            }
        }
        // currentWeight += weight
        for(Weight weight: weightMap.values()) {
            weight.setCurrentWeight(weight.getCurrentWeight() + weight.getWeight());
        }

        Weight maxCurrentWeight = null;
        for (Weight weight: weightMap.values()) {
            if (maxCurrentWeight == null || weight.getCurrentWeight() > maxCurrentWeight.getCurrentWeight()) {
                maxCurrentWeight = weight;
            }
        }

        // max(currentWeight) -= sum(weight)
        int totalWeight = 0;
        for (Integer weight: ServerIps.WEIGHT_MAP.values()) {
            totalWeight += weight;
        }

        maxCurrentWeight.setCurrentWeight(maxCurrentWeight.getCurrentWeight() - totalWeight);

        //result
        return maxCurrentWeight.getIp();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.println(getServer());
        }
    }
}
 

4. 一致性哈希實現負載均衡

4.1 為什么需要哈希算法

解決同一個用戶訪問服務器是,訪問的是不同的服務器的問題

場景:集群造成的session沒有同步

當一個用戶訪問服務器A的時候,該台服務器A會保存這台服務器的session,但是當下次再訪問的時候,被負載均衡算法可能算到了不同的服務器B,服務器B中沒有用戶的session,會要求用戶再次登錄。

解決:

  1. 加入redis,將session存到redis中
  2. Tomcat同步session
  3. 一致性哈希算法

4.2 什么是一致性哈希算法

服務器集群接收到一次請求調用時,可以根據請求的信息,比如客戶端的ip地址,或請求路徑與請求參數等信息進行哈希,可以得出一個哈希值,特點是對於相同的ip地址,或請求路徑和請求參數哈希出來的值是一樣的,只要能再增加一個算法,能夠把這個哈希值映射成一個服務端ip地址,就可以使用相同的請求(相同的ip地址,或請求路徑和請求參數)落到同一服務器上。

因為客戶端發起的請求是無窮無盡的(客戶端地址不同,請求參數不同等等),所以對於的哈希值也是無窮大的,所以我們不能把所有的哈希值都進行映射到服務端ip上,所有這里需要用到哈希環

4.3 虛擬節點

  1. 解決一個服務器掛掉造成的服務不均勻問題
  2. 使得哈希環更加平滑

當時當一台服務器掛掉的話,會造成服務器服務不均勻的情況

會發現,ip3和ip1直接的范圍是比較大的,會有更多的請求落到ip1上,這是“不公平”,解決這個問題需要加入虛擬節點,比如:

其中,ip2-1,ip3-1就是虛擬節點,並不能處理節點,而是等同於對應的服務器。
實際上,這只是處理這種不均衡性的一種思路,實際上就算哈希環本身是均衡的,你也可以增加更多的虛擬節點來使得這個環更加的平滑

上面的哈希環更加的散列,平滑

只需要找到大於hashcode的以一個虛擬節點即可

由於哈希環上的點是有序的,那么采用的數據結構是TreeMap

4.4 代碼實現

public class ServerIps {

    public static final List<String> LIST = (List<String>) 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"
            );

}

======================================

import java.util.SortedMap;
import java.util.TreeMap;

public class ConsistentHash {

    private static TreeMap<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+i);
                virtualNodes.put(hash, ip);
            }

        }
    }

    private static String getServer(String client) {
        int hash = getHash(client);

        //大於hash,virtualNodes的子樹的firstkey
        //tailMap,能獲得大於等於hash值的一棵子紅黑樹
        SortedMap subMap = virtualNodes.tailMap(hash);
        Integer firstKey = null;

        if(subMap == null) {
            firstKey = virtualNodes.firstKey();
        } else {
            firstKey = (Integer) subMap.firstKey();
        }

        return virtualNodes.get(firstKey);
    }

    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) {

        for (int i = 0; i < 12; i++) {
            System.out.println(getServer("client"+i));
        }
    }

}

5 最小活躍數

由於服務的時間並不是固定,如果平均分配服務器有的時候並不合理

比如:
有3個消息,分別請求了A、B、C三台服務器,處理的時間依次為3s、2s、1s,當1s后有一個新的請求過來,按道理說應該A服務器來服務,但是C台服務器已經空閑了,所有這樣的分配方式有時候並不合理。

解決:
采用最小活躍數,哪台機器服務最少,用哪台機器,當都大致相等的時候可以根據前面的隨機、輪詢等。

6 算法的魅力

    • 將程序“復雜化”,提升程序效率到極致
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM