作者:crossoverJie;
鏈接:https://www.jianshu.com/p/891eb9060b26

背景

最近在做分佈式相關的工作,由於人手不夠只能我一個人來懟;看着這段時間的加班表想想就是夠慘的。

不過其中也有遇到的不少有意思的事情今後再拿來分享,今天重點來討論服務的註冊與發現

分佈式帶來的問題

我的業務比較簡單,只是需要知道現在有哪些服務實例可供使用就可以了(並不是做遠程調用,只需要拿到信息即可)。

要實現這一功能最簡單的方式可以在應用中配置所有的服務節點,這樣每次在使用時只需要通過某種算法從配置列表中選擇一個就可以了。

但這樣會有一個非常嚴重的問題:

由於應用需要根據應用負載情況來靈活的調整服務節點的數量,這樣我的配置就不能寫死。

不然就會出現要麼新增的節點沒有訪問或者是已經 down 掉的節點卻有請求,這樣肯定是不行的。

往往要解決這類分佈式問題都需要一個公共的區域來保存這些信息,比如是否可以利用 Redis?

每個節點啓動之後都向 Redis 註冊信息,關閉時也刪除數據。

其實就是存放節點的 ip + port,然後在需要知道服務節點信息時候只需要去 Redis 中獲取即可。

如下圖所示:

分佈式:搞定服務註冊與發現

但這樣會導致每次使用時都需要頻繁的去查詢 Redis,爲了避免這個問題我們可以在每次查詢之後在本地緩存一份最新的數據。這樣優先從本地獲取確實可以提高效率。

但同樣又會出現新的問題,如果服務提供者的節點新增或者刪除消費者這邊根本就不知道情況。

要解決這個問題最先想到的應該就是利用定時任務定期去更新服務列表。

以上的方案肯定不完美,並且不優雅。主要有以下幾點:

  • 基於定時任務會導致很多無效的更新。
  • 定時任務存在週期性,沒法做到實時,這樣就可能存在請求異常。
  • 如果服務被強行 kill,沒法及時清除 Redis,這樣這個看似可用的服務將永遠不可用!

所以我們需要一個更加靠譜的解決方案,這樣的場景其實和 Dubbo 非常類似。

用過的同學肯定對這張圖不陌生。

分佈式:搞定服務註冊與發現

引用自 Dubbo 官網

其中有一塊非常核心的內容(紅框出)就是服務的註冊與發現。

通常來說消費者是需要知道服務提供者的網絡地址(ip + port)才能發起遠程調用,這塊內容和我上面的需求其實非常類似。

而 Dubbo 則是利用 Zookeeper 來解決問題。

Zookeeper 能做什麼

在具體討論怎麼實現之前先看看 Zookeeper 的幾個重要特性。

Zookeeper 實現了一個類似於文件系統的樹狀結構:

分佈式:搞定服務註冊與發現

這些節點被稱爲 znode(名字叫什麼不重要),其中每個節點都可以存放一定的數據。

最主要的是 znode 有四種類型:

  • 永久節點(除非手動刪除,節點永遠存在)
  • 永久有序節點(按照創建順序會爲每個節點末尾帶上一個序號如:root-1)
  • 瞬時節點(創建客戶端與 Zookeeper 保持連接時節點存在,斷開時則刪除並會有相應的通知)
  • 瞬時有序節點(在瞬時節點的基礎上加上了順序)

考慮下上文使用 Redis 最大的一個問題是什麼?

其實就是不能實時的更新服務提供者的信息。

那利用 Zookeeper 是怎麼實現的?

主要看第三個特性:瞬時節點

Zookeeper 是一個典型的觀察者模式。

  • 由於瞬時節點的特點,我們的消費者可以訂閱瞬時節點的父節點。
  • 當新增、刪除節點時所有的瞬時節點也會自動更新。
  • 更新時會給訂閱者發起通知告訴最新的節點信息。

這樣我們就可以實時獲取服務節點的信息,同時也只需要在第一次獲取列表時緩存到本地;也不需要頻繁和 Zookeeper 產生交互,只用等待通知更新即可。

並且不管應用什麼原因節點 down 掉後也會在 Zookeeper 中刪除該信息。

