如何組裝一個註冊中心

来源:https://www.cnblogs.com/zhuochongdashi/archive/2022/07/27/16523709.html
-Advertisement-
Play Games

hello,大家好呀,我是小樓。今天不寫BUG,來聊一聊註冊中心。 標題本來想叫《如何設計一個註冊中心》,但網上已經有好多類似標題的文章了。所以打算另闢蹊徑,換個角度,如何組裝一個註冊中心。 組裝意味著不必從0開始造輪子,這也比較符合許多公司對待自研基礎組件的態度。 知道如何組裝一個註冊中心有什麼用 ...


hello,大家好呀,我是小樓。今天不寫BUG,來聊一聊註冊中心。

標題本來想叫《如何設計一個註冊中心》,但網上已經有好多類似標題的文章了。所以打算另闢蹊徑,換個角度,如何組裝一個註冊中心。

組裝意味著不必從0開始造輪子,這也比較符合許多公司對待自研基礎組件的態度。

知道如何組裝一個註冊中心有什麼用呢?

第一可以更深入理解註冊中心。以我個人經歷來說,註冊中心的第一印象就是Dubbo的Zookeeper(以下簡稱zk),後來逐漸深入,學會瞭如何去zk上查看Dubbo註冊的數據,並能排查一些問題。後來瞭解了Nacos,才發現,原來註冊中心還可以如此簡單,再後來一直從事服務發現相關工作,對一些細枝末節也有了一些新的理解。

第二可以學習技術選型的方法,註冊中心中的每個模塊,都會在不同的需求下有不同的選擇,最終的選擇取決於對需求的把握以及技術視野,但這兩項是內功,一時半會練不成,學個選型的方法還是可以的。

本文打算從需求分析開始,一步步拆解各個模塊,整個註冊中心以一種如無必要,勿增實體的原則進行組裝,但也不會是個玩具,向生產可用對齊。

當然在實際項目中,不建議重覆造輪子,儘量用現成的解決方案,所以本文僅供學習參考。

需求分析

image

本文的註冊中心需求很簡單,就三點:可註冊能發現高可用

服務的註冊和發現是註冊中心的基本功能,高可用則是生產環境的基本要求,如果高可用不要求,那本文可講解的內容就很少,上圖中的高可用標註只是個示意,高可用在很多方面都有體現。

至於其他花里胡哨的功能,我們暫且不表。

我們這裡介紹三個角色,後文以此為基礎:

  • 提供者(Provider):服務的提供方(被調用方)
  • 消費者(Consumer):服務的消費方(調用方)
  • 註冊中心(Registry):本文主角,服務提供列表、消費關係等數據的存儲方

介面定義

註冊中心和客戶端(SDK)的交互介面有三個:

  • 註冊(register),將服務提供方註冊到註冊中心
  • 註銷(unregister),將註冊的服務從註冊中心中刪除
  • 訂閱(subscribe),服務消費方訂閱需要的服務,訂閱後提供方有變更將通知到對應的消費方

註冊、註銷可以是服務提供方的進程發起,也可以是其他的旁路程式輔助發起,比如發佈系統在發佈一臺機器完成後,可調用註冊介面,將其註冊到註冊中心,註銷也是類似流程,但這種方式並不多見,而且如果只考慮實現一個註冊中心,必然是可以單獨運行的,所以通常註冊、註銷由提供方進程負責。

有了這三個介面,我們該如何去定義介面呢?註冊服務到底有哪些欄位需要註冊?訂閱需要傳什麼欄位?以什麼序列化方式?用什麼協議傳輸?

這些問題接踵而來,我覺得我們先不急著去做選擇,先看看這個領域有沒有相關標準,如果有就參考或者直接按照標準實現,如果沒有,再來分析每一點的選擇。

服務發現還真有一套標準,但又不完全有。它叫OpenSergo,它其實是服務治理的一套標準,包含了服務發現:

OpenSergo 是一套開放、通用的、面向分散式服務架構、覆蓋全鏈路異構化生態的服務治理標準,基於業界服務治理場景與實踐形成通用標準規範。OpenSergo 的最大特點就是以統一的一套配置/DSL/協議定義服務治理規則,面向多語言異構化架構,做到全鏈路生態覆蓋。無論微服務的語言是 Java, Go, Node.js 還是其它語言,無論是標準微服務還是 Mesh 接入,從網關到微服務,從資料庫到緩存,從服務註冊發現到配置,開發者都可以通過同一套 OpenSergo CRD 標準配置針對每一層進行統一的治理管控,而無需關註各框架、語言的差異點,降低異構化、全鏈路服務治理管控的複雜度。

