1. 程式人生 > 其它 >從零實現一個註冊中心 - 服務端

從零實現一個註冊中心 - 服務端

什麼是註冊中心?

概念簡介

註冊中心, 也稱命名服務(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), 更改本地資料中心中的資料, 至此就完成了節點間的資料同步 注意:
  1. ZkWatcher在監聽到資料變更時需要檢查此事件是否由自身節點所產生, 如果是, 則忽略即可
  2. 其他節點將變更同步到本地資料中心時, 由於是寫操作, 如果不做區分, 會將變更再次同步到外部資料來源 (參考資料儲存模組邏輯), 這樣就造成了死迴圈, 所以對本地資料中心的寫操作需要告知是否需要同步到外部資料來源

健康檢測

這個模組主要負責管理所有當前註冊例項的狀態, 明確哪些例項是可用的, 哪些例項是不可用的
健康檢測可以分為客戶端上報 和 服務端探測 兩種模式, 客戶端在註冊時可以自己決定使用哪一種檢測模式, 兩種模式之間各有優劣, 我們直接來看兩種模式的實現邏輯 客戶端上報: 優點是實現簡單, 缺點是檢測結果不夠靠譜, 假設由於網路配置原因, 其他客戶端請求無法進入此例項的情況下, 例項自身是檢測不出來的 服務端探測: 優點是檢測結果接近實際場景, 比較可靠, 缺點是對註冊中心的效能會造成影響, 其次, 該模式要求註冊中心與客戶端例項網路互通

異常告警

這個模組的目標是讓 開發/運維 人員第一時間知道業務服務發生的異常情況
在服務註冊發現的過程中, 其實有很多值得記錄和回溯的事件, 如: 註冊 / 登出 / 禁用 / 不健康 / 恢復健康 / 長連結中斷 等等, 其中不乏一些意料之外的事件, 如: 不健康 / 長連結中斷 等, 當這些事件發生的時候, 我們需要第一時間知道, 以減小故障時間和範圍 從上圖可以看到, 服務端在特定的場景下都進行了日誌記錄, 隨後日誌檔案被 日誌收集系統(如: ELK) 收集並轉儲特定資料庫, 隨後, 我們可以在 定時任務排程平臺 建立一個任務, 定期去掃描資料庫中的異常事件, 如果發現新的異常事件資料, 則呼叫使用者觸達平臺相關服務進行告警傳送(如: 簡訊/釘釘/飛書 等)

資料管理

資料管理是方便我們對註冊中心的資料進行查閱和管理
當有了資料以後, 我們希望能有一個途徑來對資料進行檢視 比如: 我想知道現在有多少個服務? 每個服務有多少個例項? 有多少不健康的例項? 除了檢視, 在特定情況下我們需要對資料進行管理 比如: 我想禁用一個例項來對他進行dump 對於資料管理的需求, 還是比較簡單粗暴的, 我們建立一個Web前端服務 和 一個給其提供介面的 Protal服務, 註冊中心則為Portal提供底層資料操作介面, 就這樣資料管理的流程也就通了

客戶端設計

// TODO