
什么是注冊中心?
注冊中心, 也稱命名服務(Naming servive), 它的核心功能與DNS服務類似, 無非就是通過一個特定的名字來查找相關的實例集合, 但是它們也有很多不同點
1. DNS中的配置是靜態的一個ip或多個ip, 而注冊中心中是動態變化的實例列表
2. DNS無法為ip添加元信息, 而注冊中心可以根據需求為實例添加元信息
3. DNS無法保證解析到的IP是可用的, 而注冊中心可以利用健康檢查機制來保證實例是可用的
注冊中心的適用場景
當我們需要訪問一個集群服務, 但是這個服務集群中的實例地址不是固定的, 又或者說各個實例的可用狀態是不固定的, 這種情況下就需要用到注冊中心了
主要場景: 微服務 (SpringCloud / Dubbo 等)
常見的注冊中心
Zookeeper / Eureka / Consul / Nacos / Etcd ...
如何實現一個注冊中心?
設計實現一個產品, 首先我們得知道它的核心功能是什么, 其次是它的衍生需求有哪些
核心功能
1. 注冊 - 可以將自己的訪問地址注冊到注冊中心
2. 發現 - 可以從注冊中心獲取指定服務的訪問地址
衍生需求
1. 健康檢測 - 自動剔除不可用的實例
2. 元信息 - 用於服務分組/狀態管理/自定義打標等
3. 數據管理 - 了解當前注冊中心中所有實例的狀態
4. 異常告警 - 當服務實例出現突發異常時能及時感知
5. ...
服務端設計

通訊模塊
這個模塊定義了客戶端與服務端之間的通訊協議, 以完成注冊/發現過程中的數據交換, 並維護客戶端的會話
注冊中心的通訊協議有幾種選擇:
1. HTTP (Rest)協議
2. WebSocket 長鏈接協議
3. TCP 自定義私有協議 (如: Dubbo)
4. 多種協議混合·
這里我們希望實例的變更能實時的推送到客戶端,所以選擇了基於Netty實現的WebSocket來作為服務端與客戶端之間的通訊協議,
客戶端在運行期間會始終與服務端保持長鏈接

這有一個好處就是服務端可以主動給客戶端推送數據變更的消息, 並且客戶端異常斷開連接時服務端可以及時感知,
但也有一定的弊端, 如果沒有做良好的優化, 服務端短時間內頻繁向客戶端發送變更消息會使得客戶端的性能有一定影響
請求處理模塊
這個模塊服務處理客戶端的消息, 這里包含兩個過程, 1: 將請求路由給對應的Action; 2: 由Action完成對客戶端請求的邏輯處理
關於請求路由: 我們知道SpringMvc有一個DispatcherServlet和一套RequestMapping注解來自動將HTTP請求路由到具體的邏輯執行器上, 我們可以借鑒它的原理自己實現一套將Websocket消息轉發到不同執行器上的功能組件, 這個組件已經開源在github上 -
傳送

有了分發組件的支持, 我們只需要專心編寫我們的處理邏輯就可以了, 像下面這個樣子 (是不是跟SpringMvc的注解很像呢~~)

客戶端數據同步模塊
這個模塊負責處理所有向客戶端同步數據的行為, 如: 推送全量訂閱數據 / 推送增量變更數據 等
它作為客戶端消息推送的收口, 可以最大程度的保障推送的有序性, 同時可以在這里做一系列的優化(如: 延遲推送機制) 來減少推送對客戶端性能造成的影響

一致性保障線程: 用來監控每一條推送消息, 確認客戶端已經收到消息並處理完成, 如果在一定時間內沒有收到客戶端的反饋, 那么意味着本次推送已經丟失, 此時如果重推本條消息有可能會因為順序問題而導致客戶端數據不一致, 所以在這種情況下, 一致性保障線程會觸發一次向此客戶端推送全量數據的事件, 來將此客戶端的數據進行訂正
數據存儲模塊
數據存儲模塊中保存了所有注冊上來的實例以及它們的元信息, 它相當於是服務端的數據中心, 所有的讀取與寫入都是基於這個數據中心