官網:https://opensergo.io/

我們需要的服務註冊與發現也被納入其中:

image

說有但也不是完全有是因為這個標準還在建設中,服務發現相關的標準在寫這篇文章的時候還沒有給出。

既然沒有標準,可以結合現有的系統以及經驗來定義,這裡我用json的序列化方式給出,以下為筆者的總結,不能囊括所有情形,需要時根據業務適當做一些調整:

  1. 服務註冊入參
{
  "application":"provider_test", // 應用名
  "protocol":"http", // 協議
  "addr":"127.0.0.1:8080", // 提供方的地址
  "meta":{ // 攜帶的元數據,以下三個為示例
    "cluster":"small",
    "idc":"shanghai",
    "tag":"read"
  }
}
  1. 服務訂閱入參
{
    "subscribes":[
        {
            "provider":"test_provider1", // 訂閱的應用名
            "protocol":"http", // 訂閱的協議
            "meta":{ // 攜帶的元數據,以下為示例
                "cluster":"small",
                "idc":"shanghai",
                "tag":"read"
            }
        },
        {
            "provider":"test_provider2",
            "protocol":"http",
            "meta":{
                "cluster":"small",
                "tag":"read"
            }
        }
    ]
}
  1. 服務發現出參
{
    "version":"23des4f", // 版本
    "endpoints":[ // 實例
        {
            "application":"provider_test",
            "protocol":"http",
            "addr":"127.0.0.1:8080",
            "meta":{
                "cluster":"small",
                "idc":"shanghai",
                "tag":"read"
            }
        },
        {
            "application":"provider_test",
            "protocol":"http",
            "addr":"127.0.0.2:8080",
            "meta":{
                "cluster":"small",
                "idc":"shanghai",
                "tag":"read"
            }
        }
    ]
}

變更推送 & 服務健康檢查

有了定義,我們如何選擇序列化方式?選擇序列化方式有兩個重要參考點:

  • 語言的適配程度,比如 json 幾乎所有編程語言都能適配。除非能非常確定5-10年內不會有多語言的需求,否則我還是非常建議你選擇一個跨語言的序列化協議
  • 性能,序列化的性能包含了兩層意思,序列化的速度(cpu消耗)與序列化後的體積,設想一個場景,一個服務被非常多的應用訂閱,如果此時該服務發佈,則會觸發非常龐大的推送事件,此時註冊中心的cpu和網路則有可能被打滿,導致服務不可用

至於編程語言的選擇,我覺得應該更加偏向團隊對語言的掌握,以能hold住為最主要,這點沒什麼好說的,一般也只會在 Java / Go 中去選,很少見用其他語言實現的註冊中心。

對於註冊、訂閱介面,無論是基於TCP的自定義私有協議,還是用HTTP協議,甚至基於HTTP2的gRPC我覺得都可以。

但變更推送這個技術點的實現,有多種實現方式:

  1. 定時輪詢,每隔一段時間向註冊中心請求查詢訂閱的服務提供列表
  2. 長輪詢,向註冊中心查詢訂閱的服務提供列表,如果列表較上次沒有變化,則服務端hold住請求,等待有變化或者超時(較長時間)才返回
  3. UDP推送,服務列表有變化時通過UDP將事件通知給客戶端,但UDP推送不一定可靠,可能會丟失、亂序,故要配合定時輪詢(較長時間間隔)來作為一個兜底
  4. TCP長連接推送,客戶端與註冊中心建立一個TCP長連接,有變更時推送給客戶端

從實現的難易、實時性、資源消耗三個方面來比較這四種實現方式:

實現難易 實時性 資源消耗 備註
定時輪詢 簡單 實時性越高,資源消耗越多
長輪詢 中等 中等 服務端hold住很多請求
UDP推送 中等 推送可能丟失,需要配合定時輪詢(間隔較長)
TCP長連接推送 中等 中等 服務端需要保持很多長連接

似乎我們不好抉擇到底使用哪種方式來做推送,但以我自己的經驗來看,定時輪詢應該首先被排除,因為即便是一個初具規模的公司,定時輪詢的消耗也是巨大的,更何況這種消耗隨著實時性以及服務的規模日漸龐大,最後變得不可維護。

