海量數據下的註冊中心 - SOFARegistry 架構介紹

SOFAStack
S calable O pen F inancial A rchitecture Stack 是螞蟻金服自主研發的金融級分佈式架構,包含了構建金融級雲原生架構所需的各個組件,是在金融場景裏錘鍊出來的最佳實踐。
SOFARegistry 是螞蟻金服開源的具備承載海量服務註冊和訂閱能力的、高可用的服務註冊中心,最先源自於淘寶的第一版 ConfigServer,在支付寶/螞蟻金服的業務發展驅動下,近十年間已經演進至第五代。
GitHub 地址: github.com/alipay/sofa…

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

若有興趣,也歡迎加入《剖析 | SOFARegistry 實現原理》系列的共建,認領列表見文末。node

服務註冊中心是什麼

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

WX20190421-105201@2x.png

如上圖,服務註冊中心最多見的應用場景是用於 RPC 調用的服務尋址,在 RPC 遠程過程調用中,存在 2 個角色,一個服務發佈者(Publisher)、另外一個是服務訂閱者(Subscriber)。Publisher 須要把服務註冊到服務註冊中心(Registry),發佈的內容一般是該 Publisher 的 IP 地址、端口、調用方式 (協議、序列化方式)等。而 Subscriber 在第一次調用服務時,會經過 Registry 找到相應的服務的 IP 地址列表,經過負載均衡算法從 IP 列表中取一個服務提供者的服務器調用服務。同時 Subscriber 會將 Publisher 的服務列表數據緩存到本地,供後續使用。當 Subscriber 後續再調用 Publisher 時,優先使用緩存的地址列表,不須要再去請求Registry。github

WX20190421-110048@2x.png

如上圖,Subscriber 還須要能感知到 Publisher 的動態變化。好比當有 Publisher 服務下線時, Registry 會將其摘除,隨後 Subscriber 感知到新的服務地址列表後,不會再調用該已下線的 Publisher。同理,當有新的 Publisher 上線時,Subscriber 也會感知到這個新的 Publisher。redis


初步認識

WX20190425-001507@2x.png

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

1. Clientspring

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

2. SessionServer編程

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

3. DataServer

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

4. MetaServer

元數據服務器,負責維護集羣 SessionServer 和 DataServer 的一致列表,做爲 SOFARegistry 集羣內部的地址發現服務,在 SessionServer 或 DataServer 節點變動時能夠通知到整個集羣。

產品特色

WX20190424-123703@2x.png

(圖片改編自 luyiisme.github.io/2017/04/22/…

首先放一張常見的服務註冊中心的特性對比,能夠看出,在這些 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。

數據模型

模型介紹

WX20190421-211911@2x.png

注意:這裏只列出核心的模型和字段,實際的代碼中不止這些字段,但對於讀者來講,只要理解上述模型便可。

  • 服務發佈模型(PublisherRegister)
    • dataInfoId:服務惟一標識,由<dataId>、<分組 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)。

WX20190421-214936@2x.png

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

// 構造發佈者註冊表,主要是指定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)」以後,在內部會彙總成這樣的一個邏輯視圖。

WX20190425-001752@2x.png

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

image.png

(圖片摘自 www.jianshu.com/p/0356b7e9b…

最後再展現一下 SOFARPC 基於 Zookeeper 做爲服務註冊中心時,在 Zookeeper 中的數據結構(以下圖),Provider/Consumer 和 SOFARegistry 的 Publisher/Subscriber 相似,最大的區別是 SOFARegistry 在訂閱的維度上支持 scope(zone/datacenter),即訂閱範圍。

WX20190421-232335@2x.png

如何支持海量客戶端

從前面的架構介紹中咱們知道,SOFARegistry 存在數據服務器(DataServer)和會話服務器(SessionServer)這 2 個角色。爲了突破單機容量瓶頸,DataServer 基於一致性 Hash 存儲着不一樣的數據分片,從而能支持螞蟻金服海量的服務數據,這是易於理解的。但 SessionServer 存在的意義是什麼?咱們先來看看,若是沒有SessionServer的話,SOFARegistry 的架構長什麼樣子:

WX20190422-200639@2x.png

如上圖,全部 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 的區別,主要是分片機制不同,下節會再談及。

58db25961bad6 (1).jpg

(圖片摘自 www.hanzhicaoa.com/1.php?s=twe…

固然也有一些分佈式 KV 存儲系統,沒有任何鏈接代理層。好比 Tair (Alibaba 開源的分佈式 KV 存儲系統),只有 Client、DataServer、ConfigServer 這 3 個角色,Client 直接根據數據分片鏈接多臺 DataServer。但螞蟻金服內部在使用 Tair 時自己會按業務功能垂直劃分出不一樣的 Tair 集羣,所部署的機器配置也比較高,並且 Tair 的 Client 與 data server 的長鏈接一般在空閒一段時間後會關閉,這些都有助於緩解鏈接數的問題。固然,即使如此,Tair 的運維團隊也在時刻監控着鏈接數的總量。

通過上面的分析,咱們明白了爲數據分片層(DataServer)專門設計一個鏈接代理層的重要性,因此 SOFARegistry 就有了 SessionServer 這一層。如圖,隨着 Client 數量的增加,能夠經過擴容 SessionServer 就解決了單機的鏈接數瓶頸問題。

WX20190424-145609@2x.png

如何支持海量數據

面對海量數據,想突破單機的存儲瓶頸,惟一的辦法是將數據分片,接下來將介紹常見的有 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 的分片範圍都發生了變化。

WX20190425-085019@2x.png

「分片範圍不固定」,帶來的問題是:難以實現節點之間數據多副本複製。這個結論可能不太好理解,我舉個例子:若是要實現節點之間數據可以複製,首先每一個節點須要對數據分片保留操做日誌,節點之間依靠這些操做日誌進行增量的日誌同步。好比上圖的左半邊,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 再從路由表裏找到對應的節點。預分片機制的具體原理以下圖。

WX20190424-170456@2x.png


能夠看出來,相對傳統的一致性 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。

WX20190423-203620@2x.png

WX20190423-202547@2x.png

通常來講,管理路由表、對 Client 和 全部node 發號施令的功能(能夠理解成是「大腦」),一般由單獨的角色來承擔,好比 Codis 的大腦是 codis-conf + Zookeeper/Etcd,Tair 的大腦是 ConfigServer。下圖是 Tair 官方展現的部署架構圖,ConfigServer 由 2 臺服務器組成,一臺 master,一臺 slave。

191450396414815.png

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 維度的數據多副本。

WX20190425-103507@2x.png

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

服務上下線過程

WX20190424-140503@2x.png

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

服務的上下線過程,是指服務經過代碼調用作正常的註冊(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 源碼解析

image.png

本文爲 SOFARegistry 的架構介紹,但願你們對 SOFARegistry 有一個初步的認識和了解。同時,咱們開啓了《剖析 | SOFARegistry 實現原理》系列,會逐步詳細介紹各個部分的代碼設計和實現,預計按照以下的目錄進行:

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

若是有同窗對以上某個主題特別感興趣的,能夠留言討論,咱們會適當根據你們的反饋調整文章的順序,謝謝你們關注 SOFA ,關注 SOFARegistry,咱們會一直與你們一塊兒成長。

領取方式

直接回複本公衆號想認領的文章名稱,咱們將會主動聯繫你,確認資質後,便可加入,It's your show time!

除了源碼解析,也歡迎提交 issue 和 PR:

SOFARegistrygithub.com/alipay/sofa…

公衆號:金融級分佈式架構(Antfin_SOFA)

相關文章
相關標籤/搜索