我們將注冊數據存儲在內存的ConcurrentHashMap中, 其中key為實例id, value則為實例的完整信息
同時當數據發生變更時, 會觸發相應的DataCenterListener, 來執行相對應的監聽邏輯, 比如有一個新的實例加入進來了, 那么對應的監聽邏輯就是把這個實例同步給所有訂閱這個實例所屬服務的客戶端
加入注冊中心服務端為集群模式部署, 那將會通過DatasourcePublisher將增量變更同步至外部數據源, 由服務端數據同步模塊來將數據變更同步至其他注冊中心節點, 這一塊我們下面會繼續展開說明
服務端數據同步模塊
此模塊專門解決注冊中心在集群部署時節點間的數據同步問題
如果是單機模塊部署的注冊中心, 其實上面幾個模塊已經完全夠用了, 並且不需要額外的數據源, 畢竟注冊中心的數據是沒有持久化的必要的, 除非數據量大到一定級別時才會使用落盤或落庫等操作
當然, 對於注冊中心這種核心服務來說, 集群部署那是必須的, 一方面是規避單點宕機問題, 另一方面是解決單機性能瓶頸, 所以我們需要設計一套數據同步機制來將集群中的節點並聯起來

跟着我來走一遍這個流程, 首先其中一個注冊中心節點的數據發生變更, 並同步到了外部數據源Zk, 此時其它節點中的ZkWatcher監聽到了數據變更的事件, 將事件丟入有序隊列(EventQueue), 隨后隊列中的事件被事件處理器(EventProcessor)處理, 將事件消息分類整理后調用數據源變更監聽器(DatasourceListener), 更改本地數據中心中的數據, 至此就完成了節點間的數據同步
注意:
- ZkWatcher在監聽到數據變更時需要檢查此事件是否由自身節點所產生, 如果是, 則忽略即可
- 其他節點將變更同步到本地數據中心時, 由於是寫操作, 如果不做區分, 會將變更再次同步到外部數據源 (參考數據存儲模塊邏輯), 這樣就造成了死循環, 所以對本地數據中心的寫操作需要告知是否需要同步到外部數據源
健康檢測
這個模塊主要負責管理所有當前注冊實例的狀態, 明確哪些實例是可用的, 哪些實例是不可用的
健康檢測可以分為客戶端上報 和 服務端探測 兩種模式, 客戶端在注冊時可以自己決定使用哪一種檢測模式, 兩種模式之間各有優劣, 我們直接來看兩種模式的實現邏輯

客戶端上報: 優點是實現簡單, 缺點是檢測結果不夠靠譜, 假設由於網絡配置原因, 其他客戶端請求無法進入此實例的情況下, 實例自身是檢測不出來的

服務端探測: 優點是檢測結果接近實際場景, 比較可靠, 缺點是對注冊中心的性能會造成影響, 其次, 該模式要求注冊中心與客戶端實例網絡互通
異常告警
這個模塊的目標是讓 開發/運維 人員第一時間知道業務服務發生的異常情況
在服務注冊發現的過程中, 其實有很多值得記錄和回溯的事件, 如: 注冊 / 注銷 / 禁用 / 不健康 / 恢復健康 / 長鏈接中斷 等等, 其中不乏一些意料之外的事件, 如: 不健康 / 長鏈接中斷 等, 當這些事件發生的時候, 我們需要第一時間知道, 以減小故障時間和范圍

從上圖可以看到, 服務端在特定的場景下都進行了日志記錄, 隨后日志文件被 日志收集系統(如: ELK) 收集並轉儲特定數據庫, 隨后, 我們可以在 定時任務調度平台 創建一個任務, 定期去掃描數據庫中的異常事件, 如果發現新的異常事件數據, 則調用用戶觸達平台相關服務進行告警發送(如: 短信/釘釘/飛書 等)
數據管理
數據管理是方便我們對注冊中心的數據進行查閱和管理
當有了數據以后, 我們希望能有一個途徑來對數據進行查看
比如: 我想知道現在有多少個服務? 每個服務有多少個實例? 有多少不健康的實例?
除了查看, 在特定情況下我們需要對數據進行管理
比如: 我想禁用一個實例來對他進行dump

對於數據管理的需求, 還是比較簡單粗暴的, 我們創建一個Web前端服務 和 一個給其提供接口的 Protal服務, 注冊中心則為Portal提供底層數據操作接口, 就這樣數據管理的流程也就通了