歡迎光臨
每天分享高質量文章

海量資料下的註冊中心 – SOFARegistry 架構介紹

SOFAStack

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

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

GitHub 地址:

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

3 月 31 日,螞蟻金服正式開源了內部演進了近 10 年的註冊中心產品 – SOFARegistry。先前的文章介紹了 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 個角色:

1. Client

提供應用接入服務註冊中心的基本 API 能力,應用系統通過依賴客戶端 JAR 包,通過編程方式呼叫服務註冊中心的服務訂閱和服務發佈能力。

2. SessionServer

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

3. DataServer

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

4. 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:服務唯一標識,由、和構成,例如在 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)。

服務註冊和訂閱的示例代碼如下 (詳細可參看官網的《客戶端使用》文件):

// 構造發佈者註冊表,主要是指定dataInfoId和zonePublisherRegistration 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, globalregistration.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 等。

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

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

“分片範圍不固定”,帶來的問題是:難以實現節點之間資料多副本複製。這個結論可能不太好理解,我舉個例子:如果要實現節點之間資料能夠複製,首先每個節點需要對資料分片保留操作日誌,節點之間依靠這些操作日誌進行增量的日誌同步。比如上圖的左半邊,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:slotId = crc32(key) % 1024,根據 slotId 再從路由表裡找到對應的節點。預分片機制的具體原理如下圖。

可以看出來,相對傳統的一致性 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 都屬於備副本(讀寫都訪問主副本)。複製完成後方可結束。

  • 節點擴容

節點擴容的過程比節點宕機稍微複雜,因為新節點的加入可能導致 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。

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

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 維度的資料多副本。

如上圖,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 機制,所以當機器出現假死的情況(服務不可用,但連接未斷且有心跳),是無法被感知的。

服務上下線過程

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

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

  • Client 呼叫 publisher.register 向 SessionServer 註冊服務。

  • SessionServer 收到服務資料 (PublisherRegister) 後,將其寫入記憶體 (SessionServer 會儲存 Client 的資料到記憶體,用於後續可以跟 DataServer 做定期檢查),再根據 dataInfoId 的一致性 Hash 尋找對應的 DataServer,將  PublisherRegister 發給 DataServer。

  • DataServer 接收到 PublisherRegister 資料,首先也是將資料寫入記憶體 ,DataServer 會以 dataInfoId 的維度彙總所有 PublisherRegister。同時,DataServer 將該 dataInfoId 的變更事件通知給所有 SessionServer,變更事件的內容是 dataInfoId 和版本號信息 version。

  • 同時,異步地,DataServer 以 dataInfoId 維度增量地同步資料給其他副本。因為 DataServer 在一致性 Hash 分片的基礎上,對每個分片儲存了多個副本(預設是3個副本)。

  • SessionServer 接收到變更事件通知後,對比 SessionServer 記憶體中儲存的 dataInfoId 的 version,若發現比 DataServer 發過來的小,則主動向 DataServer 獲取 dataInfoId 的完整資料,即包含了所有該 dataInfoId 具體的 PublisherRegister 串列。

  • 最後,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 個方面,第一是服務的健康檢測,通過連接敏感的特性,對服務宕機做到秒級發現,但為此也帶來“網絡不穩定導致服務頻繁上下線”的負面影響;第二是內部角色之間的“推”和“拉”的機制,整個服務上下線流程都以實時的“推”為主,因此才能做到秒級的通知。

文中涉及到的相關鏈接

    已同步到看一看
    赞(0)

    分享創造快樂