簡介: SOFARegistry 是螞蟻金服開源的具有承載海量服務註冊和訂閱能力的、高可用的服務註冊中心,最早源自於淘寶的初版 ConfigServer,在支付寶/螞蟻金服的業務發展驅動下,近十年間已經演進至第五代。
SOFAStack
Scalable Open Financial Architecture Stack 是螞蟻金服自主研發的金融級分佈式架構,包含了構建金融級雲原生架構所需的各個組件,是在金融場景裏錘鍊出來的最佳實踐。
SOFARegistry 是螞蟻金服開源的具有承載海量服務註冊和訂閱能力的、高可用的服務註冊中心,最早源自於淘寶的初版 ConfigServer,在支付寶/螞蟻金服的業務發展驅動下,近十年間已經演進至第五代。
3 月 31 日,螞蟻金服正式開源了內部演進了近 10 年的註冊中心產品-SOFARegistry。先前的文章介紹了 SOFARegistry 的演進之路,而本文主要對 SOFARegistry 整體架構設計進行剖析,並着重介紹一些關鍵的設計特點,期望能幫助讀者對 SOFARegistry 有更直接的認識。
服務註冊中心是什麼
不可免俗地,先介紹一下服務註冊中心的概念。對此概念已經瞭解的讀者,可選擇跳過本節。
如上圖,服務註冊中心最常見的應用場景是用於 RPC 調用的服務尋址,在 RPC 遠程過程調用中,存在 2 個角色,一個服務發佈者(Publisher)、另一個是服務訂閱者(Subscriber)。Publisher 需要把服務註冊到服務註冊中心(Registry),發佈的內容通常是該 Publisher 的 IP 地址、端口、調用方式 (協議、序列化方式)等。而 Subscriber 在第一次調用服務時,會通過 Registry 找到相應的服務的 IP 地址列表,通過負載均衡算法從 IP 列表中取一個服務提供者的服務器調用服務。同時 Subscriber 會將 Publisher 的服務列表數據緩存到本地,供後續使用。當 Subscriber 後續再調用 Publisher 時,優先使用緩存的地址列表,不需要再去請求Registry。
如上圖,Subscriber 還需要能感知到 Publisher 的動態變化。比如當有 Publisher 服務下線時, Registry 會將其摘除,隨後 Subscriber 感知到新的服務地址列表後,不會再調用該已下線的 Publisher。同理,當有新的 Publisher 上線時,Subscriber 也會感知到這個新的 Publisher。
初步認識
在理解了常見的服務註冊中心的概念之後,我們來看看螞蟻金服的 SOFARegistry 長什麼樣子。如上圖,SOFARegistry 包含 4 個角色:
- Client
提供應用接入服務註冊中心的基本 API 能力,應用系統通過依賴客戶端 JAR 包,通過編程方式調用服務註冊中心的服務訂閱和服務發佈能力。
- SessionServer
會話服務器,負責接受 Client 的服務發佈和服務訂閱請求,並作爲一箇中間層將寫操作轉發 DataServer 層。SessionServer 這一層可隨業務機器數的規模的增長而擴容。
- DataServer
數據服務器,負責存儲具體的服務數據,數據按 dataInfoId 進行一致性 Hash 分片存儲,支持多副本備份,保證數據高可用。這一層可隨服務數據量的規模的增長而擴容。
- MetaServer
元數據服務器,負責維護集羣 SessionServer 和 DataServer 的一致列表,作爲 SOFARegistry 集羣內部的地址發現服務,在 SessionServer 或 DataServer 節點變更時可以通知到整個集羣。
產品特點
(圖片改編自 https://luyiisme.github.io/2017/04/22/spring-cloud-service-discovery-products )
首先放一張常見的服務註冊中心的特性對比,可以看出,在這些 Feature 方面,SOFARegistry 並不佔任何優勢。那麼,我們爲什麼還開源這樣的一個系統?SOFARegistry 開源的優勢是什麼?下面將着重介紹 SOFARegistry 的特點。
支持海量數據
大部分的服務註冊中心繫統,每臺服務器都是存儲着全量的服務註冊數據,服務器之間依靠一致性協議(如 Paxos/Raft/2PC 等)實現數據的複製,或者只保證最終一致性的異步數據複製。“每臺服務器都存儲着全量的服務註冊數據”,在一般規模下是沒問題的。但是在螞蟻金服龐大的業務規模下,服務註冊的數據總量早就超過了單臺服務器的容量瓶頸。
SOFARegistry 基於一致性 Hash 做了數據分片,每臺 DataServer 只存儲一部分的分片數據,隨數據規模的增長,只要擴容 DataServer 服務器即可。這是相對服務發現領域的其他競品來說最大的特點,詳細介紹見後面《如何支持海量數據》一節。
支持海量客戶端
SOFARegistry 集羣內部使用分層的架構,分別爲連接會話層(SessionServer)和數據存儲層(DataServer)。SessionServer 功能很純粹,只負責跟 Client 打交道,SessionServer 之間沒有任何通信或數據複製,所以隨着業務規模(即 Client 數量)的增長,SessionServer 可以很輕量地擴容,不會對集羣造成額外負擔。
相比之下,其他大多數的服務發現組件,如 eureka,每臺服務器都存儲着全量的數據,依靠 eurekaServer 之間的數據複製來傳播到整個集羣,所以每擴容 1 臺 eurekaServer,集羣內部相互複製的數據量就會增多一份。再如 Zookeeper 和 Etcd 等強一致性的系統,它們的複製協議(Zab/Raft)要求所有寫操作被複制到大多數服務器後才能返回成功,那麼增加服務器還會影響寫操作的效率。
秒級的服務上下線通知
對於服務的上下線變化,SOFARegistry 使用推送機制,快速地實現端到端的傳達。詳細介紹見後面《秒級服務上下線通知》一節。
接下來,我將圍繞這些特點,講解 SOFARegistry 的關鍵架構設計原理。
高可用
各個角色都有 failover 機制:
- MetaServer 集羣部署,內部基於 Raft 協議選舉和複製,只要不超過 1/2 節點宕機,就可以對外服務。
- DataServer 集羣部署,基於一致性 Hash 承擔不同的數據分片,數據分片擁有多個副本,一個主副本和多個備副本。如果 DataServer 宕機,MetaServer 能感知,並通知所有 DataServer 和 SessionServer,數據分片可 failover 到其他副本,同時 DataServer 集羣內部會進行分片數據的遷移。
- SessionServer 集羣部署,任何一臺 SessionServer 宕機時 Client 會自動 failover 到其他 SessionServer,並且 Client 會拿到最新的 SessionServer 列表,後續不會再連接這臺宕機的 SessionServer。
數據模型
模型介紹
注意:這裏只列出核心的模型和字段,實際的代碼中不止這些字段,但對於讀者來說,只要理解上述模型即可。
-
服務發佈模型(PublisherRegister)
- dataInfoId:服務唯一標識,由、<分組 group>和<租戶 instanceId>構成,例如在 SOFARPC 的場景下,一個 dataInfoId 通常是 _com.sofastack.sofa.rpc.example.HelloService#@#SOFA#@#00001_,其中SOFA 是 group 名稱,00001 是租戶 id。group 和 instance 主要是方便對服務數據做邏輯上的切分,使不同 group 和 instance 的服務數據在邏輯上完全獨立。模型裏有 group 和 instanceId 字段,但這裏不額外列出來,讀者只要理解 dataInfoId 的含義即可。
- zone:是一種單元化架構下的概念,代表一個機房內的邏輯單元,通常一個物理機房(Datacenter)包含多個邏輯單元(zone),更多內容可參考 異地多活單元化架構解決方案。在服務發現場景下,發佈服務時需指定邏輯單元(zone),而訂閱服務者可以訂閱邏輯單元(zone)維度的服務數據,也可以訂閱物理機房(datacenter)維度的服務數據,即訂閱該 datacenter 下的所有 zone 的服務數據。
- dataList:服務註冊數據,通常包含“協議”、“地址”和“額外的配置參數”,例如 SOFARPC 所發佈的數據類似“_bolt://192.168.1.100:8080?timeout=2000_”。這裏使用 dataList,表示一個 PublisherRegister 可以允許同時發佈多個服務數據(但是通常我們只會發佈一個)。
-
服務訂閱模型(SubscriberRegister)
- dataInfoId:服務唯一標識,上面已經解釋過了。
- scope: 訂閱維度,共有 3 種訂閱維度:zone、dataCenter 和 global。zone 和 datacenter 的意義,在上述有關“zone”的介紹裏已經解釋。global 維度涉及到機房間數據同步的特性,目前暫未開源。
關於“zone”和“scope”的概念理解,這裏再舉個例子。如下圖所示,物理機房內有 ZoneA 和 ZoneB 兩個單元,PublisherA 處於 ZoneA 裏,所以發佈服務時指定了 zone=ZoneA,PublisherB 處於 ZoneB 裏,所以發佈服務時指定了 zone=ZoneB;此時 Subscriber 訂閱時指定了 scope=datacenter 級別,因此它可以獲取到 PublisherA 和 PublisherB (如果 Subscriber 訂閱時指定了 scope=zone 級別,那麼它只能獲取到 PublisherA)。
服務註冊和訂閱的示例代碼如下 (詳細可參看官網的《客戶端使用》文檔):
// 構造發佈者註冊表,主要是指定dataInfoId和zone
PublisherRegistration registration = new PublisherRegistration("com.sofastack.test.demo.service");
registration.setZone("ZoneA");
// 發佈服務數據,dataList內容是 "10.10.1.1:12200?xx=yy",即只有一個服務數據
registryClient.register(registration, "10.10.1.1:12200?xx=yy");
發佈服務數據的代碼示例
// 構造訂閱者,主要是指定dataInfoId,並實現回調接口
SubscriberRegistration registration = new SubscriberRegistration("com.sofastack.test.demo.service",
(dataId, userData) -> System.out
.println("receive data success, dataId: " + dataId + ", data: " + userData));
// 設置訂閱維度,ScopeEnum 共有三種級別 zone, dataCenter, global
registration.setScopeEnum(ScopeEnum.dataCenter);
// 將註冊表註冊進客戶端並訂閱數據,訂閱到的數據會以回調的方式通知
registryClient.register(registration);
訂閱服務數據的代碼示例
SOFARegistry 服務端在接收到“服務發佈(PublisherRegister)”和“服務訂閱(SubscriberRegister)”之後,在內部會彙總成這樣的一個邏輯視圖。
注意,這個樹形圖只是邏輯上存在,實際物理上 publisherList 和 subscriberList 並不是在同一臺服務器上,publisherList 是存儲在 DataServer 裏,subscriberList 是存儲在 SessionServer 裏。
業界產品對比
可以看出來,SOFARegistry 的模型是非常簡單的,大部分服務註冊中心產品的模型也就這麼簡單。比如 eureka 的核心模型是應用(Application)和實例(InstanceInfo),如下圖,1 個 Application 可以包含多個 InstanceInfo。eureka 和 SOFARegistry 在模型上的主要區別是,eureka 在語義上是以應用(Application)粒度來定義服務的,而SOFARegistry 則是以 dataInfoId 爲粒度,由於 dataInfoId 實際上沒有強語義,粗粒度的話可以作爲應用來使用,細粒度的話則可以作爲 service 來使用。基於以上區別,SOFARegistry 能支持以接口爲粒度的 SOFARPC 和 Dubbo,也支持以應用爲粒度的 SpringCloud,而 eureka 由於主要面向應用粒度,因此最多的場景是在springCloud 中使用,而 Dubbo 和 SOFAPRC 目前均未支持 eureka。
另外,eureka 不支持 watch 機制(只能定期 fetch),因此不需要像 SOFARegistry 這樣的 Subscriber 模型。
(圖片摘自 https://www.jianshu.com/p/0356b7e9bc42)
最後再展示一下 SOFARPC 基於 Zookeeper 作爲服務註冊中心時,在 Zookeeper 中的數據結構(如下圖),Provider/Consumer 和 SOFARegistry 的 Publisher/Subscriber 類似,最大的區別是 SOFARegistry 在訂閱的維度上支持 scope(zone/datacenter),即訂閱範圍。
如何支持海量客戶端
從前面的架構介紹中我們知道,SOFARegistry 存在數據服務器(DataServer)和會話服務器(SessionServer)這 2 個角色。爲了突破單機容量瓶頸,DataServer 基於一致性 Hash 存儲着不同的數據分片,從而能支持螞蟻金服海量的服務數據,這是易於理解的。但 SessionServer 存在的意義是什麼?我們先來看看,如果沒有SessionServer的話,SOFARegistry 的架構長什麼樣子:
如上圖,所有 Client 在註冊和訂閱數據時,根據 dataInfoId 做一致性 Hash,計算出應該訪問哪一臺 DataServer,然後與該 DataServer 建立長連接。由於每個 Client 通常都會註冊和訂閱比較多的 dataInfoId 數據,因此我們可以預見每個 Client 均會與好幾臺 DataServer 建立連接。這個架構存在的問題是:“每臺 DataServer 承載的連接數會隨 Client 數量的增長而增長,每臺 Client 極端的情況下需要與每臺 DataServer 都建連,因此通過 DataServer 的擴容並不能線性的分攤 Client 連接數”。
講到這裏讀者們可能會想到,基於數據分片存儲的系統有很多,比如 Memcached、Dynamo、Cassandra、Tair 等,這些系統都也是類似上述的架構,它們是怎麼考慮連接數問題的?其實業界也給出了答案,比如 twemproxy,twitter 開發的一個 memcached/redis 的分片代理,目的是將分片邏輯放到 twemproxy 這一層,所有 Client 都直接和 twemproxy 連接,而 twemproxy 負責對接所有的 memcached/Redis,從而減少 Client 直接對memcached/redis 的連接數。twemproxy 官網也強調了這一點:“It was built primarily to reduce the number of connections to the caching servers on the backend”,如下圖,展示的是基於 twemproxy 的 redis 集羣部署架構。類似 twemproxy 的還有 codis,關於 twemproxy 和 codis 的區別,主要是分片機制不一樣,下節會再談及。
(圖片摘自 http://www.hanzhicaoa.com/1.php?s=twemproxy redis)
當然也有一些分佈式 KV 存儲系統,沒有任何連接代理層。比如 Tair (Alibaba 開源的分佈式 KV 存儲系統),只有 Client、DataServer、ConfigServer 這 3 個角色,Client 直接根據數據分片連接多臺 DataServer。但螞蟻金服內部在使用 Tair 時本身會按業務功能垂直劃分出不同的 Tair 集羣,所部署的機器配置也比較高,而且 Tair 的 Client 與 data server 的長連接通常在空閒一段時間後會關閉,這些都有助於緩解連接數的問題。當然,即便如此,Tair 的運維團隊也在時刻監控着連接數的總量。
經過上面的分析,我們明白了爲數據分片層(DataServer)專門設計一個連接代理層的重要性,所以 SOFARegistry 就有了 SessionServer 這一層。如圖,隨着 Client 數量的增長,可以通過擴容 SessionServer 就解決了單機的連接數瓶頸問題。
如何支持海量數據
面對海量數據,想突破單機的存儲瓶頸,唯一的辦法是將數據分片,接下來將介紹常見的有 2 種數據分片方式。
傳統的一致性 Hash 分片
傳統的一致性 Hash 算法,每臺服務器被虛擬成 N 個節點,如下圖所示(簡單起見虛擬份數 N 設爲 2 )。每個數據根據 Hash 算法計算出一個值,落到環上後順時針命中的第一個虛擬節點,即負責存儲該數據。業界使用一致性 Hash 的代表項目有 Memcached、Twemproxy 等。
想看完整文章內容:點擊這裏
原文出處:阿里雲大學開發者社區
文中涉及到的相關鏈接
- SOFARegistry:https://github.com/sofastack/sofa-registry
- 螞蟻金服開源服務註冊中心 SOFARegistry | SOFA 開源一週年獻禮:https://mp.weixin.qq.com/s/NYq2iMU_GlP-4DnK7C8ynQ
- 異地多活單元化架構解決方案:https://tech.antfin.com/solutions/multiregionldc
- Tair:https://github.com/alibaba/tair
- SOFABolt:https://github.com/sofastack/sofa-bolt
歡迎加入,參與 SOFARegistry 源碼解析
本文爲 SOFARegistry 的架構介紹,希望大家對 SOFARegistry 有一個初步的認識和了解。同時,我們開啓了《剖析 | SOFARegistry 實現原理》系列,會逐步詳細介紹各個部分的代碼設計和實現
如果有同學對以上某個主題特別感興趣的,可以留言討論,我們會適當根據大家的反饋調整文章的順序,謝謝大家關注 SOFA ,關注 SOFARegistry,我們會一直與大家一起成長。
歡迎提交 issue 和 PR:
SOFARegistry:https://github.com/sofastack/sofa-registry