若需文中圖檔,請關注公衆号:404P 擷取。
前言
在微服務架構體系下,服務注冊中心緻力于解決微服務之間服務發現的問題。在服務數量不多的情況下,服務注冊中心叢集中每台機器都儲存着全量的服務資料,但随着螞蟻金服海量服務的出現,單機已無法存儲所有的服務資料,資料分片成為了必然的選擇。資料分片之後,每台機器隻儲存一部分服務資料,節點上下線就容易造成資料波動,很容易影響應用的正常運作。本文通過介紹 SOFARegistry 的分片算法和相關的核心源碼來展示螞蟻金服是如何解決上述問題的。
服務注冊中心簡介
在微服務架構下,一個網際網路應用的服務端背後往往存在大量服務間的互相調用。例如服務 A 在鍊路上依賴于服務 B,那麼在業務發生時,服務 A 需要知道服務 B 的位址,才能完成服務調用。而分布式架構下,每個服務往往都是叢集部署的,叢集中的機器也是經常變化的,是以服務 B 的位址不是固定不變的。如果要保證業務的可靠性,服務調用者則需要感覺被調用服務的位址變化。

圖1 微服務架構下的服務尋址
既然成千上萬的服務調用者都要感覺這樣的變化,那這種感覺能力便下沉成為微服務中一種固定的架構模式:服務注冊中心。
圖2 服務注冊中心
服務注冊中心裡,有服務提供者和服務消費者兩種重要的角色,服務調用方是消費者,服務被調方是提供者。對于同一台機器,往往兼具兩者角色,既被其它服務調用,也調用其它服務。服務提供者将自身提供的服務資訊釋出到服務注冊中心,服務消費者通過訂閱的方式感覺所依賴服務的資訊是否發生變化。
SOFARegistry 總體架構
SOFARegistry 的架構中包括4種角色:Client、Session、Data、Meta,如圖3所示:
圖3 SOFARegistry 總體架構
- Client 層
應用伺服器叢集。Client 層是應用層,每個應用系統通過依賴注冊中心相關的用戶端 jar 包,通過程式設計方式來使用服務注冊中心的服務釋出和服務訂閱能力。
- Session 層
Session 伺服器叢集。顧名思義,Session 層是會話層,通過長連接配接和 Client 層的應用伺服器保持通訊,負責接收 Client 的服務釋出和服務訂閱請求。該層隻在記憶體中儲存各個服務的釋出訂閱關系,對于具體的服務資訊,隻在 Client 層和 Data 層之間透傳轉發。Session 層是無狀态的,可以随着 Client 層應用規模的增長而擴容。
- Data 層
資料伺服器叢集。Data 層通過分片存儲的方式儲存着所用應用的服務注冊資料。資料按照 dataInfoId(每一份服務資料的唯一辨別)進行一緻性 Hash 分片,多副本備份,保證資料的高可用。下文的重點也在于随着資料規模的增長,Data 層如何在不影響業務的前提下實作平滑的擴縮容。
- Meta 層
中繼資料伺服器叢集。這個叢集管轄的範圍是 Session 伺服器叢集和 Data 伺服器叢集的伺服器資訊,其角色就相當于 SOFARegistry 架構内部的服務注冊中心,隻不過 SOFARegistry 作為服務注冊中心是服務于廣大應用服務層,而 Meta 叢集是服務于 SOFARegistry 内部的 Session 叢集和 Data 叢集,Meta 層能夠感覺到 Session 節點和 Data 節點的變化,并通知叢集的其它節點。
SOFARegistry 如何突破單機存儲瓶頸
在螞蟻金服的業務規模下,單台伺服器已經無法存儲所有的服務注冊資料,SOFARegistry 采用了資料分片的方案,每台機器隻儲存一部分資料,同時每台機器有多副本備份,這樣理論上可以無限擴容。根據不同的資料路由方式,常見的資料分片主要分為兩大類:範圍分片和 Hash(哈希)分片。
圖4 資料分片
範圍分片
每一個資料分片負責存儲某一鍵值區間範圍的值。例如按照時間段進行分區,每個小時的 Key 放在對應的節點上。區間範圍分片的優勢在于資料分片具有連續性,可以實作區間範圍查詢,但是缺點在于沒有對資料進行随機打散,容易存在熱點資料問題。
Hash (哈希)分片
Hash 分片則是通過特定的 Hash 函數将資料随機均勻地分散在各個節點中,不支援範圍查詢,隻支援點查詢,即根據某個資料的 Key 擷取資料的内容。業界大多 KV(Key-Value)存儲系統都支援這種方式,包括 cassandra、dynamo、membase 等。業界常見的 Hash 分片算法有哈希取模法、一緻性哈希法和虛拟桶法。
哈希取模
哈希取模的 Hash 函數如下:
H(Key) = hash(key) mod K;
這是一個 key-machine 的函數。key 是資料主鍵,K 是實體機數量,通過資料的 key 能夠直接路由到實體機器。當 K 發生變化時,會影響全體資料分布。所有節點上的資料會被重新分布,這個過程是難以在系統無感覺的情況下平滑完成的。
圖5 哈希取模
一緻性哈希
分布式哈希表(DHT)是 P2P 網絡和分布式存儲中一項常見的技術,是哈希表的分布式擴充,即在每台機器存儲部分資料的前提下,如何通過哈希的方式來對資料進行讀寫路由。其核心在于每個節點不僅隻儲存一部分資料,而且也隻維護一部分路由,進而實作 P2P 網絡節點去中心化的分布式尋址和分布式存儲。DHT 是一個技術概念,其中業界最常見的一種實作方式就是一緻性哈希的 Chord 算法實作。
- 哈希空間
一緻性哈希中的哈希空間是一個資料和節點共用的一個邏輯環形空間,資料和機器通過各自的 Hash 算法得出各自在哈希空間的位置。
圖6 資料項和資料節點共用哈希空間
圖7是一個二進制長度為5的哈希空間,該空間可以表達的數值範圍是0~31(2^5),是一個首尾相接的環狀序列。環上的大圈表示不同的機器節點(一般是虛拟節點),用 Ni 來表示,i 代表着節點在哈希空間的位置。例如,某個節點根據 IP 位址和端口号進行哈希計算後得出的值是7,那麼 N7 則代表則該節點在哈希空間中的位置。由于每個實體機的配置不一樣,通常配置高的實體節點會虛拟成環上的多個節點。
圖7 長度為5的哈希空間
環上的節點把哈希空間分成多個區間,每個節點負責存儲其中一個區間的資料。例如 N14 節點負責存儲 Hash 值為8~14範圍内的資料,N7 節點負責存儲 Hash 值為31、0~7區間的資料。環上的小圈表示實際要存儲的一項資料,當一項資料通過 Hash 計算出其在哈希環中的位置後,會在環中順時針找到離其最近的節點,該項資料将會儲存在該節點上。例如,一項資料通過 Hash 計算出值為16,那麼應該存在 N18 節點上。通過上述方式,就可以将資料分布式存儲在叢集的不同節點,實作資料分片的功能。
- 節點下線
如圖8所示,節點 N18 出現故障被移除了,那麼之前 N18 節點負責的 Hash 環區間,則被順時針移到 N23 節點,N23 節點存儲的區間由19~23擴充為15~23。N18 節點下線後,Hash 值為16的資料項将會儲存在 N23 節點上。
圖8 一緻性哈希環中節點下線
- 節點上線
如圖9所示,如果叢集中上線一個新節點,其 IP 和端口進行 Hash 後的值為17,那麼其節點名為 N17。那麼 N17 節點所負責的哈希環區間為15~17,N23 節點負責的哈希區間縮小為18~23。N17 節點上線後,Hash 值為16的資料項将會儲存在 N17 節點上。
圖9 一緻性哈希環中節點上線
當節點動态變化時,一緻性哈希仍能夠保持資料的均衡性,同時也避免了全局資料的重新哈希和資料同步。但是,發生變化的兩個相鄰節點所負責的資料分布範圍依舊是會發生變化的,這對資料同步帶來了不便。資料同步一般是通過記錄檔來實作的,而一緻性雜湊演算法的記錄檔往往和資料分布相關聯,在資料分布範圍不穩定的情況下,記錄檔的位置也會随着機器動态上下線而發生變化,在這種場景下難以實作資料的精準同步。例如,上圖中 Hash 環有0~31個取值,假如日志檔案按照這種哈希值來命名的話,那麼 data-16.log 這個檔案日志最初是在 N18 節點,N18 節點下線後,N23 節點也有 data-16.log 了,N17 節點上線後,N17 節點也有 data-16.log 了。是以,需要有一種機制能夠保證記錄檔的位置不會因為節點動态變化而受到影響。
虛拟桶預分片
虛拟桶則是将 key-node 映射進行了分解,在資料項和節點之間引入了虛拟桶這一層。如圖所示,資料路由分為兩步,先通過 key 做 Hash 運算計算出資料項應所對應的 slot,然後再通過 slot 和節點之間的映射關系得出該資料項應該存在哪個節點上。其中 slot 數量是固定的,key - slot 之間的哈希映射關系不會因為節點的動态變化而發生改變,資料的記錄檔也和slot相對應,進而保證了資料同步的可行性。
圖10 虛拟桶預分片機制
路由表中存儲着所有節點和所有 slot 之間的映射關系,并盡量確定 slot 和節點之間的映射是均衡的。這樣,在節點動态變化的時候,隻需要修改路由表中 slot 和動态節點之間的關系即可,既保證了彈性擴縮容,也降低了資料同步的難度。
SOFARegistry 的分片選擇
通過上述一緻性哈希分片和虛拟桶分片的對比,我們可以總結一下它們之間的差異性:一緻性哈希比較适合分布式緩存類的場景,這種場景重在解決資料均衡分布、避免資料熱點和緩存加速的問題,不保證資料的高可靠,例如 Memcached;而虛拟桶則比較适合通過資料多副本來保證資料高可靠的場景,例如 Tair、Cassandra。
顯然,SOFARegistry 比較适合采用虛拟桶的方式,因為服務注冊中心對于資料具有高可靠性要求。但由于曆史原因,SOFARegistry 最早選擇了一緻性哈希分片,是以同樣遇到了資料分布不固定帶來的資料同步難題。我們如何解決的呢?我們通過在 DataServer 記憶體中以 dataInfoId 的粒度記錄記錄檔,并且在 DataServer 之間也是以 dataInfoId 的粒度去做資料同步(一個服務就由一個 dataInfoId 唯辨別)。其實這種日志記錄的思想和虛拟桶是一緻的,隻是每個 datainfoId 就相當于一個 slot 了,這是一種因曆史原因而采取的妥協方案。在服務注冊中心的場景下,datainfoId 往往對應着一個釋出的服務,是以總量還是比較有限的,以螞蟻金服目前的規模,每台 DataServer 中承載的 dataInfoId 數量也僅在數萬的級别,勉強實作了 dataInfoId 作為 slot 的資料多副本同步方案。
DataServer 擴縮容相關源碼
注:本次源碼解讀基于 registry-server-data 的5.3.0版本。
DataServer 的核心啟動類是 DataServerBootstrap,該類主要包含了三類元件:節點間的 bolt 通信元件、JVM 内部的事件通信元件、定時器元件。
圖11 DataServerBootstrap 的核心元件
- 外部節點通信元件:在該類中有3個 Server 通信對象,用于和其它外部節點進行通信。其中 httpServer 主要提供一系列 http 接口,用于 dashboard 管理、資料查詢等;dataSyncServer 主要是處理一些資料同步相關的服務;dataServer 則負責資料相關服務;從其注冊的 handler 來看,dataSyncServer 和 dataSever 的職責有部分重疊;
- JVM 内部通信元件:DataServer 内部邏輯主要是通過事件驅動機制來實作的,圖12列舉了部分事件在事件中心的互動流程,從圖中可以看到,一個事件往往會有多個投遞源,非常适合用 EventCenter 來解耦事件投遞和事件處理之間的邏輯;
- 定時器元件:例如定時檢測節點資訊、定時檢測資料版本資訊;
圖12 DataServer 中的核心事件流轉
DataServer 節點擴容
假設随着業務規模的增長,Data 叢集需要擴容新的 Data 節點。如圖13,Data4 是新增的 Data 節點,當新節點 Data4 啟動時,Data4 處于初始化狀态,在該狀态下,對于 Data4 的資料寫操作被禁止,資料讀操作會轉發到其它節點,同時,存量節點中屬于新節點的資料将會被新節點和其副本節點拉取過來。
圖13 DataServer 節點擴容場景
- 轉發讀操作
在資料未同步完成之前,所有對新節點的讀資料操作,将轉發到擁有該資料分片的資料節點。
查詢服務資料處理器 GetDataHandler:
public Object doHandle(Channel channel, GetDataRequest request) {
String dataInfoId = request.getDataInfoId();
if (forwardService.needForward()) {
// ... 如果不是WORKING狀态,則需要轉發讀操作
return forwardService.forwardRequest(dataInfoId, request);
}
}
轉發服務 ForwardServiceImpl:
public Object forwardRequest(String dataInfoId, Object request) throws RemotingException {
// 1. get store nodes
List<DataServerNode> dataServerNodes = DataServerNodeFactory
.computeDataServerNodes(dataServerConfig.getLocalDataCenter(), dataInfoId,
dataServerConfig.getStoreNodes());