亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

ZooKeeper 實現負載均衡

1. 前言

在分布式的環境中,我們常常使用集群部署的方式來提高某個服務的可用性,為了讓高并發的請求能夠平均的分配到集群中的每一個服務,避免有些服務壓力過大,而有些服務處于空閑狀態這樣的情況,我們需要制定一些規則來把請求進行路由,這種分配請求的做法就叫做負載均衡,路由請求的規則就是負載均衡的策略。

那么負載均衡的策略有哪些呢?如何使用 Zookeeper 實現負載均衡呢?接下來我們就帶著這些問題開始本節的內容。

2. 負載均衡的策略

當我們使用集群的方式部署的服務在不同的機器上時,根據機器的性能以及網絡環境,我們可能需要使用負載均衡策略來分配請求到不同的機器,這里我們就開始講解負載均衡的策略。

  • Round Robin 輪詢策略

    輪詢策略,按照集群的服務列表的順序,依次進行請求的分配,直到列表中所有的服務都分配了一次請求,就完成了一輪的請求分配,然后再從第一個服務開始分配請求。

    輪詢策略是很多負載均衡技術的默認策略,這樣的方式保證了的每個服務所承受的請求壓力是平均的,我們可以把服務列表按照順序放到一個數組來循環分配請求。

    /**
     * 輪詢策略 Demo
     */
    public class RoundRobinStrategy {
        public static void main(String[] args) {
            // 模擬 Server 地址列表
            String[] serverList = {"192.168.0.77","192.168.0.88","192.168.0.99"};
            // 模擬 5 次請求
            for (int i = 0; i < 5; i++) {
                // 根據數組長度取模,順序獲取地址索引
                int i1 = i % serverList.length;
                // 根據索引獲取服務器地址
                System.out.println(serverList[i1]);
            }
        }
    }
    

    執行 main 方法,查看控制臺輸出:

    192.168.0.77
    192.168.0.88
    192.168.0.99
    192.168.0.77
    192.168.0.88
    

    我們可以觀察到控制臺輸出的服務地址是順序的。

  • Random 隨機策略

    隨機策略,顧名思義就是根據隨機算法把請求隨機的分配給服務列表中的任意一個服務。

    隨機策略的實現方式:我們可以把服務列表放到一個數組,然后根據數組的長度來獲取隨機數,取到的隨機數就是服務在數組中的索引,根據這個索引,我們就可以拿到服務地址來發送請求了。

    /**
     * 隨機策略 Demo
     */
    public class RandomStrategy {
        public static void main(String[] args) {
            // 服務地址數組
            String[] serverList = {"192.168.0.77","192.168.0.88","192.168.0.99"};
            // 模擬發送 5 次請求
            for (int j = 0; j < 5; j++) {
                // 隨機獲取數組的索引
                int i = new Random().nextInt(serverList.length);
                // 根據索引獲取服務器地址
                System.out.println(serverList[i]);
            }
        }
    }
    
    

    執行 main 方法,查看控制臺輸出:

    192.168.0.88
    192.168.0.88
    192.168.0.99
    192.168.0.77
    192.168.0.77
    

    我們可以觀察到控制臺輸出的服務地址是隨機的,還有可能會出現多次請求連續隨機到同一個服務的情況。

  • Consistent Hashing 一致性哈希策略

    一致性哈希策略的實現方式:我們先把服務列表中的地址進行哈希計算,把計算后的值放到哈希環上,接收到請求后,根據請求的固定屬性值來進行哈希計算,然后根據請求的哈希值在哈希環上順時針尋找服務地址的哈希值,尋找到哪個服務地址的哈希值,就把請求分配給哪個服務。

一致性哈希

Tips: 哈希環的范圍,從 0 開始,到 2 的32 次方減 1 結束,也就是到 Integer 的最大取值范圍。

在示例的圖中,哈希環上有 3 個 Server 的 Hash 值,每個請求的 Hash 值都順時針去尋找 Server 的 Hash 值,找到哪個就將請求分配給哪個服務。接下來我們用 Java 實現一致性哈希策略,使用 IP 地址進行 Hash 計算:

/**
 * 一致性哈希策略 Demo
 */
