netty集羣方案

集羣構建的思路

  1. 服務的註冊

  2. 服務的提供

  3. 服務的發現

  4. 負載均衡策略

服務的註冊

要實現服務的註冊,首先我們要有一個註冊中心(這裏我們選擇主流的zookeeper作爲註冊中心),然後,我們在每一個netty服務啓動的時候,吧本地的一些服務信息,比如ip地址+端口號註冊到zookeeper上。

代碼實現如下:

CuratorFramework cf;public void register(String nodeName, String ip) throws Exception {
        String path = ZK_CLUSTER_PATH + "/" + nodeName;
        Stat stat = cf.checkExists().forPath(path);        if (stat == null) {           // 如果父節點不存在就先創建父節點
           cf.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path);
        }
        System.out.println("獲取本機ip:" + ip);        // 創建對應的臨時節點
        cf.create().withMode(CreateMode.EPHEMERAL).forPath(path + "/" + ip, ip.getBytes());
    }

這裏大家應該可以看見,我們關鍵的地方就是在在於對應的ip節點的創建,那麼大家可以看見我們創建的是EPHEMERAL節點,這個節點是個臨時節點。

什麼是臨時節點?

服務啓動後創建臨時節點, 服務斷掉後臨時節點就不存在了

爲什麼使用臨時節點?

正常的思路可能是註冊的時候,我們像zk註冊一個正常的節點,然後在服務下線的時候刪除這個節點,但是這樣的話會有一個弊端。
比如我們的服務泵機了,無法去刪除臨時節點,那麼這個節點就會被我們錯誤的提供給了客戶端。
另外我們還要考慮持久化的節點創建之後刪除之類的問題,問題會更加的複雜化,所以我們使用了臨時節點。

服務的提供

ok我們上面知道了我們服務註冊的流程,那麼我們需要吧註冊的服務提供給客戶端。

這裏其實關鍵的問題就是如何知道哪些服務是可用的。
這裏有兩種方式:

  1. 每次都去zk裏去查詢

  2. 本地緩存一個列表

這裏我們先說一下,第一種方式每次都要走網絡的調用,效率太低,不予考慮。那麼大家可能想了一下,用第一種方式的,如果我的服務下線,或者新服務上線,我怎麼辦,這裏就引入我們的下一個步驟了:服務的發現,利用我們的服務發現機制來更新本地的緩存,這樣的話,我們每次都是在本地中去取服務的列表,節省了很大的網絡開銷。

服務的發現

上面我們說到服務的列表變更去更新本地的緩存,那麼關鍵的是我們怎麼去實現呢。
瞭解zk的朋友應該都知道,zk有一個watch機制,就是針對某個節點進行監聽,一點這個節點發生了變化就會受到zk的通知。
我們就是利用zk的這個watch來進行服務的上線和下線的通知,也就是我們的服務發現功能。

代碼實現如下:

    CuratorFramework cf;    public void subscribe(String nodeName) throws Exception {        //訂閱某一個服務
        final String path = ZK_CLUSTER_PATH + "/" + nodeName;
        Stat stat = cf.checkExists().forPath(path);        if (stat == null) {            // 如果父節點不存在就先創建父節點
            cf.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path);
        }
        PathChildrenCache cache = new PathChildrenCache(cf, path, true);        // 在初始化的時候就進行緩存監聽
        cache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
        cache.getListenable()
            .addListener((CuratorFramework client, PathChildrenCacheEvent event) -> {                // 重新獲取子節點
                List<String> children = client.getChildren().forPath(path);                // 排序一下子節點
                Collections.sort(children);                // 子節點重新緩存起來
                data.put(nodeName, children);
            });
    }

負載均衡策略

ok,在我們解決了服務的註冊和發現問題之後,那麼我們究竟提供給客戶端那臺服務呢,這時候就需要我們做出選擇,爲了讓客戶端能夠均勻的連接到我們的服務器上(比如有個100個客戶端,2臺服務器,每臺就分配50個),我們需要使用一個負載均衡的策略。

這裏我們使用輪詢的方式來爲每個請求的客戶端分配ip。具體的代碼實現如下:

public class RoundRobinLoadBalance {    public static final String NAME = "roundrobin";    private static final AtomicInteger sequences = new AtomicInteger();    public static String doSelect(List<String> callList) {        // 取模輪循
        return callList.isEmpty() ?            null :
            callList.get(sequences.getAndIncrement() % callList.size());
    }

}

測試下效果

我們已經完成了整個集羣的搭建了,那麼我們編寫一個測試程序,來測一下我們的效果。

