SOFAStack (Scalable Open Financial Architecture Stack)是螞蟻金服自主研發的金融級分布式架構,包含了建構金融級雲原生架構所需的各個元件,是在金融場景裡錘煉出來的最佳實踐。
SOFARegistry 是螞蟻金服開源的具有承載海量服務注冊和訂閱能力的、高可用的服務注冊中心,最早源自于淘寶的初版 ConfigServer,在支付寶/螞蟻金服的業務發展驅動下,近十年間已經演進至第五代。
本文為《剖析 | SOFARegistry 架構》第二篇,本篇作者尚彧,是 SOFARegistry 開源負責人。《剖析 | SOFARegistry 架構》系列由 SOFA 團隊和源碼愛好者們出品,項目代号:<SOFA:RegistryLab/>,文末附共建清單,歡迎領取共建~
https://github.com/sofastack/sofa-registry
概述
無論傳統的 SOA 還是目前的微服務架構,都離不開分布式的特性,既然服務是分布的就必須解決服務尋址的問題。服務注冊中心是這個過程最主要的元件,通過服務注冊和服務發現特性收集服務供求關系,解耦服務消費方對服務提供方的服務定位問題。
服務注冊中心的最主要能力是服務注冊和服務發現兩個過程。服務注冊的過程最重要是對服務釋出的資訊進行存儲,服務發現的過程是把服務釋出端的所有變化(包括節點變化和服務資訊變化)及時準确的通知到訂閱方的過程。
本文較長的描述服務注冊中心 SOFARegistry 對于服務發現的實作和技術演進過程,主要涉及 SOFARegistry 的服務發現實作模式以及服務資料變化後及時推送到海量用戶端感覺的優化過程。
服務發現分類
分布式理論最重要的一個理論是 CAP 原理。關于注冊中心的解決方案,根據存儲資料一緻性次元劃分業界有很多實作,比如最有代表性的強一緻性 CP 系統 ZooKeeper 和最終一緻性 AP 系統 Eureka。SOFARegistry 在資料存儲層面采用了類似 Eureka 的最終一緻性的過程,但是存儲内容上和 Eureka 在每個節點存儲相同内容特性不同,采用每個節點上的内容按照一緻性 Hash 資料分片來達到資料容量無限水準擴充能力。
服務端發現和用戶端發現
抛開資料存儲的一緻性,我們從服務發現的實作次元考慮服務注冊中心的分類,業界也按照服務位址選擇發生主體和負載均衡政策實作主體分為用戶端服務發現和服務端服務發現。
- 用戶端服務發現:即由用戶端負責決定可用的服務執行個體的"位置"以及與其相關的負載均衡政策,就是服務發現的位址清單在用戶端緩存後由用戶端自己根據負載均衡政策進行選址完成最終調用,位址清單定期進行重新整理或服務端主動通知變更。最主要的缺點是需要有用戶端實作,對于各種異構系統不同語言不同結構的實作必須要進行對應的用戶端開發,不夠靈活,成本較高。
- 服務端服務發現:在服務端引入了專門的負載均衡層,将用戶端與服務發現相關的邏輯搬離到了負載均衡層來做。用戶端所有的請求隻會通過負載均衡子產品,其并不需要知會微服務執行個體在哪裡,位址是多少。負載均衡子產品會查詢服務注冊中心,并将用戶端的請求路由到相關可用的微服務執行個體上。這樣可以解決大量不同實作應用對用戶端的依賴,隻要對服務端的負載均衡子產品發請求就可以了,由負載均衡層擷取服務發現的位址清單并最終确定目标位址進行調用。
- SOFARegistry 服務發現模式:以用戶端服務發現模式為主。這樣的模式實作比較直接,因為在同一個公司内部實踐面對的絕大多數應用基本上都是同一個語言實作的,用戶端實作也隻需要确定一套,每個用戶端通過業務内嵌依賴方式部署,并且可以根據業務需求進行定制負載均衡政策進行選址調用。當然也會遇到特殊的異構系統,這個随着微服務架構 RPC 調用等通信能力下沉到 Mesh 執行也得到解決,可以在 Mesh 層進行特定的服務注冊中心用戶端嵌入,選擇路由都在這裡統一進行,對不同語言實作的系統進行無感覺。
服務發現的推、拉模型
服務發現最重要的過程是擷取服務釋出方位址清單的過程,這個過程可以分為兩種實作模式:用戶端主動擷取的拉模式和服務端主動變更通知的推送模式:
- 拉模式主要是在用戶端按照訂閱關系發起主動拉取過程。用戶端在首次訂閱可以進行一次相關服務 ID 的服務清單查詢,并拉取到本地緩存,後續通過長輪詢定期進行服務端服務 ID 的版本變更檢測,如果有新版本變更則及時拉取更新本地緩存達到和服務端一緻。這種模式在服務端可以不進行訂閱關系的存儲,隻需要存儲和更新服務釋出資料。由用戶端主動發起的資料擷取過程,對于用戶端實作較重,需要主動擷取和定時輪訓,服務端隻需要關注服務注冊資訊的變更和健康情況及時更新記憶體。這個過程由于存在輪訓周期,對于時效性要求不高的情況比較适用。
- 推模式主要是從服務端發起的主動變更推送。這個模式主要資料壓力集中在服務端,對于服務注冊資料的變更和提供方,節點每一次變更情況都需要及時準确的推送到用戶端,更新用戶端緩存。這個資料推送量較大,在資料釋出頻繁變更的過程,對于大量訂閱方的大量資料推送頻繁執行,資料壓力巨大,但是資料變更資訊及時,對于每次變更都準确反映到用戶端。
- SOFARegistry 服務發現模式采用的是推拉結合方式。用戶端訂閱資訊釋出到服務端時可以進行一次位址清單查詢,擷取到全量資料,并且把對應的服務 ID 版本資訊存儲在 Session 回話層,後續如果服務端釋出資料變更,通過服務 ID 版本變更通知回話層 Session,Session 因為存儲用戶端訂閱關系,了解哪些用戶端需要這個服務資訊,再根據版本号大小決定是否需要推送給這個版本較舊的訂閱者,用戶端也通過版本比較确定是否更新本次推送的結果覆寫記憶體。此外,為了避免某次變更通知擷取失敗,定期還會進行版本号差異比較,定期去拉取版本低的訂閱者所需的資料進行推送保證資料最終一緻。
SOFARegistry 服務發現模式
資料分層
前面的文章介紹過 SOFARegistry 内部進行了資料分層,在服務注冊中心的服務端因為每個存儲節點對應的用戶端的連結資料量有限,必須進行特殊的一層劃分用于專門收斂無限擴充的用戶端連接配接,然後在透傳相應的請求到存儲層,這一層是一個無資料狀态的代理層,我們稱之為 Session 層。
此外,Session 還承載了服務資料的訂閱關系,因為 SOFARegistry 的服務發現需要較高的時效性,對外表現為主動推送變更到用戶端,是以推送的主體實作也集中在 Session 層,内部的推拉結合主要是通過 Data 存儲層的資料版本變更推送到所有 Session 節點,各個 Session 節點根據存儲的訂閱關系和首次訂閱擷取的資料版本資訊進行比對,最終确定推送給那些服務消費方用戶端。
觸發服務推送的場景
直覺上服務推送既然是主動的,必然發生在主動擷取服務時刻和服務提供方變更時刻:
- 主動擷取:服務訂閱資訊注冊到服務端時,需要查詢所有的服務提供方位址,并且需要将查詢結果推送到用戶端。這個主動查詢并且拉取的過程,推送端是一個固定的用戶端訂閱方,不涉及服務 ID 版本資訊判定,直接擷取清單進行推送即可,主要發生在訂閱方應用剛啟動時刻,這個時期可能沒有服務釋出方釋出資料,會查詢到空清單給用戶端,這個過程基本上類似一個同步過程,展現為用戶端一次查詢結果的同步傳回。
- 版本變更:為了确定服務釋出資料的變更,我們對于一個服務不僅定義了服務 ID,還對一個服務 ID 定義了對應的版本資訊。服務釋出資料變更主動通知到 Session 時,Session 對服務 ID 版本變更比較,高版本覆寫低版本資料,然後進行推送。這次推送是比較大面積的推送,因為對于這個服務 ID 感興趣的所有用戶端訂閱方都需要推送,并且需要按照不同訂閱次元和不同類型的用戶端進行資料組裝,進行推送。這個過程資料量較大,并且需要所有訂閱方都推送成功才能更新目前存儲服務 ID 版本,需要版本更新确認,由于性能要求必須并發執行并且異步确定推送成功。
- 定期輪訓:因為有了服務 ID 的版本号,Session 可以定期發起版本号比較,如果Session 存儲的的服務 ID 版本号低于dataServer存儲的 ,Session再次拉取新版本資料進行推送,這樣避免了某次變更通知沒有通知到所有訂閱方的情況。
服務推送性能優化
服務訂閱方的數量決定了資料推送一次的數量,對于一台 Session 機器來說目前我們存儲 sub 數量達到60w+,如果服務釋出方頻繁變更,對于每次變更推送量是巨大的,故我們對整個推送的過程進行優化處理:
- 服務釋出方頻繁變更優化:在所有業務叢集啟動初期,每次對于一個相同的服務,會有很多服務提供方并發不停的新增,如果對于每次新增的提供方都進行一次推送顯然不合理,我們對這個情況進行服務提供方的合并,即每個服務推送前進行一定延遲等待所有pub新增到一定時間進行一次推送。這個處理極大的減少推送的頻率,提升推送效率。
- 即使對服務變更進行了合并延遲處理,但是推送任務産生也是巨大的,是以對于瞬間産生的這麼大的任務量進行隊列緩沖處理是必須的。目前進行所有推送任務會根據服務 ID、推送方 IP 和推送方資訊組成唯一任務 ID 進行任務入隊處理。隊列當中如果是相同的服務變更産生推送任務,則進行任務覆寫,執行最後一次版本變更的任務。此外任務執行進行分批次處理,批次大小可以配置,每個批次處理完成再擷取任務批次進行處理。
異常處理
對于這麼大資料量的推送過程必然會因為網絡等因素推送失敗,對于失敗的異常推送場景我們如何處理:
- 重試機制:很顯然推送失敗的用戶端訂閱依然還在,或者對應的連結還存在,這個失敗的推送必須進行重試,重試機制定義十分重要。
- 目前對于上述首次啟動主動擷取資料進行推送的重試進行了有限次重試,并且每次重試之前進行網絡監測和新版本變更檢測,此外進行了時間延遲間隔,保證網絡故障重試的成功幾率。
- 這個延遲重試,最初我們采用簡單的 sleep 方式,終止目前線程然後再發起推送請求。這個方式對于資源消耗巨大,如果出現大量的任務重試,會産生大量的線程停止占用記憶體,同時 sleep 方式對于恢複運作也不是很準确,完全取決于系統排程時間。後續我們對重試任務進行時間輪算法分片進行,對于所有重試任務進行了時間片定義,時間輪詢執行對應時間片重試任務執行,效率極大提升,并且占用資源很小。
- 補償措施:對于推送失敗之前也說有定時任務進行輪訓服務 ID 版本,服務 ID 的版本在所有推送方都接受到這個版本變更推送才進行更新,如果有一個訂閱方推送失敗,就不更新版本。後續持續檢查版本再啟動任務,對沒有推送成功的訂閱方反複執行推送,直到推送成功或者訂閱方不存在,這個過程類似于無限重試的過程。
資料處理分階段
注冊中心資料的來源主要來自于兩個方向,一個是大量應用用戶端新連接配接上來并且釋出和訂閱資料并存儲在注冊中心的階段,另外一個是之前這些釋出的服務資料必須按照訂閱方的需求推送出去的階段。這兩個階段資料量都非常巨大,都在首次部署注冊中心後發生,如果同時對伺服器進行沖擊網絡和 CPU 都會成為瓶頸,故我們通過運維模式進行了兩個階段資料的分離處理:
- 關閉推送開關:我們在所有注冊中心啟動初期進行了推送開關關閉的處理,這樣在服務注冊中心新啟動或者新釋出初期,因為用戶端有本地緩存,在推送關閉的情況下,注冊中心的啟動隻從用戶端新注冊資料,沒有推送新的内容給用戶端,做到對現有運作系統最小影響。并且,由于推送關閉,資料隻處理新增的内容這樣對網絡和 CPU 壓力減少。
- 開推送:在關閉推送時刻記錄沒有推送過的訂閱者,所有資料注冊完成(主要和釋出之前的資料數量比較),沒有明顯增長情況下,打開推送,對于所有訂閱方進行資料推送更新記憶體。