public class ConsistentHashingStrategy {
    public static void main(String[] args) {
        // 模擬 Server 地址列表
        String[] serverList = {"192.168.0.15", "192.168.0.30", "192.168.0.45"};
        // 新建 TreeMap 集合 ,以 Key,Value 的方式綁定 Hash 值與地址
        SortedMap<Integer, String> serverHashMap = new TreeMap<>();
        // 計算 Server 地址的 Hash 值
        for (String address : serverList) {
            int serverHash = Math.abs(address.hashCode());
            // 綁定 Hash 值與地址
            serverHashMap.put(serverHash, address);
        }
        // 模擬 Request 地址
        String[] requestList = {"192.168.0.10", "192.168.0.20", "192.168.0.40", "192.168.0.50"};
        // 計算 Request 地址的 Hash 值
        for (String request : requestList) {
            int requestHash = Math.abs(request.hashCode());
            // 在 serverHashMap 中尋找所有大于 requestHash 的 key
            SortedMap<Integer, String> tailMap = serverHashMap.tailMap(requestHash);
            //如果有大于 requestHash 的 key, 第一個 key 就是離 requestHash 最近的 serverHash
            if (!tailMap.isEmpty()) {
                Integer key = tailMap.firstKey();
                // 根據 key 獲取 Server address
                String address = serverHashMap.get(key);
                System.out.println("請求 " + request + " 被分配給服務 " + address);
            } else {
                // 如果 serverHashMap 中沒有比 requestHash 大的 key
                // 則直接在 serverHashMap 取第一個服務
                Integer key = serverHashMap.firstKey();
                // 根據 key 獲取 Server address
                String address = serverHashMap.get(key);
                System.out.println("請求 " + request + " 被分配給服務 " + address);
            }
        }
    }
}

執行 main 方法,查看控制臺輸出:

請求 192.168.0.10 被分配給服務 192.168.0.15
請求 192.168.0.20 被分配給服務 192.168.0.30
請求 192.168.0.40 被分配給服務 192.168.0.45
請求 192.168.0.50 被分配給服務 192.168.0.15
  • 加權輪詢策略

    加權輪詢策略就是在輪詢策略的基礎上,對 Server 地址進行加權處理,除了按照服務地址列表的順序來分配請求外,還要按照權重大小來決定請求的分配次數。加權的目的是為了讓性能和網絡較好的服務多承擔請求分配的壓力。

    比如 Server_1 的權重是 3,Server_2 的權重是 2,Server_3 的權重是 1,那么在進行請求分配時,Server_1 會被分配 3 次請求,Server_2 會被分配 2 次請求,Server_3 會被分配 1 次請求,就這樣完成一輪請求的分配,然后再從 Server_1 開始進行分配。

  • 加權隨機策略

    加權隨機策略就是在隨機策略的基礎上,對 Server 地址進行加權處理,Server 地址的加權有多少,那么 Server 地址的數組中的地址就會有幾個,然后再從這個數組中進行隨機選址。

  • Least Connection 最小連接數策略

    最小連接數策略,就是根據客戶端與服務端會話數量來決定請求的分配情況,它會把請求分配到會話數量小的服務,會話的數量越少,也能說明服務的性能和網絡較好。

學習完負載均衡的策略,接下來我們使用 Zookeeper 實現負載均衡。

3. Zookeeper 實現負載均衡

Zookeeper 實現負載均衡,我們可以使用 Zookeeper 的臨時節點來維護 Server 的地址列表,然后選擇負載均衡策略來對請求進行分配。

我們回顧一下臨時節點的特性:當創建該節點的 Zookeeper 客戶端與 Zookeeper 服務端斷開連接時,該節點會被 Zookeeper 服務端移除。使用臨時節點來維護 Server 的地址列表就保證了請求不會被分配到已經停機的服務上。

在上面的講解中,輪詢策略,隨機策略和一致性哈希策略都使用 Java 簡單的實現了 Demo,那么接下來我們就使用最小連接數策略來實現請求的分配。

3.1 臨時節點和最小連接數策略實現負載均衡

首先我們需要在集群的每一個 Server 中都使用 Zookeeper 客戶端 Curator 來連接 Zookeeper 服務端,當 Server 啟動時,使用 Curator 連接 Zookeeper 服務端,并用自身的地址信息創建臨時節點到 Zookeeper 服務端。

我們還可以提供手動下線 Server 的方法,需要 Server 下線時可以手動調用刪除節點的方法,需要 Server 上線時再次使用自身的地址信息來創建臨時節點。

除了維護 Server 的地址信息外,我們還需要維護請求的會話連接數,我們可以使用節點的 data 來保存請求會話的連接數。

我們使用在 Zookeeper Curator 一節創建的 Spring Boot 測試項目來實現:

/**
 * 最小連接數策略 Demo
 * Server 服務端注冊地址
 */
@Component
public class MinimumConnectionsStrategyServer implements ApplicationRunner {

    @Autowired
    private CuratorService curatorService;

    // Curator 客戶端
    public CuratorFramework client;
    // 當前服務地址的臨時節點
    public static String SERVER_IP;
    // 當前服務地址臨時節點的父節點,節點類型為持久節點
    public static final String IMOOC_SERVER = "/imooc-server";

