集羣構建的思路
服務的註冊
服務的提供
服務的發現
負載均衡策略
服務的註冊
要實現服務的註冊,首先我們要有一個註冊中心(這裏我們選擇主流的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我們上面知道了我們服務註冊的流程,那麼我們需要吧註冊的服務提供給客戶端。
這裏其實關鍵的問題就是如何知道哪些服務是可用的。
這裏有兩種方式:
每次都去zk裏去查詢
本地緩存一個列表
這裏我們先說一下,第一種方式每次都要走網絡的調用,效率太低,不予考慮。那麼大家可能想了一下,用第一種方式的,如果我的服務下線,或者新服務上線,我怎麼辦,這裏就引入我們的下一個步驟了:服務的發現,利用我們的服務發現機制來更新本地的緩存,這樣的話,我們每次都是在本地中去取服務的列表,節省了很大的網絡開銷。
服務的發現
上面我們說到服務的列表變更去更新本地的緩存
,那麼關鍵的是我們怎麼去實現呢。
瞭解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檢測完原來的節點了,發現原來的服務掛掉了,吧節點刪除了,那麼也就是說我這個節點除非再次重啓,否則無法正常的註冊成功了。
方式一
如果,我在註冊的時候判斷節點是否存在,不存在的話正常註冊,存在的話刪除原來的節點並且重新註冊。
但是感覺這麼做的話,感覺在某些併發情況下,可能會產生其他的新問題
方式二
這個時候我們可以提供兩種服務註冊失敗時候的策略:
第一種就是服務註冊失敗拋出異常,讓用戶知道服務沒有註冊成功。
第二種方式就是定時重試,如果註冊失敗了就過一點時間之後繼續重新嘗試註冊,到一定次數之後在選擇失敗或者其他策略。
總結
讓我們再看一下整個過程的步驟:
我們在每一個netty服務啓動的時候,吧本地的一些服務信息,比如ip地址+端口號註冊到zookeeper上
有一個對外提供netty集羣列表的服務(可以是常規的http服務),在本地緩存一個服務列表,我們的這個服務去監聽zk上對應的節點變化,如果有變化就改變自己的緩存服務列表。
客戶端每次去對外提供的服務請求,服務端根據相應的策略去選擇節點提供給客戶端(這裏我們使用輪詢的方式),客戶端根據服務端提供的節點地址去建立連接進行業務通信。
歡迎工作一到五年的Java工程師朋友們加入Java技術交流羣:659270626
羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!