完整的測試代碼如下:

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

        dataList.add("19.21.2.1");
        dataList.add("12.34.33.12");
        dataList.add("21.1.235.22");
        dataList.add("6.12.36.233");
        dataList.add("71.12.36.233");
    }    /**
     * Zookeeper info
     */

    public static void main(String[] args) throws Exception {
        String ZK_ADDRESS = "127.0.0.1:2181";
        String Node = "IM";
        String path = "/zk_cluster/IM";        // Connect to zk
        CuratorFramework client =
            CuratorFrameworkFactory.newClient(ZK_ADDRESS, new RetryNTimes(10, 5000));
        client.start();

        List<String> children = client.getChildren().forPath(path);        assert children.isEmpty();        // test
        ZkList zkList = new ZkList(client);        // 先訂閱
        zkList.subscribe(Node);        // 添加節點
        dataList.stream().forEach(ip -> {            try {
                zkList.register(Node, ip);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });        // 初始化一個統計的map,來檢測每個節點被分配的次數
        Map<String, AtomicInteger> map = new ConcurrentHashMap<>();
        dataList.stream().forEach(ip -> {            try {
                map.putIfAbsent(ip, new AtomicInteger(0));
            } catch (Exception e) {
                e.printStackTrace();
            }
        });        //Map
        for (int i = 0; i < 20000; i++) {
            String node = zkList.selectNode(Node);
            map.get(node).incrementAndGet();
        }        for (Map.Entry<String, AtomicInteger> m : map.entrySet()) {
            System.out.println(String.format("[%s]一共被執行了:%s次", m.getKey(), m.getValue().get()));
        }

    }

看一下我們的效果

[12.34.33.12]一共被執行了:4000次[21.1.235.22]一共被執行了:4000次[6.12.36.233]一共被執行了:4000次[19.21.2.1]一共被執行了:4000次[71.12.36.233]一共被執行了:4000次

我們在更改一下我們的測試程序,模擬一下節點上線和下線,吧我們去查詢節點的部分來改一下,具體的代碼如下:

        for (int i = 0; i < 20001; i++) {            if (i == 10000) {
                zkList.unRegister(Node, dataList.get(0));
            }            if (i == 15000) {
                zkList.register(Node, dataList.get(0));
            }
            String node = zkList.selectNode(Node);
            map.get(node).incrementAndGet();
            System.out.println(node);
        }

實際的效果如下:

[12.34.33.12]一共被執行了:4285次
[21.1.235.22]一共被執行了:4284次
[6.12.36.233]一共被執行了:4284次
[19.21.2.1]一共被執行了:2863次
[71.12.36.233]一共被執行了:4285次

ok大家能看到我們的請求基本被均勻的分配到了每個節點,無論有服務上線還是下線都不會過多的影響到服務的分配。

Note

大家可以看到上面做那種服務註冊的時候,按照我們現在的邏輯,先去註冊一個臨時節點,
如果這個時候我服務掛了,我瞬間重啓,zk一般對臨時節點的對應session斷開的話默認會有3次的重試判斷的,那麼我在這3次重試判斷完成之前吧我的服務重啓完了,那麼我這時候去註冊當前的臨時節點的話,又會因爲節點存在而無法創建,然後過一會zk檢測完原來的節點了,發現原來的服務掛掉了,吧節點刪除了,那麼也就是說我這個節點除非再次重啓,否則無法正常的註冊成功了。

方式一

如果,我在註冊的時候判斷節點是否存在,不存在的話正常註冊,存在的話刪除原來的節點並且重新註冊。
但是感覺這麼做的話,感覺在某些併發情況下,可能會產生其他的新問題

方式二

這個時候我們可以提供兩種服務註冊失敗時候的策略:

  • 第一種就是服務註冊失敗拋出異常,讓用戶知道服務沒有註冊成功。

  • 第二種方式就是定時重試,如果註冊失敗了就過一點時間之後繼續重新嘗試註冊,到一定次數之後在選擇失敗或者其他策略。

總結

讓我們再看一下整個過程的步驟:

  1. 我們在每一個netty服務啓動的時候,吧本地的一些服務信息,比如ip地址+端口號註冊到zookeeper上

  2. 有一個對外提供netty集羣列表的服務(可以是常規的http服務),在本地緩存一個服務列表,我們的這個服務去監聽zk上對應的節點變化,如果有變化就改變自己的緩存服務列表。

  3. 客戶端每次去對外提供的服務請求,服務端根據相應的策略去選擇節點提供給客戶端(這裏我們使用輪詢的方式),客戶端根據服務端提供的節點地址去建立連接進行業務通信。

歡迎工作一到五年的Java工程師朋友們加入Java技術交流羣:659270626
羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章