最近在作分佈式相關的工做,因爲人手不夠只能我一我的來懟;看着這段時間的加班表想一想就是夠慘的。java
不過其中也有遇到的很多有意思的事情從此再拿來分享,今天重點來討論服務的註冊與發現。node
個人業務比較簡單,只是須要知道如今有哪些服務實例可供使用就能夠了(並非作遠程調用,只須要拿到信息便可)。git
要實現這一功能最簡單的方式能夠在應用中配置全部的服務節點,這樣每次在使用時只須要經過某種算法從配置列表中選擇一個就能夠了。github
但這樣會有一個很是嚴重的問題:算法
因爲應用須要根據應用負載狀況來靈活的調整服務節點的數量,這樣個人配置就不能寫死。apache
否則就會出現要麼新增的節點沒有訪問或者是已經 down 掉的節點卻有請求,這樣確定是不行的。api
每每要解決這類分佈式問題都須要一個公共的區域來保存這些信息,好比是否能夠利用 Redis?緩存
每一個節點啓動以後都向 Redis 註冊信息,關閉時也刪除數據。服務器
其實就是存放節點的 ip + port
,而後在須要知道服務節點信息時候只須要去 Redis 中獲取便可。網絡
以下圖所示:
但這樣會致使每次使用時都須要頻繁的去查詢 Redis,爲了不這個問題咱們能夠在每次查詢以後在本地緩存一份最新的數據。這樣優先從本地獲取確實能夠提升效率。
但一樣又會出現新的問題,若是服務提供者的節點新增或者刪除消費者這邊根本就不知道狀況。
要解決這個問題最早想到的應該就是利用定時任務按期去更新服務列表。
以上的方案確定不完美,而且不優雅。主要有如下幾點:
因此咱們須要一個更加靠譜的解決方案,這樣的場景其實和 Dubbo 很是相似。
用過的同窗確定對這張圖不陌生。
引用自 Dubbo 官網
其中有一塊很是核心的內容(紅框出)就是服務的註冊與發現。
一般來講消費者是須要知道服務提供者的網絡地址(ip + port)才能發起遠程調用,這塊內容和我上面的需求其實很是相似。
而 Dubbo 則是利用 Zookeeper 來解決問題。
在具體討論怎麼實現以前先看看 Zookeeper 的幾個重要特性。
Zookeeper 實現了一個相似於文件系統的樹狀結構:
這些節點被稱爲 znode(名字叫什麼不重要),其中每一個節點均可以存放必定的數據。
最主要的是 znode 有四種類型:
root-1
)考慮下上文使用 Redis 最大的一個問題是什麼?
其實就是不能實時的更新服務提供者的信息。
那利用 Zookeeper 是怎麼實現的?
主要看第三個特性:瞬時節點
Zookeeper 是一個典型的觀察者模式。
這樣咱們就能夠實時獲取服務節點的信息,同時也只須要在第一次獲取列表時緩存到本地;也不須要頻繁和 Zookeeper 產生交互,只用等待通知更新便可。
而且無論應用什麼緣由節點 down 掉後也會在 Zookeeper 中刪除該信息。
這樣實現方式就變爲這樣。
爲此我新建了一個應用來進行演示:
https://github.com/crossoverJie/netty-action/tree/master/netty-action-zk
就是一個簡單的 SpringBoot 應用,只是作了幾件事情。
我在本地啓動了兩個應用分別是:127.0.0.1:8083,127.0.0.1:8084
。來看看效果圖。
兩個應用啓動完成:
當前 Zookeeper 的可視化樹狀結構:
當想知道全部的服務節點信息時:
想要獲取一個可用的服務節點時:
這裏只是採起了簡單的輪詢。
當 down 掉一個節點時:應用會收到通知更新本地緩存。同時 Zookeeper 中的節點會自動刪除。
再次獲取最新節點時:
當節點恢復時天然也能獲取到最新信息。本地緩存也會及時更新。
實現起來倒也比較簡單,主要就是 ZKClient 的 api 使用。
貼幾段比較核心的吧。
啓動註冊 Zookeeper。
主要邏輯都在這個線程中。
/route
根節點,建立的時候會判斷是否已經存在。/route
,這樣才能在其餘服務上下線時候得到通知。監聽到服務變化
public void subscribeEvent(String path) { zkClient.subscribeChildChanges(path, new IZkChildListener() { @Override public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception { logger.info("清除/更新本地緩存 parentPath=【{}】,currentChilds=【{}】", parentPath,currentChilds.toString()); //更新全部緩存/先刪除 再新增 serverCache.updateCache(currentChilds) ; } }); }
能夠看到這裏是更新了本地緩存,該緩存採用了 Guava 提供的 Cache,感興趣的能夠查看以前的源碼分析。
/** * 更新全部緩存/先刪除 再新增 * * @param currentChilds */ public void updateCache(List<String> currentChilds) { cache.invalidateAll(); for (String currentChild : currentChilds) { String key = currentChild.split("-")[1]; addCache(key); } }
同時在客戶端提供了一個負載算法。
其實就是一個輪詢的實現:
/** * 選取服務器 * * @return */ public String selectServer() { List<String> all = getAll(); if (all.size() == 0) { throw new RuntimeException("路由列表爲空"); } Long position = index.incrementAndGet() % all.size(); if (position < 0) { position = 0L; } return all.get(position.intValue()); }
固然這裏能夠擴展出更多的如權重、隨機、LRU 等算法。
Zookeeper 天然是一個很棒的分佈式協調工具,利用它的特性還能夠有其餘做用。
在實現註冊、發現這一需求時,Zookeeper 其實並非最優選。
因爲 Zookeeper 在 CAP 理論中選擇了 CP(一致性、分區容錯性),當 Zookeeper 集羣有半數節點不可用時是不能獲取到任何數據的。
對於一致性來講天然沒啥問題,但在註冊、發現的場景下更加推薦 Eureka
,已經在 SpringCloud 中獲得驗證。具體就不在本文討論了。
但鑑於個人使用場景來講 Zookeeper 已經可以勝任。
本文全部完整代碼都託管在 GitHub。
https://github.com/crossoverJie/netty-action。
一個看似簡單的註冊、發現功能實現了,但分佈式應用遠遠不止這些。
因爲網絡隔離以後帶來的一系列問題還須要咱們用其餘方式一一完善;後續會繼續更新分佈式相關內容,感興趣的朋友不妨持續關注。
你的點贊與轉發是最大的支持。