剩下三種方案都可以選擇,我們可以繼續結合服務節點的健康檢查來綜合判斷。

服務啟動時註冊到註冊中心,當服務停止時,從註冊中心摘除,通常摘除會藉助劫持kill信號實現,如果是Java則有封裝好的ShutdownHook,當進程被 kill 時,觸發劫持邏輯,從註冊中心摘除,實現優雅退出。

但事情不總是如預期,如果有人執行了kill -9強制殺死進程,或者機器出現硬體故障,會導致提供者還在註冊中心,但已無法提供服務。

此時需要一種健康檢查機制來確保服務宕機時,消費者能正常感知,從而切走流量,保證線上服務的穩定性。

關於健康檢查機制,在之前的文章《服務探活的五種方式》中有專門的總結,這裡也列舉一下,以便做出正確的選擇:

優點 缺點
消費者被動探活 不依賴註冊中心 需在服務調用處實現邏輯;用真實流量探測,可能會有滯後性
消費者主動探活 不依賴註冊中心 需在服務調用處實現邏輯
提供者上報心跳 對調用無入侵 需消費者服務發現模塊實現邏輯,服務端處理心跳消耗資源大
註冊中心主動探測 對客戶端無要求 資源消耗大,實時性不高
提供者與註冊中心會話保持 實時性好,資源消耗少 與註冊中心需保持TCP長連接

我們暫時無法控制調用動作,故而前2項依賴消費者的方案排除,提供者上報心跳如果規模較小還好,上點規模也會不堪重任,這點在Nacos中就體現了,Nacos 1.x版本使用提供者上報心跳的方式保持服務健康狀態,由於每次上報健康狀態都需要寫入數據(最後健康檢查時間),故對資源的消耗是非常大的,所以Nacos 2.0版本後就改為了長連接會話保持健康狀態。

所以健康檢查我個人比較傾向最後兩種方案:註冊中心主動探測提供者與註冊中心會話保持的方式。

結合上述變更推送,我們發現如果實現了長連接,好處將很多,很多情況下,一個服務既是消費者,又是提供者,此時一條TCP長連接可以解決推送和健康檢查,甚至在註冊註銷介面的實現,我們也可以復用這條連接,可謂是一石三鳥。

長連接技術選型

長連接的技術選型,在《Nacos架構與原理》這本電子書中有有詳細的介紹,我覺得這部分堪稱技術選型的典範,我們參考下,本節內容大量參考《Nacos架構與原理》,如有雷同,那便是真是雷同。

首先是長連接的核心訴求:

image

圖來自《Nacos架構與原理》

  • 低成本快速感知:客戶端需要在服務端不可用時儘快地切換到新的服務節點,降低不可用時間
    • 客戶端正常重啟:客戶端主動關閉連接,服務端實時感知
    • 服務端正常重啟 : 服務端主動關閉連接,客戶端實時感知
  • 防抖:網路短暫不可用,客戶端需要能接受短暫網路抖動,需要一定重試機制,防止集群抖動,超過閾值後需要自動切換 server,但要防止請求風暴
  • 斷網:斷網場景下,以合理的頻率進行重試,斷網結束時可以快速重連恢復
  • 低成本多語言實現:在客戶端層面要儘可能多的支持多語言,降低多 語言實現成本
  • 開源社區:文檔,開源社區活躍度,使用用戶數等,面向未來是否有足夠的支持度

據此,我們可選的輪子有:

gRPC Rsocket Netty Mina
客戶端感知斷連 基於 stream 流 error complete 事件可實現 支持 支持 支持
服務端感知斷連 支持 支持 支持 支持
心跳保活 應用層自定義,ping-pong 消息 自定義 kee palive frame TCP+ 自定義 自定義 kee palive filter
多語言支持 一般 只Java 只Java

我比較傾向gRPC,而且gRPC的社區活躍度要強於Rsocket。

數據存儲

註冊中心數據存儲方案,大致可分為2類:

  • 利用第三方組件完成,如Mysql、Redis等,好處是有現成的水平擴容方案,穩定性強;壞處是架構變得複雜
  • 利用註冊中心本身來存儲數據,好處是無需引入額外組件;壞處是需要解決穩定性問題