效果演示

這樣實現方式就變爲這樣。

分佈式:搞定服務註冊與發現

爲此我新建了一個應用來進行演示:

https://github.com/crossoverJie/netty-action/tree/master/netty-action-zk

就是一個簡單的 SpringBoot 應用,只是做了幾件事情。

  • 應用啓動時新開一個線程用於向 Zookeeper 註冊服務。
  • 同時監聽一個節點用於更新本地服務列表。
  • 提供一個接口用於返回一個可有的服務節點。

我在本地啓動了兩個應用分別是:127.0.0.1:8083,127.0.0.1:8084。來看看效果圖。

兩個應用啓動完成:

分佈式:搞定服務註冊與發現

分佈式:搞定服務註冊與發現


當前 Zookeeper 的可視化樹狀結構:

分佈式:搞定服務註冊與發現


當想知道所有的服務節點信息時:

分佈式:搞定服務註冊與發現


想要獲取一個可用的服務節點時:

分佈式:搞定服務註冊與發現


這裏只是採取了簡單的輪詢。


當 down 掉一個節點時:應用會收到通知更新本地緩存。同時 Zookeeper 中的節點會自動刪除。

分佈式:搞定服務註冊與發現

分佈式:搞定服務註冊與發現


再次獲取最新節點時:


分佈式:搞定服務註冊與發現


當節點恢復時自然也能獲取到最新信息。本地緩存也會及時更新。

分佈式:搞定服務註冊與發現

分佈式:搞定服務註冊與發現


編碼實現

實現起來倒也比較簡單,主要就是 ZKClient 的 api 使用。

貼幾段比較核心的吧。

註冊

啓動註冊 Zookeeper。

分佈式:搞定服務註冊與發現

主要邏輯都在這個線程中。

  • 首先創建父節點。如上圖的 Zookeeper 節點所示;需要先創建 /route 根節點,創建的時候會判斷是否已經存在。
  • 接着需要判斷是否需要將自己註冊到 Zookeeper 中,因爲有些節點只是用於服務發現,他自身是不需要承擔業務功能(是我自己項目的需求)。
  • 將當前應用的所在 ip 以及端口註冊上去,同時需要監聽根節點 /route ,這樣才能在其他服務上下線時候獲得通知。

根據本地緩存

監聽到服務變化

 public void subscribeEvent(String path) {
zkClient.subscribeChildChanges(path, new IZkChildListener() {
@Override
public void handleChildChange(String parentPath, List currentChilds) throws Exception {
logger.info("清除/更新本地緩存 parentPath=【{}】,currentChilds=【{}】", parentPath,currentChilds.toString());
//更新所有緩存/先刪除 再新增
serverCache.updateCache(currentChilds) ;
}
});
}

可以看到這裏是更新了本地緩存,該緩存採用了 Guava 提供的 Cache,感興趣的可以查看之前的源碼分析。

 /**
* 更新所有緩存/先刪除 再新增
*
* @param currentChilds
*/
public void updateCache(List currentChilds) {
cache.invalidateAll();
for (String currentChild : currentChilds) {
String key = currentChild.split("-")[1];
addCache(key);
}
}

客戶端負載

同時在客戶端提供了一個負載算法。

其實就是一個輪詢的實現:

 /**
* 選取服務器
*
* @return
*/
public String selectServer() {
List 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 其實並不是最優選。

由於 Zookeeper 在 CAP 理論中選擇了 CP(一致性、分區容錯性),當 Zookeeper 集羣有半數節點不可用時是不能獲取到任何數據的。

對於一致性來說自然沒啥問題,但在註冊、發現的場景下更加推薦 Eureka,已經在 SpringCloud 中得到驗證。具體就不在本文討論了。

但鑑於我的使用場景來說 Zookeeper 已經能夠勝任。

總結

本文所有完整代碼都託管在 GitHub。

https://github.com/crossoverJie/netty-action。

一個看似簡單的註冊、發現功能實現了,但分佈式應用遠遠不止這些。

由於網絡隔離之後帶來的一系列問題還需要我們用其他方式一一完善;後續會繼續更新分佈式相關內容,感興趣的朋友不妨持續關注。

相關文章