SOFAStack( Scalable Open Financial Architecture Stack )是螞蟻金服自主研發的金融級雲原生架構,包含了構建金融級雲原生架構所需的各個組件,是在金融場景裏錘鍊出來的最佳實踐。
SOFARegistry 是螞蟻金服開源的具備承載海量服務註冊和訂閱能力的、高可用的服務註冊中心,最先源自於淘寶的第一版 ConfigServer,在支付寶/螞蟻金服的業務發展驅動下,近十年間已經演進至第五代。java
本文爲《剖析 | SOFARegistry 框架》最後一篇,本篇做者404P(花名巖途)。《剖析 | SOFARegistry 框架》系列由 SOFA 團隊和源碼愛好者們出品,項目代號:<SOFA:RegistryLab/>,文末包含往期系列文章。node
GitHub 地址:https://github.com/sofastack/sofa-registrygit
在微服務架構體系下,服務註冊中心致力於解決微服務之間服務發現的問題。在服務數量很少的狀況下,服務註冊中心集羣中每臺機器都保存着全量的服務數據,但隨着螞蟻金服海量服務的出現,單機已沒法存儲全部的服務數據,數據分片成爲了必然的選擇。數據分片以後,每臺機器只保存一部分服務數據,節點上下線就容易形成數據波動,很容易影響應用的正常運行。本文經過介紹 SOFARegistry 的分片算法和相關的核心源碼來展現螞蟻金服是如何解決上述問題的。~~github
在微服務架構下,一個互聯網應用的服務端背後每每存在大量服務間的相互調用。例如服務 A 在鏈路上依賴於服務 B,那麼在業務發生時,服務 A 須要知道服務 B 的地址,才能完成服務調用。而分佈式架構下,每一個服務每每都是集羣部署的,集羣中的機器也是常常變化的,因此服務 B 的地址不是固定不變的。若是要保證業務的可靠性,服務調用者則須要感知被調用服務的地址變化。算法
圖1 微服務架構下的服務尋址編程
既然成千上萬的服務調用者都要感知這樣的變化,那這種感知能力便下沉成爲微服務中一種固定的架構模式:服務註冊中心。緩存
圖2 服務註冊中心服務器
服務註冊中內心,有服務提供者和服務消費者兩種重要的角色,服務調用方是消費者,服務被調方是提供者。對於同一臺機器,每每兼具二者角色,既被其它服務調用,也調用其它服務。服務提供者將自身提供的服務信息發佈到服務註冊中心,服務消費者經過訂閱的方式感知所依賴服務的信息是否發生變化。網絡
SOFARegistry 的架構中包括4種角色:Client、Session、Data、Meta,如圖3所示:session
圖3 SOFARegistry 整體架構
應用服務器集羣。Client 層是應用層,每一個應用系統經過依賴註冊中心相關的客戶端 jar 包,經過編程方式來使用服務註冊中心的服務發佈和服務訂閱能力。
Session 服務器集羣。顧名思義,Session 層是會話層,經過長鏈接和 Client 層的應用服務器保持通信,負責接收 Client 的服務發佈和服務訂閱請求。該層只在內存中保存各個服務的發佈訂閱關係,對於具體的服務信息,只在 Client 層和 Data 層之間透傳轉發。Session 層是無狀態的,能夠隨着 Client 層應用規模的增加而擴容。
數據服務器集羣。Data 層經過分片存儲的方式保存着所用應用的服務註冊數據。數據按照 dataInfoId(每一份服務數據的惟一標識)進行一致性 Hash 分片,多副本備份,保證數據的高可用。下文的重點也在於隨着數據規模的增加,Data 層如何在不影響業務的前提下實現平滑的擴縮容。
元數據服務器集羣。這個集羣管轄的範圍是 Session 服務器集羣和 Data 服務器集羣的服務器信息,其角色就至關於 SOFARegistry 架構內部的服務註冊中心,只不過 SOFARegistry 做爲服務註冊中心是服務於廣大應用服務層,而 Meta 集羣是服務於 SOFARegistry 內部的 Session 集羣和 Data 集羣,Meta 層可以感知到 Session 節點和 Data 節點的變化,並通知集羣的其它節點。
在螞蟻金服的業務規模下,單臺服務器已經沒法存儲全部的服務註冊數據,SOFARegistry 採用了數據分片的方案,每臺機器只保存一部分數據,同時每臺機器有多副本備份,這樣理論上能夠無限擴容。根據不一樣的數據路由方式,常見的數據分片主要分爲兩大類:範圍分片和 Hash(哈希)分片。
圖4 數據分片
每個數據分片負責存儲某一鍵值區間範圍的值。例如按照時間段進行分區,每一個小時的 Key 放在對應的節點上。區間範圍分片的優點在於數據分片具備連續性,能夠實現區間範圍查詢,可是缺點在於沒有對數據進行隨機打散,容易存在熱點數據問題。
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 值爲3一、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 和動態節點之間的關係便可,既保證了彈性擴縮容,也下降了數據同步的難度。
經過上述一致性哈希分片和虛擬桶分片的對比,咱們能夠總結一下它們之間的差別性:一致性哈希比較適合分佈式緩存類的場景,這種場景重在解決數據均衡分佈、避免數據熱點和緩存加速的問題,不保證數據的高可靠,例如 Memcached;而虛擬桶則比較適合經過數據多副原本保證數據高可靠的場景,例如 Tair、Cassandra。
顯然,SOFARegistry 比較適合採用虛擬桶的方式,由於服務註冊中心對於數據具備高可靠性要求。但因爲歷史緣由,SOFARegistry 最先選擇了一致性哈希分片,因此一樣遇到了數據分佈不固定帶來的數據同步難題。咱們如何解決的呢?咱們經過在 DataServer 內存中以 dataInfoId 的粒度記錄操做日誌,而且在 DataServer 之間也是以 dataInfoId 的粒度去作數據同步(一個服務就由一個 dataInfoId 惟標識)。其實這種日誌記錄的思想和虛擬桶是一致的,只是每一個 datainfoId 就至關於一個 slot 了,這是一種因歷史緣由而採起的妥協方案。在服務註冊中心的場景下,datainfoId 每每對應着一個發佈的服務,因此總量仍是比較有限的,以螞蟻金服目前的規模,每臺 DataServer 中承載的 dataInfoId 數量也僅在數萬的級別,勉強實現了 dataInfoId 做爲 slot 的數據多副本同步方案。
注:本次源碼解讀基於 registry-server-data 的5.3.0版本。
DataServer 的核心啓動類是 DataServerBootstrap,該類主要包含了三類組件:節點間的 bolt 通訊組件、JVM 內部的事件通訊組件、定時器組件。
圖11 DataServerBootstrap 的核心組件
圖12 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()); // 2. find nex node boolean next = false; String localIp = NetUtil.getLocalAddress().getHostAddress(); DataServerNode nextNode = null; for (DataServerNode dataServerNode : dataServerNodes) { if (next) { nextNode = dataServerNode; break; } if (null != localIp && localIp.equals(dataServerNode.getIp())) { next = true; } } // 3. invoke and return result }
轉發讀操做時,分爲3個步驟:首先,根據當前機器所在的數據中心(每一個數據中心都有一個哈希空間)、 dataInfoId 和數據備份數量(默認是3)來計算要讀取的數據項所在的節點列表;其次,從這些節點列表中找出一個 IP 和本機不一致的節點做爲轉發目標節點;最後,將讀請求轉發至目標節點,並將讀取的數據項返回給 session 節點。
圖14 DataServer 節點擴容時的讀請求
在數據未同步完成以前,禁止對新節點的寫數據操做,防止在數據同步過程當中出現新的數據不一致狀況。
發佈服務處理器 PublishDataHandler
public Object doHandle(Channel channel, PublishDataRequest request) { if (forwardService.needForward()) { // ... response.setSuccess(false); response.setMessage("Request refused, Server status is not working"); return response; } }
圖15 DataServer 節點擴容時的寫請求
以圖16爲例,數據項 Key 12 的讀寫請求均落在 N14 節點上,當 N14 節點接收到寫請求後,會同時將數據同步給後繼的節點 N1七、N23(假設此時的副本數是 3)。當 N14 節點下線,MetaServer 感知到與 N14 的鏈接失效後,會剔除 N14 節點,同時向各節點推送 NodeChangeResult 請求,各數據節點收到該請求後,會更新本地的節點信息,並從新計算環空間。在哈希空間從新刷新以後,數據項 Key 12 的讀取請求均落在 N17 節點上,因爲 N17 節點上有 N14 節點上的全部數據,因此此時的切換是平滑穩定的。
圖16 DataServer 節點縮容時的平滑切換
MetaServer 會經過網絡鏈接感知到新節點上線或者下線,全部的 DataServer 中運行着一個定時刷新鏈接的任務 ConnectionRefreshTask,該任務定時去輪詢 MetaServer,獲取數據節點的信息。須要注意的是,除了 DataServer 主動去 MetaServer 拉取節點信息外,MetaServer 也會主動發送 NodeChangeResult 請求到各個節點,通知節點信息發生變化,推拉獲取信息的最終效果是一致的。
當輪詢信息返回數據節點有變化時,會向 EventCenter 投遞一個 DataServerChangeEvent 事件,在該事件的處理器中,若是判斷出是當前機房節點信息有變化,則會投遞新的事件 LocalDataServerChangeEvent,該事件的處理器 LocalDataServerChangeEventHandler 中會判斷當前節點是否爲新加入的節點,若是是新節點則會向其它節點發送 NotifyOnlineRequest 請求,如圖17所示:
圖17 DataServer 節點上線時新節點的邏輯
同機房數據節點變動事件處理器 LocalDataServerChangeEventHandler
public class LocalDataServerChangeEventHandler { // 同一集羣數據同步器 private class LocalClusterDataSyncer implements Runnable { public void run() { if (LocalServerStatusEnum.WORKING == dataNodeStatus.getStatus()) { //if local server is working, compare sync data notifyToFetch(event, changeVersion); } else { dataServerCache.checkAndUpdateStatus(changeVersion); //if local server is not working, notify others that i am newer notifyOnline(changeVersion);; } } } }
圖17展現的是新加入節點收到節點變動消息的處理邏輯,若是是線上已經運行的節點收到節點變動的消息,前面的處理流程都相同,不一樣之處在於 LocalDataServerChangeEventHandler 中會根據 Hash 環計算出變動節點(擴容場景下,變動節點是新節點,縮容場景下,變動節點是下線節點在 Hash 環中的後繼節點)所負責的數據分片範圍和其備份節點。
當前節點遍歷自身內存中的數據項,過濾出屬於變動節點的分片範圍的數據項,而後向變動節點和其備份節點發送 NotifyFetchDatumRequest 請求, 變動節點和其備份節點收到該請求後,其處理器會向發送者同步數據(NotifyFetchDatumHandler.fetchDatum),如圖18所示。
圖18 DataServer 節點變動時已存節點的邏輯
SOFARegistry 爲了解決海量服務註冊和訂閱的場景,在 DataServer 集羣中採用了一致性 Hash 算法進行數據分片,突破了單機存儲的瓶頸,理論上提供了無限擴展的可能性。同時 SOFARegistry 爲了實現數據的高可用,在 DataServer 內存中以 dataInfoId 的粒度記錄服務數據,並在 DataServer 之間經過 dataInfoId 的緯度進行數據同步,保障了數據一致性的同時也實現了 DataServer 平滑地擴縮容。