SOFAStack( Scalable Open Financial Architecture Stack) 是螞蟻金服自主研發的金融級分佈式架構,包含了構建金融級雲原生架構所需的各個組件,是在金融場景裏錘鍊出來的最佳實踐。
SOFARegistry 是螞蟻金服開源的具備承載海量服務註冊和訂閱能力的、高可用的服務註冊中心,在支付寶/螞蟻金服的業務發展驅動下,近十年間已經演進至第五代。git
本文爲《剖析 | SOFARegistry 框架》第四篇,本篇做者明不二。《剖析 | SOFARegistry 框架》系列由 SOFA 團隊和源碼愛好者們出品,項目代號:<SOFA:RegistryLab/>,文末包含往期系列文章。github
GitHub 地址:https://github.com/sofastack/sofa-registryweb
在前面的章節中咱們已經提到,SOFARegistry 與其餘服務發現領域的產品相比,最大的不一樣點在於支持海量數據。本章即將講述 SOFARegistry 在支撐海量數據上的一些特性。算法
本文將從以下幾個方面進行講解:數組
在大部分的服務註冊中心繫統中,每臺服務器都存儲着全量的服務註冊數據,服務器之間經過一致性協議(paxos、Raft 等)實現數據的複製,或者採用只保障最終一致性的算法,來實現異步數據複製。這樣的設計對於通常業務規模的系統來講沒有問題,而當應用於有着海量服務的龐大的業務系統來講,就會遇到性能瓶頸。緩存
爲解決這一問題,SOFARegistry 採用了數據分片的方法。全量服務註冊數據再也不保存在單機裏,而是分佈於每一個節點中,每臺服務器保存必定量的服務註冊數據,同時進行多副本備份,從理論上實現了服務無限擴容,且實現了高可用,最終達到支撐海量數據的目的。服務器
在各類數據分片算法中,SOFARegistry 採用了業界主流的一致性 Hash 算法作數據分片,當節點動態擴縮容時,數據仍能均勻分佈,維持數據的平衡。數據結構
在數據同步時,沒有采用與 Dynamo、Casandra、Tair、Codis、Redis cluster 等項目中相似的預分片機制,而是在 DataServer 內存裏以 dataInfoId 爲粒度進行操做日誌記錄,這種實現方式在某種程度上也實現了「預分片」,從而保障了數據同步的有效性。架構
圖 1 SOFARegistry 整體架構圖框架
DataServer 模塊的各個 bean 在 JavaConfig 中統一配置,JavaConfig 類爲 DataServerBeanConfiguration, 啓動入口類爲 DataServerInitializer,該類不禁 JavaConfig 管理配置,而是繼承了 SmartLifecycle 接口,在啓動時由 Spring 框架調用其 start 方法。
該方法中調用了 DataServerBootstrap#start 方法(圖 2),用於啓動一系列的初始化服務。
從代碼中能夠看出,DataServer 服務在啓動時,會啓動 DataServer、DataSyncServer、HttpServer 三個 bolt 服務。在啓動這些 Server 之時,DataServer 註冊了一系列 Handler 來處理各種消息。
圖2 DataServerBootstrap 中的 start 方法
這幾個 Server 的做用以下:
各 Handler 具體做用如圖 3 所示:
圖 3 各 Handler 做用
同時啓動了 RaftClient 用於保障 DataServer 節點之間的分佈式一致性,啓動了各項啓動任務,具體內容如圖 4 所示:
圖 4 DataServer 各項啓動任務
各個服務的啓動監聽端口如圖 5 所示:
圖5 監聽端口
除上述的啓動服務以外,還有一些 bean 在模塊啓動時被初始化, 系統初始化時的 bean 都在 DataServerBeanConfiguration 裏面經過 JavaConfig 來註冊,主要以以下幾個配置類體現(配置類會有變動,具體內容能夠參照源碼實現):
數據分片機制是 SOFARegistry 支撐海量數據的核心所在,DataServer 負責存儲具體的服務數據,數據按照 dataInfoId 進行一致性 Hash 分片存儲,支持多副本備份,保證數據的高可用。
(對一致性 Hash 算法感興趣想深刻了解的同窗能夠閱讀該算法的提出者 Karger 及其合做者的原始論文:Consistent hashing and random trees: distributed caching protocols for relieving hot spots on the World Wide Web。)
在講解 SOFARegistry 的數據分片以前,咱們先看下最簡單的傳統數據分片 Hash 算法。
在傳統的數據分片算法中,先對每一個節點的 ID 進行 1 到 K 的標號,而後再對每一個要存儲到節點上的數據使用 Hash 算法,計算以後的值對 K 取模,所得結果就是要落在的節點 ID。
該算法簡單且經常使用,不少場景中都使用該算法進行數據分片。
圖 6 傳統 Hash 分片算法
在這種算法下,當某個節點下線(如圖 6 中的 Node 2),該節點以後的全部節點須要從新標號。全部數據要從新求 Hash 值取模,再從新存儲到相應節點中。(圖 7)
在海量數據場景下,該方式將會帶來很大的性能開銷。
圖 7 傳統 Hash 分片算法,某個節點下線後將影響全局數據分佈
爲了使服務節點上下線不會影響到全局數據的分佈,在實際的生產環境中,不少系統使用的是一致性 Hash 算法進行數據分片。業界使用一致性 Hash 的表明項目有 Memcached、Twemproxy 等。
一致性 Hash 算法採用了 $$2^{32}$$ 個桶來存儲全部的 Hash 值,0 ~ $$2^{32}-1$$ 做爲取值範圍,而且造成一個環。
在圖 8 中,NodeA#一、NodeB#一、NodeC#1 分別爲 A、B、C 三個節點的 ID 通過一致性 Hash 算法的計算後落在環上的位置。
三角形爲不一樣的數據通過一致性 Hash 算法以後落在環上的位置。每一個數據通過順時針,找尋最近的一個節點,做爲數據存儲的節點。
圖 8 一致性 Hash 算法
從圖 8 中不難想到,當有節點上下線時,僅僅影響到上下線節點與該節點逆時針方向最近的一個節點之間的數據分佈。此時,只須要對掉落到這個區間內的數據重排便可。(如圖 9)
圖 9 一致性 Hash 算法中 NodeB#1 下線
該算法中,每一個節點的 ID 須要經過一致性 Hash 算法計算後映射到圓環上,以此帶來了一致性 Hash 算法的兩個特色:
圖 10 虛擬節點排布
在 SOFARegistry 中,由 ConsistentHash 類來實現一致性 Hash 類圖,如圖 11 所示:
圖 11 SOFARegistry 的一致性 Hash 類圖
在該類中,SIGN 爲 ID 的分隔符,numberOfReplicas 則是每一個節點的虛擬節點數,realNodes 爲節點列表,hashFunction 爲採用的 Hash 算法,circle 爲預分片機制中的 Hash 環。
ConsistentHash 默認採用了 MD5 摘要算法來進行 hash,同時構造函數支持 hash 函數定製化,用戶能夠定製本身的 Hash 算法。同時,該類中 circle 的實現爲 TreeMap,巧妙地使用了 TreeMap 的 tailMap() 方法來實現一致性 Hash 的節點查找能力,數據最近的節點 hash 值計算代碼如圖 12 所示:
圖 12 數據最近節點 hash 值計算方法
傳統的一致性 Hash 算法有數據分佈範圍不固定的特性,該特性使得服務註冊數據在服務器節點宕機、下線、擴容以後,須要從新存儲排布,這爲數據的同步帶來了困難。大多數的數據同步操做是利用操做日誌記錄的內容來進行的,傳統的一致性 Hash 算法中,數據的操做日誌是以節點分片來劃分的,節點變化致使數據分佈範圍的變化。
在計算機領域,大多數難題均可以經過增長一箇中間層來解決,那麼對於數據分佈範圍不固定所致使的數據同步難題,也能夠經過一樣的思路來解決。
這裏的問題在於,當節點下線後,若再以當前存活節點 ID 一致性 Hash 值去同步數據,就會致使已失效節點的數據操做日誌沒法獲取到,既然數據存儲在會變化的地方沒法進行數據同步,那麼若是把數據存儲在不會變化的地方是否就能保證數據同步的可行性呢?答案是確定的,這個中間層就是預分片層,經過把數據與預分片這個不會變化的層相互對應就能解決這個數據同步的難題。
目前業界主要表明項目如 Dynamo、Casandra、Tair、Codis、Redis
cluster 等,都採用了預分片機制來實現這個不會變化的層。
事先將數據存儲範圍等分爲 N 個 slot 槽位,數據直接與 slot 相對應,數據的操做日誌與相應的 solt 對應,slot 的數目不會由於節點的上下線而產生變化,由此保證了數據同步的可行性。除此以外,還須要引進「路由表」的概念,如圖 13,「路由表」負責存放每一個節點和 N 個 slot 的映射關係,並保證儘可能把全部 slot 均勻地分配給每一個節點。這樣,當節點上下線時,只須要修改路由表內容便可。保持 slot 不變,即保證了彈性擴縮容,也大大下降了數據同步的難度。
圖 13 預分片機制
SOFARegistry 爲了實現服務註冊數據的分佈式存儲,採用了基於一致性 Hash 的數據分片。而因爲歷史緣由,爲了實現數據在節點間的同步,則採用了在 DataServer 之間以 dataInfoId 爲粒度進行數據同步。
當 DataServer 節點初始化成功後,會啓動任務自動去鏈接 MetaServer。該任務會往事件中心 EventCenter 註冊一個 DataServerChangeEvent 事件,該事件註冊後會被觸發,以後將對新增節點計算 Hash 值,同時進行納管分片。
DataServerChangeEvent 事件被觸發後,由 DataServerChangeEventHandler 來進行相應的處理,分別分爲以下一些步驟:
圖 14 初始化一致性 Hash 環
圖 15 獲取變動了的 DataServer 節點
至此,節點初始化以及分片入 Hash 環的工做已經完成。
數據節點相關數據,儲存在 Map 中,相關的數據結構如圖 16 所示。
圖 16 DataServer 節點一致性 Hash 存儲結構
當服務上線時,會計算新增服務的 dataInfoId Hash 值,從而對該服務進行分片,最後尋找最近的一個節點,存儲到相應的節點上。
前文已經說過,DataServer 服務在啓動時添加了 publishDataProcessor 來處理相應的服務發佈者數據發佈請求,該 publishDataProcessor 就是 PublishDataHandler。當有新的服務發佈者上線,DataServer 的 PublishDataHandler 將會被觸發。
該 Handler 首先會判斷當前節點的狀態,如果非工做狀態則返回請求失敗。如果工做狀態,則觸發數據變化事件中心 DataChangeEventCenter 的 onChange 方法。
DataChangeEventQueue 中維護着一個 DataChangeEventQueue 隊列數組,數組中的每一個元素是一個事件隊列。當上文中的 onChange 方法被觸發時,會計算該變化服務的 dataInfoId 的 Hash 值,從而進一步肯定出該服務註冊數據所在的隊列編號,進而把該變化的數據封裝成一個數據變化對象,傳入到隊列中。
DataChangeEventQueue#start 方法在 DataChangeEventCenter 初始化的時候被一個新的線程調用,該方法會源源不斷地從隊列中獲取新增事件,而且進行分發。新增數據會由此添加進節點內,實現分片。
SOFARegistry 是 Client、SessionServer、DataServer 三層架構,同時經過 MetaServer 管理 Session 和 Data 集羣,在服務註冊的過程當中,數據既有層間的數據同步,也有層內的節點間同步。
Client 端在本地內存內已經存儲了須要訂閱和發佈的服務數據,在鏈接上 Session 後會回放訂閱和發佈數據給 Session,最終再發布到 Data。同時,Session 存儲着客戶端發佈的全部 Pub 數據,按期經過數據比對保持和 Data 一致性。當數據發生變動時,持有數據一方的 Data 發起變動通知,須要同步的 SessionServer 進行版本對比,在判斷出數據須要更新時,將拉取最新的數據操做日誌。
操做日誌存儲採用堆棧方式,獲取日誌是經過當前版本號在堆棧內所處位置,把全部版本以後的操做日誌同步過來執行。
爲保障 Data 層數據的可用性,SOFARegistry 作了 Data 層的多副本機制。當有 Data 節點縮容、宕機發生時,備份節點能夠當即經過備份數據生效成爲主節點,對外提供服務,而且把相應的備份數據再按照新列表計算備份給新的節點。
當有 Data 節點擴容時,新增節點進入初始化狀態,期間禁止新數據寫入,對於讀取請求會轉發到後續可用的 Data 節點獲取數據。在其餘節點的備份數據按照新節點信息同步完成後,新擴容的 Data 節點狀態變成 Working,開始對外提供服務。
在海量服務註冊場景下,爲保障 DataServer 可否無限擴容面對海量數據的業務場景,與其餘服務註冊中心不一樣的是,SOFARegistry 採用了一致性 Hash 算法進行數據分片,保障了數據的可擴展性。同時,經過在 DataServer 內存裏以 dataInfoId 的粒度記錄操做日誌,而且在 DataServer 之間也是以 dataInfoId 的粒度去作數據同步,保障了數據的一致性。
公衆號:金融級分佈式架構(Antfin_SOFA)