突發熱點事件下微博高可用註冊中心vintage的設計&實踐
文章概要
當前微博服務化採用公有云+私有云的混合雲部署方式,承載了每天百億級的流量,vintage 作為微博微服務的註冊中心,為管理 10w 級微服務節點以及在流量激增情況下的服務快速擴縮容,面臨了極大挑戰。例如:複雜網路條件下 vintage 服務的高可用保障、解決大批量節點狀態變更觸發的通知風暴、異常情況下 vintage 節點的自適應和自恢復能力、vintage 多雲部署場景下節點資料同步和一致性保障等。
vintage 3.0 優雅的解決了上述問題,具備了良好的擴充套件性,可用性達到 6 個 9。在 12 月 8 日北京 ArchSummit 全球架構師峰會上,來自微博研發中心的邊劍,以挑戰和問題切入與大家分享了 vintage 設計原則與方案,以及線上應用的最佳實踐。
一、突發熱點對微博註冊中心的要求與挑戰
1.1 突發熱點事件
突發熱點是微博面臨的最常見、最重要的挑戰,左圖顯示的就是最近的一個熱點事件,右圖是熱點發生時的流量圖,我們可以看出熱點事件發生時,在短時間內會引發微博流量的數倍增長。那麼面對如海嘯般的突發流量,如何應對就顯得尤為重要。
根據峰值流量模型不同,應對策略多種多樣,但通常總結為下面三種措施:
常備充足的 buffer:該方式更佳適用於可提前預測的流量高峰。如:雙十一購物節,微博的春晚保障等。而極熱事件往往都具備突發性,很難提前預料,並做好充足的準備。同時極熱事件發生時的流量往往高於日常峰值的數倍,且不同極熱事件之間流量峰值和流量模型不盡相同,因此也很難準確評估 buffer 的數量。
採用服務降級的方式,保障核心業務:通過對系統介面邏輯中,非核心業務的逐步剪枝,來緩解核心介面整體壓力,保障業務介面的功能執行及效能穩定,這類方式在很多年前是比較流行的。但此類方式通常會伴隨系統功能受限,頁面內容不完整等是使用者體驗下降的情況發生。此外在極端峰值場景下,還可能會出現降無可降的情況。因此這種方法也不是目前最好的選擇。
彈性擴容: 為了克服前兩種措施帶來的不足,經過多年應對突發流量的經驗積累,微博目前使用混合雲的部署方式。通過對後端微服務資源的快速擴容,在短時間內迅速為整個系統構建起堅實的大壩。
1.2 突發熱點事件對微博註冊中心的要求與挑戰
眾所周知,微服務的執行必須依賴註冊中心的支援。這就對微博註冊中心提出了 4 點要求:
要支援每秒百級別以上的微服務擴容
秒級別的微服務變更通知延遲
能承載 10w+ 級別微服務節點
公司級多語言微服務註冊 & 服務發現支援
然而即使我們滿足了以上四個要求,但時常還會遇到網路抖動,流量極端峰值時專線頻寬擁塞等情況。那麼如何在網路狀態不好的條件下,保障微博註冊中心的可用性,實現微服務正常擴容就成為了微博註冊中心在設計時必須要面臨的挑戰。
二、vintage 高可用設計 & 實踐
2.1 設計目標:
針對上面提到的要求和挑戰,設計微博註冊中心時在功能上要實現公司級別的註冊中心及配置管理平臺,支援多 IDC,支援多語言,具備 10w+ 級微服務承載能力,可用性上需滿足 6 個 9。在服務效能方面,要具備每秒對百級別以上微服務的擴容能力。同時要求通知的平均延遲低於 500ms。而在伸縮性上,微博註冊中心自身必須具備十秒級節點擴容,以及分鐘級 IDC 註冊中心搭建。
2.2 選型對比
根據設計目標,對業界比較流行的註冊中心進行了調研。ZooKeeper,Etcd 無法較好滿足微博對多機房支援要求。Consul 雖然滿足了多機房的支援,但在網路 QoS 發生時由於其本身的 CP 模型設計,在服務可用性方面不能提供很好的保障。而 Eureka 雖然在多機房和可用性方面都滿足微博的要求,但 Eureka 目前僅支援以長輪詢的方式訂閱服務,在通知延遲方面不能很好的支援,同時 在社群支援方面 Eureka 已暫停了新版本的研發。
綜合考慮上述指標 ZooKeeper,Etcd,Consul,Eureka 均都無法很好的滿足微博對註冊中心提出的要求。因此我們決定在現有的 vintage 系統基礎上進行升級改造,來滿足具備公司級註冊中心的能力。
2.3 vintage 架構設計
vintage 部署拓撲
先從系統拓撲部署瞭解下新版 vintage 系統的整體設計。vintage 服務採用多 IDC 的部署方式,通過 Gossipe 協議,實現叢集節點間的自動發現。IDC 間主主通訊實現多機房間資料同步。IDC 內採用一主多從的部署方式,基於 Raft 實現了可支援分割槽多主的選舉協議,主從節點角色之間實現讀寫分離。
vintage 系統採用典型三層結構設計,將網路層,業務層,儲存層抽象分離。
最上層是 Restful 風格實現的 API 介面,提供微服務註冊,訂閱及健康檢測等功能。同時通過 307,308 模組完成對上下行流量的主,從節點分流。
中間的業務層包括命名和配置兩大核心業務的功能實現。在內部功能方面,Node discovery 模組負責叢集的全域性節點自動發現,timer 和 service state Machine(微服務狀態機)共同配合完成對微服務狀態的高效管理。同時在 vintage 中的任何註冊,登出及微服務元資料更新,節點狀態變更等行為都將被抽象為一個獨立事件並交由 event handler 進行統一處理。Transfer 和 repair 模組用於整個叢集的節點間資料同步及一致性修復功能。而為應對網路問題對註冊中心及整個微服務體系可用性產生的影響,vintage 內部通過 partion hanlder 實現網路分割槽狀態的感知並結合多樣化的系統保障策略予以應對。
最下面的儲存層:採用基於樹形結構的多版本資料儲存架構,實現了對眾多微服務及其所屬關係的有效管理,支援微服務的灰度釋出。event notify 模組將資料變更事件實時通知到業務層,配合 watch hub 完成對訊息訂閱者的實時推送。在資料可靠性方面,採用 aof+snapshot 方式實現本地的資料持久化。
2.4 高可用實踐
瞭解整個系統後,將通過網路分割槽,通知風暴,資料一致性,高可用部署四個方面介紹 vintage 在高可用方面的實踐,以及 AP 模型註冊中心在資料一致性保障方面的措施。
網路分割槽
場景一:vintage 內部網路分割槽
如上圖,網路分割槽將 vintage 叢集一分為二,同時每個分割槽中的節點數量均少於 n/2+1 個(n 為該 IDC 子叢集 vintage 服務的節點總數)。在該場景下基於傳統 Raft 協議的註冊中心,由於對資料強一致性的要求,將無法提供正常的讀寫服務。而 Etcd 和 Consul 在解決這個問題時,也僅是通過 stale read 的方式滿足了對資料讀取的需要,但仍然無法實現在該場景下對微服務註冊的需要。
微博註冊中心為實現在網路分割槽下注冊中心服務高可用,對原有 Raft 協議選舉機制進行改造,實現了可對任意分割槽情況下的多主選舉支援:
解決了分割槽發生後處於少數分割槽內 rpc-server 與 rpc-client 的註冊及服務發現能力。實現任何粒度分割槽下 vintage 服務的高可用。
對故障節點的承受能力大幅提升,由原 Raft 協議的 n/2-1 個節點,提升至最大 n-1 節點。
同時在該場景下網路分割槽後,微服務會將自己的心跳彙報到所屬分割槽的主節點。而由於分割槽存在,分割槽間節點資料無法通訊。對於(vintage)IDC 叢集中的其他主節點來說就出現了微服務心跳丟失的現象,而心跳是作為服務健康狀態的重要識別手段。心跳超時的微服務節點將會被標識為不可用狀態,從而不會被呼叫方訪問。網路分割槽時往往會出現大批量的微服務心跳超時,未能妥善處理將直接影響業務系統服務的穩定性及 SLA 指標。而此時 partion handler 模組將會優先於心跳超時感知到網路分割槽的發生,啟動 freeze 保護機制(freeze:註冊中心內部對微服務狀態的保護機制,基於樂觀策略凍結當前 vintage 系統中的微服務節點狀態,禁止節點不可用狀態的變更,同時對於接受到心跳的微服務節點,支援其恢復可用狀態)。
分割槽恢復後,各節點間通訊恢復正常。vintage 會進行再次選舉,從分割槽時的多個主節點中選舉出新的主節點。vintage 中的資料變更都會抽象為一個獨立事件,系統基於向量時鐘實現了對全域性事件的順序控制。在多主合併過程中通過 repair 模組完成節點間資料的一致性修復,保障 IDC 內叢集最終一致性。
場景二:client 與 vintage 間網路分割槽
圖中綠色和黃色區域分別代表兩個不同 IDC 中的 vintage 子叢集。紅色和紫色代表 IDC1 內的 rpc-server 和 rpc-clinet。此時紅色的 rpc-server 與 IDC1 中的 vintage 主節點出現了網路分割槽,而紫色的 rpc-client 與 IDC1 的 vintage 叢集整體存在網路隔離現象。rpc-server 和 rpc-client 之間網路通訊正常。雖然這是一個相對複雜且極端的分割槽場景,但對於微博註冊中心的高可用設計要求是必須要解決的問題。下面我們來看看 vintage 是如何解決的。
在該場景下 rpc-server 可通過 DNS 發現的方式獲取該 IDC 內 vintage 子叢集的全部機器列表,並使用 proxy 模式將註冊請求傳送至子叢集中任意可達的從節點。並通過從節點代理,將註冊請求轉發至主節點完成服務註冊。rpc-server 會定期檢測自己與主節點的連線狀態,一旦發現連線恢復,將自動關閉 proxy 模式,並通過向主節點發送心跳維護自身狀態。
rpc-client 可通過跨 IDC 服務發現方式,在 IDC2 中訂閱所關注的 rpc-server 服務資訊,其自身還會定期將獲取到的 rpc-serve 服務列表儲存至本地 snapshot 檔案。防止即使 rpc-client 與全部的 vintage 服務均出現網路隔離,也可保證自身正常啟動及呼叫。從而實現了 client 與 vintage 服務分割槽時,微服務註冊與服務發現的高可用。
注:vintage 服務同樣支援跨 IDC 的微服務註冊。但從實際訪問及運維模型考慮,要求每個 rpcserver 節點在同一時刻僅可屬於一個 IDC 叢集。
通知風暴:
在微博場景下,根據呼叫方對每種微服務的訂閱數量的不同,通常會出現百級至千級別的通知放大。同時老版本 vintage 的服務訂閱是基於 sign(md5 後服務列表指紋)對比 + 全量資料拉取的方式,根據微服務自身規模的大小,會出現十級至百級別的訊息體放大(二次放大現象)。當出現大量(百級別)微服務同時擴縮容,網路 QoS 或大面積微服務機器故障。經過兩次放大後,通知規模可達到百萬甚至千萬級別,嚴重佔用機器頻寬資源。頻寬不足導致大量心跳階段性丟失,微服務的狀態極易在‘不可用’和‘正常’之間頻繁轉換,由此帶來更大量的更變事件,出現風暴疊加,導致網路擁塞。更將引發下面兩個問題:1. 註冊中心業務介面呼叫失敗;2. 大面積微服務心跳彙報失敗,被錯誤標示為不可用狀態,影響線上業務微服務的正常呼叫。
針對通知風暴嚴重危害,改造後的 vintage 系統使用了“梳”和“保”兩種策略應對。
“疏”主要採用的三種策略
分流及負載均衡:首先網際網路公司通常根據使用者的地域和運營商等資訊,將請求分流至不同 IDC,並在核心 IDC 內部部署全量的核心服務,保障 IDC 內完成對使用者請求的響應。vintage 根據微博流量分配及業務服務多機房部署的特點,在公司幾大核心機房均有部署。實現了多機房微服務上行請求的流量分離。同時根據註冊中心寫少讀多的特點在 IDC 內採用一主多從的部署結構,除保障了服務的 HA 外,也對下行流量實現了負載均衡。
快速擴容:對於通知風暴造成的下行流量,可通過對從節點快速擴容的方式提供充足的服務吞吐及頻寬。
增量事件,避免訊息體二次放大:vintage 內部將全部的資料變更,統一抽象為獨立事件並在叢集資料同步,一致性修復和服務發現 & 訂閱功能上均採用增量通知方式,避免了訊息體放大問題。將整體訊息量有效壓縮 2-3 個數量級,降低頻寬的使用。而發生事件溢位時,系統也會先對全量資料壓縮後再進行傳遞。
“保”是通過系統多重防護策略,保障通知風暴下 vintage 及微服務的狀態穩定。其主要採用的三種策略:
首先使用 partion handler 實現對 vintage 叢集內分割槽和微服務與叢集節點分割槽網路問題的綜合監控。結合 freeze 機制有效避免了微服務節點狀態的頻繁變更從而同樣避免了在網路 QoS 引發的通知風暴。
其次對於業務系統來說,系統整體服務狀態相對穩定。基於這點 vintage 為服務增加了保護閾值的設計(預設 60%)。通過設定保護閾值,可有效控制通知風暴發生時 vintage 內部異常服務資料變更的規模。更保障業務系統的整體穩定,避免發生服務雪崩。
最後對於頻寬枯竭問題,vintage 增加了對頻寬使用率的檢測和限制。當超過機器頻寬消耗>70%,會觸發系統 304 降級,暫停全部通知推送,避免 vintage 節點頻寬枯竭保障叢集角色心跳維護,節點發現,資料同步等內部功能對頻寬的基本要求,維護整個叢集的穩定執行。
資料一致性
不難看出 vintage 是基於 AP 模型設計的註冊中心。該模型下注冊中心內節點間資料的一致性問題,成為了 vintage 必須要處理的核心問題。vintage 選擇了最終一致性作為叢集的資料一致性模型。
接下來介紹 vintage 的最終一致性實現機制。
首先來回顧下 vintage 叢集的部署拓撲。vintage 採用多 IDC 部署,各 IDC 間分治獨立,IDC 間通過主主節點完成資料同步,IDC 內主從角色讀寫分離。在該拓撲下,vintage 通過叢集內主從,叢集間主主的資料同步 + 差異對比修復相結合的方式,實現節點間資料的最終一致性。(1) 資料同步:各 IDC 內均由主節點負責上行請求處理,從節點通過拉取主節點新增的變更事件完成資料同步。(2) 同時 vintage 內部對全部儲存的資料構建了對應的 merkle Tree,並隨資料變化進行實時更新。
vintage 會定期觸發叢集一致性檢測邏輯。以主節點中資料為標準,IDC 內採用主從,IDC 間則是主主方式使用 merkle tree 特點由根節點逐層對比,精確定位資料差異節點,並完成一致性修復。
注:通過線上執行統計,通常僅有<0.5% 的檢測結果出現數據不一致現象。利用 merkle Tree 的特點有效優化了資料對比及修復過程中對網路頻寬不必要的消耗:
大於 99.5% 一致性檢測僅需對根節點資料對比即可確認資料一致性。
支援逐層節點對比,可準確定位不一致節點,並針對該節點進行資料修復。
高可用部署:
服務的高可用通常與部署方式必密不可分,vintage 作為公司級別的註冊中心本身必須具備極高的可用性和故障恢復能力。
vintage 在部署上主要考慮以下 4 點:
適度冗餘度部署:冗餘度方面,vintage 服務採用 2 倍冗餘部署,冗餘的節點在提升註冊中心自身可用性的同時,也為業務系統在突發峰值流量時的快速擴容反應提供了良好的彈性空間。
與網路消耗型業務隔離:作為頻寬敏感型服務,vintage 在機器部署時會與網路消耗類業務儘量隔離
多機架部署,同機架內多服務部署:考慮到網路分割槽和裝置故障的因素,vintage 採用多機架部署方式,同機架內通常部署至少兩個服務節點。
多 IDC 間資料互備:vintage 天然實現了 IDC 間的資料互備,同時通過冗餘一套 IDC 子叢集用於全叢集的資料災備。極端情況時可在分鐘級實現 IDC 維度的任意子叢集搭建,並通過系統內部介面優先從資料災備的子叢集中完成資料恢復。
2.5 高效能:
在介紹 vintage 高可用後,再來看看 vintage 系統在高效能方面如何實現 10w+ 節點的支撐及平均百毫秒級別的通知延遲。
List+Map 資料結構實現了支援 10w 節點高效能定時器。
watch 變更實時推送機制。
瞭解下微服務在 vintage 系統中的生命週期,這是一張 vintage 內部維護微服務生命週期的狀態機。其中 initial,working,unreachable 為 vintage 內部對管理微服務的三個狀態,分別用於說明節點處於初始註冊狀態,可用狀態,以及不可用狀態。
微服務節點通過呼叫 register 介面完成註冊,vintage 會將節點狀態設定為 initial。註冊成功後微服務節點會週期性(例:5s)向 vintage 傳送心跳,彙報自身健康狀態,並不斷更新自身心跳過期時間。vintage 在收到第一個心跳請求後,會將節點狀態變為 working。vintage 會定期對全部 working 狀態微服務節點進行健康檢測,當發現節點心跳超時,該節點狀態將被變更至 unreachable,並通過 watch 介面將變更時間推送給訂閱方。而 unreachable 狀態可通過再次傳送心跳,轉變為 working 狀態。在 working To unreachable 的變更過程中如果觸發了 vintage 節點狀態保護機制,或出現網路分割槽,狀態變更會被凍結。微服務節點下線時可通過呼叫 unregister 介面實現登出,完成整個生命週期。
vintage 每個 IDC 中僅由主節點負責全部的上行請求並通過心跳方式維護該 IDC 內全部微服務的健康檢測及狀態變更。而心跳作為週期性的高頻請求,當單 IDC 內節點數達到一定量級時,對主節點的處理能力帶來極大的挑戰。並直接影響到單 IDC 叢集的整體吞吐和服務節點承載能力。這就需要一個高效定時器來完成上述的挑戰。
從圖中可以看出,在設計上要求單 IDC 叢集同樣可承擔 10w 級的服務節點,支援頻繁的 expire 更新,同時要求對節點狀態變更精度達到 10ms 級別。
首先對現有的 heartbeat 彙報及超時檢測模型分析後,得出以下幾個特點:
heartbeat 彙報請求時間,為節點 expire 的續租起始時間。
由於 heartbeat 續租時間固定,節點過期時間可根據續租時間固定排序。
健康檢測與心跳獨立處理。採用週期性觸發機制實現對已排序資料 expire 的超時判斷。
通過對特點的分析,並參考了目前比較流行的一些定時器處理演算法。不難看出連結串列,最小堆以及時間輪這些主流定時器的時間複雜度上均會受到節點數的直接影響。那是否有一種資料結構可以做到 update 和 trigger 都是 o(1) 的時間複雜度呢?
定時器(timer)由 List+Map 共同組成用於維護 vintage 內部所有 working 狀態的微服務節點的健康監測和心跳續租。
List 是一個根據 expire 升序排列的有序雙向連結串列,連結串列中的元素為微服務節點的狀態物件,包括節點 ID 和超時時間。Map 儲存了微服務的 ID 及狀態物件的引用。當 vintage 收到某個節點的心跳請求,會根據節點 ID 從 Map 中獲取該微服務節點狀態物件,由於心跳續租時間固定,完成 expire 欄位更新後無需排序,可直接將節點物件插入在連結串列的尾部。
timer 會定期觸發微服務的超時檢測,根據連結串列 expire 升序的特點,每次檢測的順序都是由首都到尾部,發現首節點 expire 小於當前時間,觸發過期操作,將節點從列表中刪除,並呼叫 callback 函式,通知儲存模組將節點狀態更新為 unreachabl。依次向後檢查連結串列中的各節點,直到出現第一個未過期的節點為止。
當服務狀態變更,節點的註冊與登出時都會將資料記錄到儲存中。vintage 實現了一套基於樹形結構的多版本資料儲存。依賴於樹形儲存結構,vintage 在實現對資料儲存能力外,還可將微服務依照 /IDC/ 部門 / 業務線 / 服務 / 叢集 / 服務節點 的方式分層管理。同時藉助於多版本實現了從資料到事件的轉換,並通過 Event notify 模組將變更事件實時回撥通知 watch hub,完成對新增事件的訂閱推送。
watch 推送機制
client 註冊 watchhub
client 向 vintage 訂閱服務變化過程。會首先向 vintage 的 watch hub 進行註冊,watch hub 會為每個訂閱方生成一一對應的 watcher 物件,並根據訂閱目標的 path(ID),將相同路徑的 watcher 合併至一個 watchers 列表中,儲存在 watch hub 的 map 索引結構。
儲存層資料變更通知
當 storage 完成資料儲存後,會將資料變更的資訊轉換為新增事件通過 Event notify 模組回撥通知 watch hub。watch hub 通過該 event 中的 path 在 map 索引找到訂閱的 watchers 物件,並將事件寫入全部 watcher 的 event queue 中。由 watch 函式完成事實資料推送。
注:若 path 是多層結構時,watch hub 通過逆向遞迴的方式,將該事件依次插入多個 event 佇列,實現 watch hub 的 path 遞迴功能。
系統伸縮性
為保障微服務的快速擴容,vintage 服務自身必須具備快速的擴容能力 vintage 會通過 Docker 化部署及 node discovery 節點自動發現能力,實現秒級別的擴容。並通過整合 Jpool+Dcp 體系,目前可實現分鐘級 IDC 註冊中心搭建。
多語言支援
在多語言支援方面,vintage 系統通過 HTTP Restful API 滿足了公司級跨語言註冊中心的服務支援。同時也在不斷擴充套件多語言官方 SDK,目前已實現了 Java,Go 的支援。
三、總結
新版 vintage 系統已在線上運行了一年多,經歷了微博春晚保障,多個核心機房網路升級和數不勝數的突發熱點事件應對。
vintage 系統使用多 IDC 部署方式支援公司混合雲戰略,承擔十萬級別微服務註冊與服務發現。同時系統可用性達到 99.9999%,通知變更平均延遲< 200ms,p999 延遲低於 800ms。vintage 註冊中心在可用性和效能上,也滿足了業務在突發熱點時的應對保障,將常備業務機器的 buffer 由 40% 降低至 25%。
原文連結
https://mp.weixin.qq.com/s/ASjPkdcikmSBh5A2Iec7vQ