第一種方案我們不必多說,第二種方案中最關鍵的就是解決數據在註冊中心各節點之間的同步,因為在數據存儲在註冊中心本身節點上,如果是單機,機器故障或者掛掉,數據存在丟失風險,所以必須得有副本。

數據不能丟失,這點必須要保證,否則穩定性就無從談起了。保證數據不丟失怎麼理解?在客戶端向註冊中心發起註冊請求後,收到正常的響應,這就意味著數據存儲了起來,除非所有註冊中心節點故障,否則數據就一定要存在。

如下圖,比如提供者往一個節點註冊數據後,正常響應,但是數據同步是非同步的,在同步完成前,nodeA節點就掛掉,則這條註冊數據就丟失了。

image

所以,我們要極力避免這種情況。

而一致性演算法(如raft)就解決了這個問題,一致性演算法能保證大部分節點是正常的情況下,能對外提供一致的數據服務,但犧牲了性能和可用性,raft演算法在選主時便不能對外提供服務。

有沒有退而求其次的演算法呢?還真有,像Nacos、Eureka提供的AP模型,他們的核心點在於客戶端可以recover數據,也就是註冊中心追求最終一致性,如果某些數據丟失,服務提供方是可以重新將數據註冊上來。

比如我們將提供方與註冊中心之間設計為長連接,提供方註冊服務後,連接的節點還沒來得及將數據同步到其他節點就掛了,此時提供方的連接也會斷開,當連接重新建立時,服務提供方可以重新註冊,恢復註冊中心的數據。

對於註冊中心選用AP、還是CP模型,業界早有爭論,但也基本達成了共識,AP要優於CP,因為數據不一致總比不可用要好吧?你說是不是。

高可用

其實高可用的設計散落在各個細節點,如上文提到的數據存儲,其基本要求就是高可用。除此之外,我們的設計也都必須是面向失敗的設計。

假設我們的伺服器會全部掛掉,怎樣才能保持服務間的調用不受影響?

通常註冊中心不侵入服務調用,而是在記憶體(或磁碟)中緩存一份服務列表,當註冊中心完全掛了,大不了這份緩存不再更新,但也不影響現有的服務調用,但新應用啟動就會受到影響。

總結

本文內容略多,用一幅圖來總結:

image

組裝一個線上可用的註冊中心最小集,從需求分析出發,每一步都有許多選擇,本文通過一些核心的技術選型來描繪出一個大致藍圖,剩下的工作就是用代碼將這些組裝起來。

其中有些細節,我在之前的文章中有提及,這裡也一併推薦,感謝大家的閱讀,如果稍有收穫,麻煩點個在看,你的支持是我創作的最大動力~

