天天看點

海量資料下的注冊中心 - SOFARegistry 架構介紹

SOFAStack

Scalable Open Financial Architecture Stack 是螞蟻金服自主研發的金融級分布式架構,包含了建構金融級雲原生架構所需的各個元件,是在金融場景裡錘煉出來的最佳實踐。 

SOFARegistry 是螞蟻金服開源的具有承載海量服務注冊和訂閱能力的、高可用的服務注冊中心,最早源自于淘寶的初版 ConfigServer,在支付寶/螞蟻金服的業務發展驅動下,近十年間已經演進至第五代。

GitHub 位址:

https://github.com/alipay/sofa-registry 

3 月 31 日,螞蟻金服正式開源了内部演進了近 10 年的注冊中心産品-SOFARegistry。先前的文章介紹了 SOFARegistry 的演進之路,而本文主要對 SOFARegistry 整體架構設計進行剖析,并着重介紹一些關鍵的設計特點,期望能幫助讀者對 SOFARegistry 有更直接的認識。

如有興趣,也歡迎加入《剖析 | SOFARegistry  實作原理》系列的共建,認領清單見文末。

服務注冊中心是什麼

不可免俗地,先介紹一下服務注冊中心的概念。對此概念已經了解的讀者,可選擇跳過本節。

海量資料下的注冊中心 - SOFARegistry 架構介紹

如上圖,服務注冊中心最常見的應用場景是用于 RPC 調用的服務尋址,在 RPC 遠端過程調用中,存在 2 個角色,一個服務釋出者(Publisher)、另一個是服務訂閱者(Subscriber)。Publisher 需要把服務注冊到服務注冊中心(Registry),釋出的内容通常是該 Publisher 的 IP 位址、端口、調用方式 (協定、序列化方式)等。而 Subscriber 在第一次調用服務時,會通過 Registry 找到相應的服務的 IP 位址清單,通過負載均衡算法從 IP 清單中取一個服務提供者的伺服器調用服務。同時 Subscriber 會将 Publisher 的服務清單資料緩存到本地,供後續使用。當 Subscriber 後續再調用 Publisher 時,優先使用緩存的位址清單,不需要再去請求Registry。

海量資料下的注冊中心 - SOFARegistry 架構介紹

如上圖,Subscriber 還需要能感覺到 Publisher 的動态變化。比如當有 Publisher 服務下線時, Registry 會将其摘除,随後 Subscriber 感覺到新的服務位址清單後,不會再調用該已下線的 Publisher。同理,當有新的 Publisher 上線時,Subscriber 也會感覺到這個新的 Publisher。

初步認識

海量資料下的注冊中心 - SOFARegistry 架構介紹

在了解了常見的服務注冊中心的概念之後,我們來看看螞蟻金服的 SOFARegistry 長什麼樣子。如上圖,SOFARegistry 包含 4 個角色:

1、Client

提供應用接入服務注冊中心的基本 API 能力,應用系統通過依賴用戶端 JAR 包,通過程式設計方式調用服務注冊中心的服務訂閱和服務釋出能力。

2、SessionServer

會話伺服器,負責接受 Client 的服務釋出和服務訂閱請求,并作為一個中間層将寫操作轉發 DataServer 層。SessionServer 這一層可随業務機器數的規模的增長而擴容。

3、DataServer

資料伺服器,負責存儲具體的服務資料,資料按 dataInfoId 進行一緻性 Hash 分片存儲,支援多副本備份,保證資料高可用。這一層可随服務資料量的規模的增長而擴容。

4、MetaServer

中繼資料伺服器,負責維護叢集 SessionServer 和 DataServer 的一緻清單,作為 SOFARegistry 叢集内部的位址發現服務,在 SessionServer 或 DataServer 節點變更時可以通知到整個叢集。

産品特點

海量資料下的注冊中心 - SOFARegistry 架構介紹

(圖檔改編自 

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。

資料模型

模型介紹

海量資料下的注冊中心 - SOFARegistry 架構介紹

注意:這裡隻列出核心的模型和字段,實際的代碼中不止這些字段,但對于讀者來說,隻要了解上述模型即可。

  • 服務釋出模型(PublisherRegister)
    • dataInfoId:服務唯一辨別,由、<分組 group>和<租戶 instanceId>構成,例如在 SOFARPC 的場景下,一個 dataInfoId 通常是 _com.alipay.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)。