    /**
     * 服務啟動后自動執行
     *
     * @param args args
     * @throws Exception Exception
     */
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // Curator 客戶端開啟會話
        client = curatorService.getCuratorClient();
        client.start();
        // 注冊地址信息到 Zookeeper
        registerAddressToZookeeper();
    }

    /**
     * 注冊地址信息到 Zookeeper
     * 服務啟動時和服務手動上線時調用此方法
     *
     * @throws Exception Exception
     */
    public void registerAddressToZookeeper() throws Exception {
        // 判斷父節點是否存在,不存在則創建持久節點
        Stat stat = client.checkExists().forPath(IMOOC_SERVER);
        if (stat == null) {
            client.create().creatingParentsIfNeeded().forPath(IMOOC_SERVER);
        }
        // 獲取本機地址
        String address = InetAddress.getLocalHost().getHostAddress();
        // 創建臨時節點,節點路徑為 /IMOOC_SERVER/address,節點 data 為 請求會話數,初始化時為 0.
        // /imooc-server/192.168.0.77
        SERVER_IP = client.create()
                .withMode(CreateMode.EPHEMERAL)
                .forPath(IMOOC_SERVER + "/" + address, "0".getBytes());
    }

    /**
     * 注銷在 Zookeeper 上的注冊的地址
     * 服務手動下線時調用此方法
     *
     * @throws Exception Exception
     */
    public void deregistrationAddress() throws Exception {
        // 檢查該節點是否存在
        Stat stat = client.checkExists().forPath(SERVER_IP);
        // 存在則刪除
        if (stat != null) {
            client.delete().forPath(SERVER_IP);
        }
    }
}

在客戶端的請求調用集群服務之前,先使用 Curator 獲取 IMOOC_SERVER 下所有的臨時節點,并尋找出 data 最小的臨時節點,也就是最小連接數的服務。

在客戶端發送請求時,我們可以讓當前 Server 的請求會話數加 1,并更新到臨時節點的 data,完成請求時,我們可以讓當前 Server 的請求會話數減 1,并更新到臨時節點的 data 。

/**
 * 最小連接數策略 Demo
 * Client 客戶端發送請求
 */
@Component
public class MinimumConnectionsStrategyClient implements ApplicationRunner {

    @Autowired
    private CuratorService curatorService;

    // Curator 客戶端
    public CuratorFramework client;
    // 服務列表節點的 父節點
    public static final String IMOOC_SERVER = "/imooc-server";

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // Curator 客戶端開啟會話
        client = curatorService.getCuratorClient();
        client.start();
    }

    /**
     * 獲取最小連接數的服務
     * 發送請求前調用此方法,獲取服務地址
     *
     * @return String
     * @throws Exception Exception
     */
    public String getTheMinimumNumberOfConnectionsService() throws Exception {
        // 獲取所有子節點
        List<String> list = client.getChildren().forPath(IMOOC_SERVER);
        // 新建 Map
        Map<String, Integer> map = new HashMap<>();
        // 遍歷服務列表,保存服務地址與請求會話數的映射關系
        for (String s : list) {
            byte[] bytes = client.getData().forPath(IMOOC_SERVER + "/" + s);
            int i = Integer.parseInt(new String(bytes));
            map.put(s, i);
        }
        // 尋找 map 中會話數最小的值
        Optional<Map.Entry<String, Integer>> min = map.entrySet().stream().min(Map.Entry.comparingByValue());
        // 不為空的話
        if (min.isPresent()) {
            // 返回 服務地址 ip
            Map.Entry<String, Integer> entry = min.get();
            return entry.getKey();
        } else {
            // 沒有則返回服務列表第一個服務地址 ip
            return list.get(0);
        }
    }

    /**
     * 增加該服務的請求會話數量
     * 使用服務地址處理業務前調用此方法
     *
     * @param ip 服務地址
     * @throws Exception Exception
     */
    public void increaseTheNumberOfRequestedSessions(String ip) throws Exception {
        byte[] bytes = client.getData().forPath(IMOOC_SERVER + "/" + ip);
        int i = Integer.parseInt(new String(bytes));
        i++;
        client.setData().forPath(IMOOC_SERVER + "/" + ip, String.valueOf(i).getBytes());
    }

    /**
     * 減少該服務的請求會話數量
     * 請求結束時調用此方法減少會話數量
     *
     * @param ip 服務地址
     * @throws Exception Exception
     */
    public void reduceTheNumberOfRequestedSessions(String ip) throws Exception {
        byte[] bytes = client.getData().forPath(IMOOC_SERVER + "/" + ip);
        int i = Integer.parseInt(new String(bytes));
        i--;
        client.setData().forPath(IMOOC_SERVER + "/" + ip, String.valueOf(i).getBytes());
    }
}

這樣我們就使用 Zookeeper 的臨時節點完成了一個簡單的最小連接數策略的負載均衡。

4. 總結

在本節的內容中,我們學習了為什么要使用負載均衡,負載均衡的策略,以及使用 Zookeeper 的臨時節點來實現負載均衡。以下是本節內容的總結:

  1. 分布式環境下為什么要使用負載均衡。
  2. 負載均衡的策略有哪些。
  3. 使用 Zookeeper 的臨時節點實現負載均衡。