第七色在线视频,2021少妇久久久久久久久久,亚洲欧洲精品成人久久av18,亚洲国产精品特色大片观看完整版,孙宇晨将参加特朗普的晚宴

ZooKeeper 實現(xiàn)負載均衡

1. 前言

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

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

2. 負載均衡的策略

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

  • Round Robin 輪詢策略

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

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

    /**
     * 輪詢策略 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++) {
                // 根據(jù)數(shù)組長度取模,順序獲取地址索引
                int i1 = i % serverList.length;
                // 根據(jù)索引獲取服務(wù)器地址
                System.out.println(serverList[i1]);
            }
        }
    }
    

    執(zhí)行 main 方法,查看控制臺輸出:

    192.168.0.77
    192.168.0.88
    192.168.0.99
    192.168.0.77
    192.168.0.88
    

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

  • Random 隨機策略

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

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

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

    執(zhí)行 main 方法,查看控制臺輸出:

    192.168.0.88
    192.168.0.88
    192.168.0.99
    192.168.0.77
    192.168.0.77
    

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

  • Consistent Hashing 一致性哈希策略

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

一致性哈希

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

在示例的圖中,哈希環(huán)上有 3 個 Server 的 Hash 值,每個請求的 Hash 值都順時針去尋找 Server 的 Hash 值,找到哪個就將請求分配給哪個服務(wù)。接下來我們用 Java 實現(xiàn)一致性哈希策略,使用 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();
                // 根據(jù) key 獲取 Server address
                String address = serverHashMap.get(key);
                System.out.println("請求 " + request + " 被分配給服務(wù) " + address);
            } else {
                // 如果 serverHashMap 中沒有比 requestHash 大的 key
                // 則直接在 serverHashMap 取第一個服務(wù)
                Integer key = serverHashMap.firstKey();
                // 根據(jù) key 獲取 Server address
                String address = serverHashMap.get(key);
                System.out.println("請求 " + request + " 被分配給服務(wù) " + address);
            }
        }
    }
}

執(zhí)行 main 方法,查看控制臺輸出:

請求 192.168.0.10 被分配給服務(wù) 192.168.0.15
請求 192.168.0.20 被分配給服務(wù) 192.168.0.30
請求 192.168.0.40 被分配給服務(wù) 192.168.0.45
請求 192.168.0.50 被分配給服務(wù) 192.168.0.15
  • 加權(quán)輪詢策略

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

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

  • 加權(quán)隨機策略

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

  • Least Connection 最小連接數(shù)策略

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

學(xué)習完負載均衡的策略,接下來我們使用 Zookeeper 實現(xiàn)負載均衡。

3. Zookeeper 實現(xiàn)負載均衡

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

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

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

3.1 臨時節(jié)點和最小連接數(shù)策略實現(xiàn)負載均衡

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

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

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

我們使用在 Zookeeper Curator 一節(jié)創(chuàng)建的 Spring Boot 測試項目來實現(xiàn):

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

    @Autowired
    private CuratorService curatorService;

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

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

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

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

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

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

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

    @Autowired
    private CuratorService curatorService;

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

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

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

    /**
     * 增加該服務(wù)的請求會話數(shù)量
     * 使用服務(wù)地址處理業(yè)務(wù)前調(diào)用此方法
     *
     * @param ip 服務(wù)地址
     * @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());
    }

    /**
     * 減少該服務(wù)的請求會話數(shù)量
     * 請求結(jié)束時調(diào)用此方法減少會話數(shù)量
     *
     * @param ip 服務(wù)地址
     * @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 的臨時節(jié)點完成了一個簡單的最小連接數(shù)策略的負載均衡。

4. 總結(jié)

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

  1. 分布式環(huán)境下為什么要使用負載均衡。
  2. 負載均衡的策略有哪些。
  3. 使用 Zookeeper 的臨時節(jié)點實現(xiàn)負載均衡。