海量資料下的注冊中心 - SOFARegistry 架構介紹

服務注冊和訂閱的示例代碼如下 (詳細可參看官網的《

用戶端使用

》文檔):

// 構造釋出者系統資料庫,主要是指定dataInfoId和zone
PublisherRegistration registration = new PublisherRegistration("com.alipay.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.alipay.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)”之後,在内部會彙總成這樣的一個邏輯視圖。

海量資料下的注冊中心 - SOFARegistry 架構介紹

注意,這個樹形圖隻是邏輯上存在,實際實體上 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 模型。

海量資料下的注冊中心 - SOFARegistry 架構介紹

(圖檔摘自 

https://www.jianshu.com/p/0356b7e9bc42

最後再展示一下 SOFARPC 基于 Zookeeper 作為服務注冊中心時,在 Zookeeper 中的資料結構(如下圖),Provider/Consumer 和 SOFARegistry 的 Publisher/Subscriber 類似,最大的差別是 SOFARegistry 在訂閱的次元上支援 scope(zone/datacenter),即訂閱範圍。

海量資料下的注冊中心 - SOFARegistry 架構介紹

如何支援海量用戶端

從前面的架構介紹中我們知道,SOFARegistry 存在資料伺服器(DataServer)和會話伺服器(SessionServer)這 2 個角色。為了突破單機容量瓶頸,DataServer 基于一緻性 Hash 存儲着不同的資料分片,進而能支援螞蟻金服海量的服務資料,這是易于了解的。但 SessionServer 存在的意義是什麼?我們先來看看,如果沒有SessionServer的話,SOFARegistry 的架構長什麼樣子:

海量資料下的注冊中心 - 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 的差別,主要是分片機制不一樣,下節會再談及。

海量資料下的注冊中心 - SOFARegistry 架構介紹
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 就解決了單機的連接配接數瓶頸問題。

海量資料下的注冊中心 - SOFARegistry 架構介紹

如何支援海量資料

面對海量資料,想突破單機的存儲瓶頸,唯一的辦法是将資料分片,接下來将介紹常見的有 2 種資料分片方式。

傳統的一緻性 Hash 分片

傳統的一緻性 Hash 算法,每台伺服器被虛拟成 N 個節點,如下圖所示(簡單起見虛拟份數 N 設為 2 )。每個資料根據 Hash 算法計算出一個值,落到環上後順時針命中的第一個虛拟節點,即負責存儲該資料。業界使用一緻性 Hash 的代表項目有 Memcached、Twemproxy 等。

海量資料下的注冊中心 - SOFARegistry 架構介紹

一緻性 Hash 分片的優點:在虛拟節點足夠多的情況下,資料分片在每台節點上是非常分散均勻的,并且增加或減少節點的數量,還是能維持資料的平衡。比如當 Memcached 單機遇到記憶體瓶頸時,通過擴容 Memcached 機器,資料将會被重新均勻地分攤到新的節點上,是以每台 Memcached 伺服器的記憶體就能得到降低。當某台伺服器當機時,資料會被重新均勻地分攤到剩餘的節點上,如下圖所示,A 機器當機,原先在 A 機器上的資料會分别重新分攤到 B 機器和 C 機器。

海量資料下的注冊中心 - SOFARegistry 架構介紹

一緻性 Hash 分片的缺點:分片範圍不固定(一旦節點數發生變化,就會導緻分片範圍變化)。嚴格來說,這不是一緻性 Hash 的缺點,而是它的特點,這個特點在追求資料分散的場景下是優點,但在談及資料複制的這個場景下它是個缺點。從上面的機器當機過程,可以看到,僅擴縮容少量節點,就會影響到其他大部分已有節點的分片範圍,即每台節點的分片範圍會因為節點數變化而發生變化。如下圖,當 A 當機時,分片 6 和 1 合并成 7,分片 3 和 4 合并成 8,就是說,A 當機後,B 和 C 的分片範圍都發生了變化。

海量資料下的注冊中心 - SOFARegistry 架構介紹

“分片範圍不固定”,帶來的問題是:難以實作節點之間資料多副本複制。這個結論可能不太好了解,我舉個例子:如果要實作節點之間資料能夠複制,首先每個節點需要對資料分片保留記錄檔,節點之間依靠這些記錄檔進行增量的日志同步。比如上圖的左半邊,B 節點負責分片 1 和 5,是以 B 節點需要使用 2 個日志檔案(假設叫做 data-1.log 和 data-5.log)記錄這 2 個分片的所有更新操作。當 A 當機時(上圖的右半邊),B 節點負責的分片變成 7 和 5,那麼 data-1.log 日志檔案就失效了,因為分片 1 不複存在。可見,在分片範圍易變的情況下,儲存資料分片的記錄檔,并沒有意義。這就是為什麼這種情況下節點之間的日志複制不好實作的原因。

值得一提的是,Twemproxy 也是因為“分片範圍不固定(一旦節點數發生變化,就會導緻分片範圍變化)”這個問題,是以不支援平滑的節點動态變化。比如使用 Twemproxy + Redis,如果要擴容 Redis 節點,那麼需要使用者自己實作資料遷移的過程,這也是後來 Codis 出現的原因。當然,對于不需要資料多副本複制的系統,比如 Memcached,由于它的定位是緩存,不保證資料的高可靠,節點之間不需要做資料多副本複制,是以不存在這個顧慮。

思考:對于那些需要基于資料多副本複制,來保證資料高可靠的 kv 存儲系統,比如 Tair、dynamo 和 Cassandra,它們是怎麼做資料分片的呢?

預分片機制 Pre-Sharding

預分片機制,了解起來比一緻性 Hash 簡單,首先需要從邏輯上将資料範圍劃分成 N 個大小相等的 slot,并且 slot 數量(即 N 值)後續不可再修改。然後,還需要引進“路由表”的概念,“路由表”負責存放這每個節點和 N 個slot 的映射關系,并保證盡量把所有 slot 均勻地配置設定給每個節點。在對資料進行路由時,根據資料的 key 計算出哈希值,再将 hash 值對 N 取模,這個餘數就是對應 key 的 slot 位置。比如 Codis 預設将資料範圍分成 1024 個 slots,對于每個 key 來說,通過以下公式确定所屬的 slotId:s_lotId = crc32(key) % 1024_,根據 slotId 再從路由表裡找到對應的節點。預分片機制的具體原理如下圖。

海量資料下的注冊中心 - SOFARegistry 架構介紹

可以看出來,相對傳統的一緻性 Hash 分片,預分片機制的每個 slot 的大小(代表資料分片範圍)是固定的,是以解決了“分片範圍不固定”的問題,現在,節點之間可以基于 slot 的次元做資料同步了。至于 slot 之間資料複制的方式,比如“采取異步複制還是同步複制”,“複制多少個節點成功才算成功”,不同系統的因其 cap 定位不同,實作也大有不同,這裡無法展開講。

接下來,我們看看節點增删的過程。

  • 節點當機

如下圖,副本數為 2,路由表裡每個 slot id 需要映射到 2 個節點,1 個節點存儲主副本,1 個節點存儲備副本。對于 S1 的所有寫操作,需要路由到 nodeA,然後 nodeA 會将 S1 的記錄檔同步給 nodeB。如果 nodeA 發生當機,則系統需要修改路由表,将 nodeA 所負責的 slot ( 如圖中的 S1和 S3 ) 重新配置設定給其他節點,如圖,經過調整,S1 的節點變為 nodeB 和 nodeC,S3 的節點變為 nodeC 和 nodeE。然後系統會指令 nodeC 和 nodeE 開始做資料複制的工作,複制過程不會影響到 S1 和 S3 對外服務,因為 nodeC 和 nodeE 都屬于備副本(讀寫都通路主副本)。複制完成後方可結束。

海量資料下的注冊中心 - SOFARegistry 架構介紹
  • 節點擴容

節點擴容的過程比節點當機稍微複雜,因為新節點的加入可能導緻 slot 遷移,而遷移的過程中需要保證系統仍可以對外服務。以下圖為例,擴容 nodeF 之後,系統需要對路由表的重新平衡,S1 的主節點由 nodeA 變為 nodeF,S12 的備節點由 nodeC 變為 nodeF。我們講一下 S1 的資料遷移過程:首先用戶端所看到的路由表還不會發生變化,用戶端對 S1 的讀寫請求仍然會路由到 nodeA。與此同時  nodeA 開始将 S1 的資料複制給 nodeF;然後,當 nodeF 即将完成資料的備份時,短暫地對 S1 禁寫,確定 S1 不會再更新,然後 nodeF 完成最終的資料同步;最後,修改路由表,将 S1 的主節點改為 nodeF,并将最新的路由表資訊通知給 Client,至此就完成 S1 的遷移過程。Client 後續對 S1 的讀寫都會發送給 nodeF。 

海量資料下的注冊中心 - SOFARegistry 架構介紹
海量資料下的注冊中心 - SOFARegistry 架構介紹

一般來說,管理路由表、對 Client 和 所有node 發号施令的功能(可以了解成是“大腦”),通常由單獨的角色來承擔,比如 Codis 的大腦是 codis-conf + Zookeeper/Etcd,Tair 的大腦是 ConfigServer。下圖是 Tair 官方展示的部署架構圖,ConfigServer 由 2 台伺服器組成,一台 master,一台 slave。

海量資料下的注冊中心 - SOFARegistry 架構介紹

Tair(Alibaba 開源的分布式 KV 存儲系統)架構圖

SOFARegistry 的選擇

總結一下,“一緻性 Hash 分片機制” 和 “預分片機制” 的主要差別:

  • 一緻性 Hash 分片機制

在虛拟節點足夠多的情況下,資料分片在每台節點上是非常分散均勻的,即使增加或減少節點的數量,還是能維持資料的平衡,并且不需要額外維護路由表。但是,由于“分片範圍不固定(一旦節點數發生變化,就會導緻分片範圍變化)”的特點,導緻它不适用于需要做資料多副本複制的場景。目前業界主要代表項目有 Memcached、Twemproxy 等。

  • 預分片機制

通過事先将資料範圍等分為 N 個 slot,解決了“分片範圍不固定”的問題,是以可以友善的實作資料的多副本複制。但需要引進“路由表”,并且在節點變化時可能需要做資料遷移,實作起來也不簡單。目前業界主要代表項目有 Dynamo、Casandra、Tair、Codis、Redis cluster 等。

SOFARegistry 的 DataServer 需要存儲多個副本的服務資料,其實比較适合選擇“預分片機制”,但由于曆史原因,我們的分片方式選擇了“一緻性 Hash分片”。在“一緻性 Hash分片”的基礎上,當然也不意外地遇到了 “分片資料不固定”這個問題,導緻 DataServer 之間的資料多副本複制實作難度很大。最後,我們選擇在 DataServer 記憶體裡以 dataInfoId 的粒度記錄記錄檔,并且在 DataServer 之間也是以 dataInfoId 的粒度去做資料同步。聰明的讀者應該看出來了,其實思想上類似把每個 dataInfoId 當做一個 slot 去對待。這個做法很妥協,好在,服務注冊中心的場景下,dataInfoId 的總量是有限的(以螞蟻的規模,每台 DataServer 承載的 dataInfoId 數量也就在數萬的級别),是以也勉強實作了 dataInfoId 次元的資料多副本。

海量資料下的注冊中心 - SOFARegistry 架構介紹

如上圖,A-F 代表 6 個 dataInfoId 資料。使用一緻性 Hash 分片後,DataServer1 負責 A 和 D,DataServer2 負責 B 和 E,DataServer3 負責 C 和 F。并且每個資料均有 3 個副本。對 A 的寫操作是在 DataServer1 即主副本上進行,随後 DataServer1 将寫操作異步地複制給 DataServer2 和 DataServer3,DataServer2 和 DataServer3 将寫操作應用到記憶體中的 A 備副本,這樣就完成了多副本間的資料複制工作。

秒級服務上下線通知

服務的健康檢測

首先,我們簡單看一下業界其他注冊中心的健康檢測機制:

  • Eureka:定期有 renew 心跳,資料具有 TTL(Time To Live);并且支援自定義 healthcheck 機制,當 healthcheck 檢測出系統不健康時會主動更新 instance 的狀态。
  • Zookeeper:定期發送連接配接心跳以保持會話 (Session),會話本身 (Session) 具有TTL。
  • Etcd:定期通過 http 對資料進行 refresh,資料具有 TTL。
  • Consul:agent 定期對服務進行 healthcheck,支援 http/tcp/script/docker;也可以由服務主動定期向 agent 更新 TTL。

SOFARegistry 的健康檢測

我們可以看到上述其他注冊中心的健康檢測都有個共同的關鍵詞:“定期”,定期檢測的時間周期通常設定為秒級,比如 3 秒、5 秒或 10 秒,甚至更長,也就是說服務的健康狀态總是滞後的。螞蟻金服的注冊中心從最初的版本設計開始,就把健康狀态的及時感覺,當做一個重要的設計目标,特别是需要做到“服務當機能被及時發現”。為此, SOFARegistry 在健康檢測的設計上做了這個決定:“服務資料與服務釋出者的實體連接配接綁定在一起,斷連馬上清資料”,我簡稱這個特點叫做連接配接敏感性。

連接配接敏感性:在 SOFARegistry 裡,所有 Client 都與 SessionServer 保持長連接配接,每條長連接配接都會有基于 

bolt

 的連接配接心跳,如果連接配接斷開,Client 會馬上重建立連,時刻保證 Client 與 SessionServer 之間有可靠的連接配接。

SOFARegistry 将服務資料 (PublisherRegister) 和服務釋出者 (Publisher) 的連接配接的生命周期綁定在一起:每個 PublisherRegister 有一個屬性是 connId,connId 由注冊本次服務的 Publisher 的連接配接辨別 (IP 和 Port)構成, 意味着,隻要該 Publisher 和 SessionServer 斷連,資料就失效。Client 重建立連成功後,會重新注冊服務資料,但重新注冊的服務資料會被當成新的資料,因為換了連接配接之後,Publisher 的 connId 不一樣了。

比如,當服務的程序當機時,一般情況下 os 會馬上斷開程序相關的連接配接(即發送 FIN),是以 SessionServer 能馬上感覺連接配接斷開事件,然後把該 connId 相關的所有 PublisherRegister 都清除,并及時推送給所有訂閱者 (Subscriber)。當然,如果隻是網絡問題導緻連接配接斷開,實際的程序并沒有當機,那麼 Client 會馬上重連 SessionServer 并重新注冊所有服務資料。對訂閱者來說它們所看到的,是釋出者經曆短暫的服務下線後,又重新上線。如果這個過程足夠短暫(如 500ms 内發生斷連和重連),訂閱者也可以感受不到,這個是 DataServer 内部的資料延遲合并的功能,這裡不展開講,後續在新文章裡再介紹。

需要承認的是,SOFARegistry 太過依賴服務所綁定的連接配接狀态,當網絡不穩定的情況下,大量服務頻繁上下線,對網絡帶寬會帶來一些沒必要的浪費,甚至如果是 SessionServer 整個叢集單方面存在網絡問題,那麼可能會造成誤判,這裡也缺乏類似 eureka 那樣的保護模式。另外,SOFARegistry 目前不支援自定義的 healthcheck 機制,是以當機器出現假死的情況(服務不可用,但連接配接未斷且有心跳),是無法被感覺的。

服務上下線過程

海量資料下的注冊中心 - SOFARegistry 架構介紹

一次服務的上線(注冊)過程

服務的上下線過程,是指服務通過代碼調用做正常的注冊(publisher.register) 和 下線(publisher.unregister),不考慮因為服務當機等意外情況導緻的下線。如上圖,大概呈現了“一次服務注冊過程”的服務資料在内部流轉過程。下線流程也是類似,這裡忽略不講。

  1. Client 調用 publisher.register 向 SessionServer 注冊服務。
  2. SessionServer 收到服務資料 (PublisherRegister) 後,将其寫入記憶體 (SessionServer 會存儲 Client 的資料到記憶體,用于後續可以跟 DataServer 做定期檢查),再根據 dataInfoId 的一緻性 Hash 尋找對應的 DataServer,将  PublisherRegister 發給 DataServer。
  3. DataServer 接收到 PublisherRegister 資料,首先也是将資料寫入記憶體 ,DataServer 會以 dataInfoId 的次元彙總所有 PublisherRegister。同時,DataServer 将該 dataInfoId 的變更事件通知給所有 SessionServer,變更事件的内容是 dataInfoId 和版本号資訊 version。
  4. 同時,異步地,DataServer 以 dataInfoId 次元增量地同步資料給其他副本。因為 DataServer 在一緻性 Hash 分片的基礎上,對每個分片儲存了多個副本(預設是3個副本)。
  5. SessionServer 接收到變更事件通知後,對比 SessionServer 記憶體中存儲的 dataInfoId 的 version,若發現比 DataServer 發過來的小,則主動向 DataServer 擷取 dataInfoId 的完整資料,即包含了所有該 dataInfoId 具體的 PublisherRegister 清單。
  6. 最後,SessionServer 将資料推送給相應的 Client,Client 就接收到這一次服務注冊之後的最新的服務清單資料。

基于對上下線流程的初步認識後,這裡對 SOFARegistry 内部角色之間的資料互動方式做一下概括:

  • SessionServer 和 DataServer 之間的通信,是基于推拉結合的機制
    • 推:DataServer 在資料有變化時,會主動通知 SessionServer,SessionServer 檢查确認需要更新(對比 version) 後主動向 DataServer 擷取資料。
    • 拉:除了上述的 DataServer 主動推以外,SessionServer 每隔一定的時間間隔(預設30秒),會主動向 DataServer 查詢所有 dataInfoId 的 version 資訊,然後再與 SessionServer 記憶體的 version 作比較,若發現 version 有變化,則主動向 DataServer 擷取資料。這個“拉”的邏輯,主要是對“推”的一個補充,若在“推”的過程有錯漏的情況可以在這個時候及時彌補。
  • Client 與 SessionServer 之間,完全基于推的機制
    • SessionServer 在接收到 DataServer 的資料變更推送,或者 SessionServer 定期查詢 DataServer 發現資料有變更并重新擷取之後,直接将 dataInfoId 的資料推送給 Client。如果這個過程因為網絡原因沒能成功推送給 Client,SessionServer 會嘗試做一定次數(預設5次)的重試,最終還是失敗的話,依然會在 SessionServer 定期每隔 30s 輪訓 DataServer 時,會再次推送資料給 Client。

總結,本節介紹了 SOFARegistry 實作秒級的服務上下線通知的原理,主要是 2 個方面,第一是服務的健康檢測,通過連接配接敏感的特性,對服務當機做到秒級發現,但為此也帶來“網絡不穩定導緻服務頻繁上下線”的負面影響;第二是内部角色之間的“推”和“拉”的機制,整個服務上下線流程都以實時的“推”為主,是以才能做到秒級的通知。

文中涉及到的相關連結

歡迎加入,參與 SOFARegistry 源碼解析 

海量資料下的注冊中心 - SOFARegistry 架構介紹

本文為 SOFARegistry 的架構介紹,希望大家對 SOFARegistry 有一個初步的認識和了解。同時,我們開啟了《剖析 | SOFARegistry  實作原理》系列,會逐漸詳細介紹各個部分的代碼設計和實作,預計按照如下的目錄進行:

  • 【已完成】海量資料下的注冊中心 - SOFARegistry 架構介紹
  • 【待領取】SOFARegistry 分片存儲的實作詳解
  • 【待領取】SOFARegistry 資料推送機制詳解
  • 【待領取】SOFARegistry Meta 實作剖析
  • 【待領取】SOFARegistry 最終一緻性詳解

如果有同學對以上某個主題特别感興趣的,可以留言讨論,我們會适當根據大家的回報調整文章的順序,謝謝大家關注 SOFA ,關注 SOFARegistry,我們會一直與大家一起成長。

領取方式

直接回複本公衆号想認領的文章名稱,我們将會主動聯系你,确認資質後,即可加入,It's your show time!

除了源碼解析,也歡迎送出 issue 和 PR:

繼續閱讀