SOFAStack (Scalable Open Financial Architecture Stack) 是螞蟻金服自主研發的金融級雲原生架構,包含了構建金融級雲原生架構所需的各個組件,是在金融場景裡錘鍊出來的最佳實踐。
SOFARegistry 是螞蟻金服開源的具有承載海量服務註冊和訂閱能力的、高可用的服務註冊中心,在支付寶/螞蟻金服的業務發展驅動下,近十年間已經演進至第五代。
本文為《剖析 | SOFARegistry 框架》第七篇,本篇作者明不二。《剖析 | SOFARegistry 框架》系列由 SOFA 團隊和源碼愛好者們出品,項目代號:,文末包含往期系列文章。
GitHub 地址:https://github.com/sofastack/sofa-registry
概述
在前面的文章已經做過介紹,與其他註冊中心相比,SOFARegistry 主要特點在於支持海量數據、支持海量客戶端、秒級的服務上下線通知以及高可用特性。本文將從如下幾個方面來講述 SOFARegistry 的一致性方案:
- MetaServer 數據一致性
為支持高可用特性,對於 MetaServer 來說,存儲了 SOFARegistry 的元數據,為了保障 MetaServer 集群的一致性,其採用了 Raft 協議來進行選舉和複製。
- SessionServer 數據一致性
為支持海量客戶端的連接,SOFARegistry 在客戶端與 DataServer 之間添加了一個 SessionServer 層,客戶端與 SessionServer 連接,避免了客戶端與 DataServer 之間存在大量連接所導致的連接數過多不可控的問題。客戶端通過 SessionServer 與 DataServer 連接的時候,Publisher 數據同時會緩存在 SessionServer 中,此時就需要解決 DataServer 與 SessionServer 之間數據一致性的問題。
- DataServer 數據一致性
為支持海量數據,SOFARegistry 採用了一致性 Hash 來分片存儲 Publisher 數據,避免了單個服務器存儲全量數據時產生的容量瓶頸問題。而在這個模型中,每個數據分片擁有多個副本,當存儲註冊數的 DataServer 進行擴容、縮容時,MetaServer 會把這個變更通知到 DataServer 和 SessionServer,數據分片會在集群內部進行數據遷移與同步,此時就出現了 DataServer 內部數據的一致性問題。
MetaServer 數據一致性
MetaServer 在 SOFARegistry 中,承擔著集群元數據管理的角色,用來維護集群成員列表,可以認為是 SOFARegistry 註冊中心的註冊中心。當 SessionServer 和 DataServer 需要知道集群列表,並且需要擴縮容時,MetaServer 將會提供相應的數據。
圖1 MetaServer 內部結構
圖源自 《螞蟻金服服務註冊中心 MetaServer 功能介紹和實現剖析 | SOFARegistry 解析》
因為 SOFARegistry 集群節點列表數據並不是很多,因此不需要使用數據分片的方式在 MetaServer 中存儲。如圖 1 所示,集群節點列表存儲在 Repository 中,上面通過 Raft 強一致性協議對外提供節點註冊、續約、列表查詢等 Bolt 請求,從而保障集群獲得的數據是強一致性的。
Raft 協議
關於 Raft 協議算法,具體可以參考 The Raft Consensus Algorithm 中的解釋。在 SOFA 體系中,對於 Raft 協議有 SOFAJRaft 實現。下面對 Raft 協議算法的原理進行簡要介紹。
Raft 協議由三個部分組成,領導人選舉(Leader Election)、日誌複製(Log Replication)、安全性(Safety)。
- 領導人選舉
通過一定的算法選舉出領導人,用於接受客戶端請求,並且把指令追加到日誌中。
圖2 Raft 狀態機狀態轉換圖
圖源自Understanding the Raft consensus algorithm: an academic article summary
- 日誌複製
領導人接受到客戶端請求之後,把操作追加到日誌中,同時與其他追隨者同步消息,最終 Commit 日誌,並且把結果返回給客戶端。
圖3 複製狀態機
圖源自 Raft一致性算法筆記
- 安全性
安全性保證了數據的一致性。
基於 Raft 協議的數據一致性保障
圖4 SOFARegistry 中的 Raft 存儲過程
圖源自 《螞蟻金服服務註冊中心 MetaServer 功能介紹和實現剖析 | SOFARegistry 解析》
如圖 4 所示,SOFARegistry 中的 Raft 協議數據存儲經歷瞭如上的一些流程。客戶端發起 Raft 協議調用,進行數據註冊、續約、查詢等操作時,會通過動態代理實現 ProxyHandler
類進行代理,通過 RaftClient
把數據發送給 RaftServer
,並且通過內部的狀態機 Statemachine
,最終實現數據的操作,從而保證了 MetaServer 內部的數據一致性。
SessionServer 數據一致性
SessionServer 在 SOFARegistry 中,承擔著會話管理及連接的功能。同時,Subscriber 需要通過 SessionServer 來訂閱 DataServer 的服務數據,Publisher 需要通過 SessionServer 來把服務數據發佈到 DataServer 中。
在這個場景下,SessionServer 作為中間代理層,緩存從 DataServer 中獲取的數據成了必然。DataServer 的數據需要通過 SessionServer 推送到 Subscriber 中,觸發 SessionServer 推送的場景有兩個:一個是 Publisher 到 DataServer 的數據發生變化;另外一個是 Subscriber 有了新增。
而在實際的場景中,Subscriber 新增的情況更多,在這種場景下,直接把 SessionServer 緩存的數據推送到 Subscriber 中即可,能夠大大減輕 SessionServer 從 DataServer 獲取數據對 DataServer 的壓力。因此,這也進一步確認了在 SessionServer 緩存數據的必要性。
圖5 兩種場景的數據推送對比圖
SessionServer 與 DataServer 數據對比機制
當服務 Publisher 上下線或者斷連時,相應的數據會通過 SessionServer 註冊到 DataServer 中。此時,DataServer 的數據與 SessionServer 會出現短暫的不一致性。為了保障這個數據的一致性,DataServer 與 SessionServer 之間通過 推
和 拉
兩種方式實現了數據的同步。
- 數據推送模式
DataServer 在服務數據有變化時會主動通知到 SessionServer 中,此時 SessionServer 會比對兩者數據的版本號 version
,對比之後若需要更新數據,則會主動向 DataServer 獲取相應的數據。
- 數據拉取模式
SessionServer 會每隔一定的時間(默認 30s)主動向 DataServer 查詢所有 dataInfoId
的 version
信息,若發現有版本號有變化,則會進行相應的同步操作。
- SessionServer 從 DataServer 同步數據:常規情況下,一般是 DataServer 的數據要比 SessionServer 更新,此時,當 SessionServer 發現數據版本號有變化時,會主動拉取 DataServer 的數據進行同步。注意,此時緩存的數據只與當前 SessionServer 管理的客戶端所訂閱的服務信息有關,並不會緩存全量的數據,而且容量也不允許。
- DataServer 從 SessionServer 同步數據:特殊情況下,DataServer 數據出現缺失,並且副本數據也出現問題之後,當 SessionServer 與 DataServer 數據進行版本號比對時,會觸發數據恢復操作,能夠把 SessionServer 內存中所存儲的全量數據恢復到 DataServer 中,實現了數據的反向同步與補償機制。
- 數據的緩存方式
SOFARegistry 中採用了 LoadingCache<Key, Value>
的數據結構來在 SessionServer 中緩存從 DataServer 中同步來的數據。每個 cache 中的 entry 都有過期時間,在拉取數據的時候可以設置過期時間(默認是 30s),使得 cache 定期去 DataServer 查詢當前 session 所有 sub 的 dataInfoId,對比如果 session 記錄的最近推送version(見com.alipay.sofa.registry.server.session.store.SessionInterests#interestVersions
)比 DataServer 小,說明需要推送,然後 SessionServer 主動從 DataServer 獲取該 dataInfoId 的數據(此時會緩存到 cache 裡),推送給 client。
同時,當 DataServer 中有數據更新時,也會主動向 SessionServer 發請求使對應 entry 失效,從而促使 SessionServer 去更新失效 entry。
SessionServer 與 Subscriber 之間的數據一致性同步
當 SessionServer 的數據發生變更時,會與 Subscriber 之間進行數據同步,把變化的 dataInfoId
數據推送到 Subscriber 中,保證客戶端本地所緩存的數據與 SessionServer 中的一致。
DataServer 數據一致性
DataServer 在 SOFARegistry 中,承擔著核心的數據存儲功能。數據按 dataInfoId 進行一致性 Hash 分片存儲,支持多副本備份,保證數據高可用。這一層可隨服務數據量的規模的增長而擴容。
如果 DataServer 宕機,MetaServer 能感知,並通知所有 DataServer 和 SessionServer,數據分片可 failover 到其他副本,同時 DataServer 集群內部會進行分片數據的遷移。
DataServer 請求接收過程
在講解一致性之前,先講一下 DataServer 的啟動之後關於數據同步方面做了哪些事情。DataServer 啟動之時,會啟動一個數據同步 Bolt 服務 openDataSyncServer ,進行相應的 DataServer 數據同步處理。
啟動 DataSyncServer 時,註冊瞭如下幾個 handler 用於處理 bolt 請求 :
圖5 DayaSyncServer 註冊的 Handler
- getDataHandler
該 Handler 主要用於數據的獲取,當一個請求過來時,會通過請求中的 DataCenter 和 DataInfoId 獲取當前 DataServer 節點存儲的相應數據。
- publishDataProcessor unPublishDataHandler
當有數據發佈者 publisher 上下線時,會分別觸發 publishDataProcessor 或 unPublishDataHandler ,Handler 會往 dataChangeEventCenter 中添加一個數據變更事件,用於異步地通知事件變更中心數據的變更。事件變更中心收到該事件之後,會往隊列中加入事件。此時 dataChangeEventCenter 會根據不同的事件類型異步地對上下線數據進行相應的處理。
與此同時,DataChangeHandler 會把這個事件變更信息通過 ChangeNotifier 對外發布,通知其他節點進行數據同步。
- notifyFetchDatumHandler
這是一個數據拉取請求,當該 Handler 被觸發時,通知當前 DataServer 節點進行版本號對比,若請求中數據的版本號高於當前節點緩存中的版本號,則會進行數據同步操作,保證數據是最新的。
- notifyOnlineHandler
這是一個 DataServer 上線通知請求 Handler,當其他節點上線時,會觸發該 Handler,從而當前節點在緩存中存儲新增的節點信息。用於管理節點狀態,究竟是 INITIAL 還是 WORKING 。
- syncDataHandler
節點間數據同步 Handler,該 Handler 被觸發時,會通過版本號進行比對,若當前 DataServer 所存儲數據版本號含有當前請求版本號,則會返回所有大於當前請求數據版本號的所有數據,便於節點間進行數據同步。
- dataSyncServerConnectionHandler
連接管理 Handler,當其他 DataServer 節點與當前 DataServer 節點連接時,會觸發 connect 方法,從而在本地緩存中註冊連接信息,而當其他 DataServer 節點與當前節點斷連時,則會觸發 disconnect 方法,從而刪除緩存信息,進而保證當前 DataServer 節點存儲有所有與之連接的 DataServer 節點。
最終一致性
SOFARegistry 在數據存儲層面採用了類似 Eureka 的最終一致性的過程,但是存儲內容上和 Eureka 在每個節點存儲相同內容特性不同,採用每個節點上的內容按照一致性 Hash 數據分片來達到數據容量無限水平擴展能力。
SOFARegistry 是一個 AP 分佈式系統,表明了在已有條件 P 的前提下,選擇了 A 可用性。當數據進行同步時,獲取到的數據與實際數據不一致。但因為存儲的信息為服務的註冊節點,儘管會有短暫的不一致產生,但對於客戶端來說,大概率還是能從這部分數據中找到可用的節點,不會因為數據暫時的不一致對業務系統帶來致命性的傷害。
集群內部數據遷移過程
SOFARegistry 的 DataServer 選擇了“一致性 Hash分片”來存儲數據。在“一致性 Hash分片”的基礎上,為了避免“分片數據不固定”這個問題,SOFARegistry 選擇了在 DataServer 內存裡以 dataInfoId 的粒度記錄操作日誌,並且在 DataServer 之間也是以 dataInfoId 的粒度去做數據同步。
圖6 DataServer 之間進行異步數據同步
數據和副本分別分佈在不同的節點上,進行一致性 Hash 分片,當時對主副本進行寫操作之後,主副本會把數據異步地更新到其他副本中,實現了集群內部不同副本之間的數據遷移工作。
總結
在分佈式系統的設計中,可用性、分區容錯性、一致性是我們必須進行權衡的選項,CAP 理論告訴我們,這三者中只能同時滿足兩個的要求。在設計分佈式系統時,如何進行權衡選擇,是擺在每個系統設計者面前的一個難題。
SOFARegistry 系統分為三個集群,分別是元數據集群 MetaServer、會話集群 SessionServer、數據集群 DataServer。複雜的系統有多個地方需要考慮到一致性問題,SOFARegistry 針對不同模塊的一致性需求也採取了不同的方案。對於 MetaServer 模塊來說,採用了強一致性的 Raft 協議來保證集群信息的一致性。對於數據模塊來說,SOFARegistry 選擇了 AP 保證可用性,同時保證了最終一致性。
SOFARegistry 的設計給了我們啟示,在設計一個多模塊的分佈式系統時,可以根據不同模塊的需求選擇不同的一致性方案,同時 CAP 三者的權衡也需要結合系統不同模塊的目標作出合理的權衡,不必拘泥。