搜索關註微信公眾號"捉蟲大師",後端技術分享,架構設計、性能優化、源碼閱讀、問題排查、踩坑實踐。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 技術棧是 Vue 的同學,在面試中難免會被問到 Vue2 和 Vue3 的相關知識點的實現原理和比較,面試官是步步緊逼,一環扣一環。 Vue2 的響應式原理是怎麼樣的? Vue3 的響應式原理又是怎麼樣的? Vue2 中是怎麼監測數 ...
  • 本文將介紹用於佈局的容器組件,使用 `Flexbox` 功能將其所控制區域設定為特定的佈局,方便快速搭建頁面的基本結構。 ...
  • 1、 Vue概述 Vue (讀音/vju/, 類似於view)是一套用於構建用戶界面的漸進式框架,發佈於2014年2月。 與其它大型框架不同的是,Vue被設計為可以自底向上逐層應用。 Vue的核心庫只關註視圖層,不僅易於上手,還便於與第三方庫(如: vue-router: 跳轉,vue-resour ...
  • vue項目導航菜單問題 目標:橫向菜單點擊跳轉,顏色變換,刷新可保持狀態 // 模板template中通過迴圈菜單列表生成,動態類名改變顏色 <li v-for="(item, index) in navList" :key="index" v-text="item.name" :class="{ ...
  • 1 WebRTC音視頻通話功能簡介 本文介紹如何基於WebRTC快速實現一個簡單的實時音視頻通話。 在開始之前,您可以先瞭解一些實時音視頻推拉流相關的基礎概念: 流:一組按指定編碼格式封裝的音視頻數據內容。一個流可以包含幾個軌道,比如視頻和音頻軌道。 推流:把採集階段封包好的音視頻數據流推送到 ZE ...
  • 首先,瞭解預解析之前先看兩個問題 1.大家思考下 這個結果會是多少呢? console.log(num); var num=10; 結果是 undefined 2.這個輸出結果又會是多少呢? fun(); var fun=function(){ console.log(22); } 顯然這個結果報錯 ...
  • 條件控制語句及表達式 運算符及表達式 1.()前面不能直接用++ console.log(++(a--)); //() 不能和++ 一起使用 2.str 與 Number值比較(字元串比較時會自動變為Number值) console.log('123A'>213);//false 自動轉為numbe ...
  • 在電腦發展的早期,一直都是集中式計算,計算能力依賴大型電腦。隨著互聯網的發展,繁重的業務需要巨大的計算能力才能完成,而集中式計算無法滿足要求,大型電腦的價格也非常昂貴。分散式計算將任務分解成更小的部分,分配給多台電腦處理,這樣可以節約整體計算時間,大大提高計算效率。互聯網大型網站往往面臨高並... ...
一周排行
    -Advertisement-
    Play Games
  • 使用原因: 在我們服務端調用第三方介面時,如:支付寶,微信支付,我們服務端需要模擬http請求並加上一些自己的邏輯響應給前端最終達到我們想要的效果 1.使用WebClient 引用命名空間 using System.Net; using System.Collections.Specialized; ...
  • WPF 實現帶蒙版的 MessageBox 消息提示框 WPF 實現帶蒙版的 MessageBox 消息提示框 作者:WPFDevelopersOrg 原文鏈接: https://github.com/WPFDevelopersOrg/WPFDevelopers.Minimal 框架使用大於等於.N ...
  • 一、JSON(JavaScript Object Notation)的簡介: ① JSON和XML類似,主要用於存儲和傳輸文本信息,但是和XML相比,JSON更小、更快、更易解析、更易編寫與閱讀。 ② C、Python、C++、Java、PHP、Go等編程語言都支持JSON。 二、JSON語法規則: ...
  • 1.避免Scoped模式註冊的服務變成Singleton模式 當提供一個生命周期模式為Singleton的服務實例時,如果發現該服務中還依賴生命周期模式為Scoped的服務實例(Scoped服務實例將被一個Singleton服務實例所引用),那麼這個被依賴的Scoped服務實例最終會成為一個Sing ...
  • 索引時資料庫提高數據查詢處理性能的一個非常關鍵的技術,索引的使用可以對性能產生上百倍甚至上千倍的影響。接下來,會介紹索引的基本原理、概念,並深入學習資料庫中所使用的索引結構和存儲方式,以及如何管理、維護索引等。 1.索引的基本概念 索引時用來快速查詢表記錄的一種存儲結構,一般使用索引有一下兩個方面: ...
  • django2 路由控制器 Route路由,是一種映射關係。路由是把客戶端請求的url路徑和用戶請求的應用程式,這裡意指django裡面的視圖進行綁定映射的一種關係。 請求路徑和視圖函數不是一一對應的關係 在django中所有的路由最終都被保存到一個叫urlpatterns的文件里,並且該文件必須在 ...
  • 1、我們的目標是獲取微博某博主的全部圖片、視頻 2、拿到網址後 我們先觀察 打開F12 隨著下滑我們發現載入出來了一個叫mymblog的東西,展開響應發現需要的東西就在裡面 3、重點來了!!! 通過觀察發現第二頁比第一頁多了參數since_id 而第二頁的since_id參數剛好在上一頁中能獲取到, ...
  • 一、實現原理 在Servlet3協議規範中,包含在JAR文件/META-INFO/resources/路徑下的資源可以直接訪問。 二、舉例說明 如下圖所示,是我新建的一個Spring Boot Starter項目:zimug-minitor-threadpool,用於實現可配置、可觀測的線程池。其中 ...
  • 精華筆記: static final常量:應用率高 必須聲明同時初始化 由類名打點來訪問,不能被改變 建議:常量所有字母都大寫,多個單詞用_分隔 編譯器在編譯時會將常量直接替換為具體的數,效率高 何時用:數據永遠不變,並且經常使用 抽象方法: 由abstract修飾 只有方法的定義,沒有具體的實現( ...
  • Python有一個for...else語法,它的寫法如下 for i in range(0,100): if i == 3: break else: print("Not found") 該語句表示:若for迴圈遍歷完畢,則執行else部分的語句。也就是說上述代碼不會有任何輸出,而下述代碼會輸出“N ...