B站服務穩定性建設:高可用架構與多活治理
一、高可用多活架構
相較于傳統的災備單活的架構,多活指的是在同城或異地的一個數據中心建立一套與本地生產系統部分或完全對應的一套服務,再進行流量調度,使所有可用區的一個應用同時對外提供服務。
災難發生時,借助多活的業務快速實現流量切換,可以避免或大幅降低用戶受到故障的影響。較為典型的兩種方案類型主要包括同城多活和異地多活。
1.高可用整體架構
2.多活的方案類型
我們使用螞蟻的CRG多活定義類型,CRG分別代表GZone 、RZone、CZone的三種模式。
- GZone模式
用戶間的數據可以共享,比如B站的視頻播放、番劇播放、稿件信息直播間等數據偏向于平臺側,這類業務場景都可以做成GZone模式。
- RZone模式
單元化模式,它更適用于用戶側的流水型數據,比如社區的評論、彈幕、動態及支付,比較適合做成Rzone模式。
- CZone模式
介于GZone和 RZone之間,做用戶間的數據共享,但它支持在當地的多可用區內可讀可寫,并且能夠接受一定的數據延遲和不一致性。
B站具有多個數據中心,但整體定位比較混亂,所以我們在多活場景里重新做了梳理和劃分。
以上海的機房為例,我們有4個機房,整體分為了2個可用區,用來做我們GZone的服務部署。因為兩個可用區都在上海,一般延時情況都在一毫秒左右,所以在同城雙活方面無需擔心出現網絡延時問題。這也是目前主要在推進的一個業務改造覆蓋的方案。
另外,我們還在江蘇設置機房,作為 RZone的服務部署,但由于距離上海較近,無法模擬出遠端異地高延遲的情況,同時容災能力存在不足。因此,目前我們仍在驗證在華南或是在一些L2、L3的邊緣機房進行的異地多活的方案,后文將詳細介紹。
1)同城多活
同城多活是指我們通過流量調度,在應用多數據中心的同時,對外提供服務。同城多活的機房距離較近,所以耗時一般會控制在5毫秒以內,在數據層面具備主從同步和切換的能力。因為同城多活的網絡延時比較低,所以在業務改造過渡的階段,服務的調用不會增加過多耗時。
2)異地多活
異地多活也是通過流量調度,同時對外提供多數據中心的服務。
如果是距離比較遠的兩個機房,那么全程耗時可能會達到30毫秒以上。這方面很難使用單集群的模式來提供服務,而多集群的模式下,又會因數據一致性的復雜度造成問題。
通常有兩種解決方案,一種是要求業務進行單元化的改造,它需要具備單元數據的分片能力,而單元間需要雙向同步數據;另一種是面向一些可以接受延遲的業務,它們在主可用區可寫可讀,然后在異地建立一個只讀的單元,支持讀的流量分流。
3) B站的同城多活
因為很多數據都偏向于用戶間共享,所以B站的很多業務適合做成同城多活。下圖為B站同城多活的架構圖,由一個流量層即DCDN層面,對用戶維度的一些信息來進行Hash路由,然后進行請求分發。
目前,B站主要是基于向用戶的一個MID或者是用戶的一些設備ID來做路由,分發到不同的可用區,不同可用區的用戶能夠訪問在該可用區的緩存,比如KV存儲和DB存儲。這兩個存儲通過Proxy模式訪問,可以就近讀本機房,寫可回源到上海可用區一。另外,這套方案中又加了一個Invoker組件,進行多活的全局管控。
①流量接入層
我們通常將流量的管控面分為南北向,即從用戶側到我們內部服務的方向。另一方向是東西向,一般指內網的服務間調用的流量。其中南北向這個部分就是由我們的DCDN、SLB,也就是7層負載,還有API網關。
在實際的架構中,用戶從端上過來訪問,會基于DNS或是HTTP DNS來訪問我們的DCDN的節點,在DCDN回源時會有POP點來做流量的匯聚,再進入到我們的可用區。我們的DCDN基于邊緣CDN節點來做整體的路由管控,實現動態的最佳選路。
我們自研了一個Picker模塊,可支持用戶的一個MID或者是設備ID Hash到不同的可用區,同時支持用戶流量權重在多機房進行全動態和靈活的調整,99:1或日常的50:50都可以靈活定義。至于7層負載SLB,它能通過服務發現多可用區的下游服務和APIGW的節點,支持API的降級和全局限流的能力。
APIGW本身是一個容器部署的服務,和我們內部其他BFF層一樣,支持發現多可用區的服務節點,所以我們將其放在了南北向和東西向流量銜接的這一層,使它支持單可用區服務的故障重試。目前我們也逐步將SLB側的管控能力轉移到GW層面統一進行管理,它可以支持服務的API降級、熔斷和限流,包括我們和客戶端聯動的流控能力,避免服務故障時客戶端瘋狂重試導致進一步壓垮服務。
APIGW也部署于PaaS平臺,支持HPA彈性擴容,可以應對一些流量突增的情況。
Discovery是我們的服務發現組件,在服務啟動后,它會向注冊中心進行注冊,并且定期更新Review,同時對服務下游依賴的信息如HTTP或者是GRPC的地址端口,及其可用區的信息進行拉取和緩存。
在調用下游時,客戶端即SDK層面會默認選取同可用區的一個節點。若下游在該可用區未做部署,比如一些非多活的業務,則會回到主可用區調用。所以在同城多活的場景下,要求服務能夠在本地完整地支持寫請求的處理,以及強弱依賴必須在本地進行部署,而弱依賴則會被允許應用跨專線回主機房。另外,Discovery也能對東西向的流量權重進行管理,包括一些灰度驗證等。
②緩存一致性
為避免業務直連緩存實例,緩存接入方面我們提供統一的Proxy。SDK 在各開發語言上并不統一,所以可能會引發短連接的風暴,并引起性能下降。我們通過用Proxy來長連接緩存實例,Proxy支持 Sidecar和ProxylessSDK等模式提供接入。B站緩存的使用場景不支持多機房的同步,在多活場景下要求服務訂閱同機房的一個數據源,對緩存進行數據更新或者刪除,維護緩存數據最終的一致性。
- 保證緩存一致性的處理方式
Cache Aside的模式即DB/KV的存儲加上緩存,因為數據最終都會落到存儲上,所以這類情況下業務只需要通過Canal訂閱同可用區存儲Binlog變更然后投遞消息隊列,由業務的的Job解析處理后刪除或更新緩存。
- 純緩存場景
一類是熱數據的情況,業務在多可用區可能通過Job定時刷新,把數據從DB或者是其他的離線數據源中拉取,隨后存放到緩存中。在多活情況下,這類場景要求獨立部署,再通過job做定時更新,目的是使業務和緩存在單個可用區內實現閉環的調用。
另一類就是將緩存當做存儲的用法,這個用法一般不推薦,出于性能考慮,B站不支持做持久化的緩存場景。若作為存儲使用,即使是Cache Aside這樣的模式,在遇到一些較大規模的故障時,仍舊會出現數據丟失的情況。
所以在這方面建議業務改造為Cache Aside的模式,或者是通過KV進行存儲。KV是B站自研的分布式KV存儲系統,本身的數據存儲在SSD中,所以它的性能必然不如Redis Cluster內存的性能有優勢。但我們做過評估,當業務的QPS小于10萬,基本上可以遷移到我們的KV存儲系統內。KV存儲也支持Redis協議的一些常用命令和操作,它的最大特性是支持機房的多活。
- 分布式鎖場景
例如涉及一些分布式鎖或者說處理冪等,這類情況就建議把業務改造為KV。因為KV本身支持如Cache這樣的命令,并且它的數據持久化,同時它也支持跨可用區同步,與多活場景比較契合。
③消息多活
我們提供了三種模式以根據不同場景進行選擇:
- 各可用區的消息自產自銷
這個模式下Topic間不會進行消息同步,需要生產端投遞本可用區的 Topic,再由本可用區的下游直接進行消費。這種模式比較適用于 Service至Job異步消息的場景,或者是ServiceA投遞給已多活的下游ServiceB這樣的情況。
- 多可用區全量消費(Global)
這個模式下Topic間會通過Sync的一個組件雙向同步消息,每一個topic中有兩個可用區,即可用區一+可用區二的全量消息,然后同步全量消息。它適用于ServiceA投遞給未多活下游ServiceB的場景,比如離線或大數據,下游一般要繼續在可用區一消費全量數據。
- 全量消費(Global)自定義不消費可用區
從Global模式衍生出來的一種模式,允許選擇任意一個可能區不消費,比較適用于消息由解析Binlog觸發全量數據的情況。在可用區二的下游,需要考慮消費后的冪等處理,包括一些存儲或者下游的調用放大的情況下,可選擇其中一個可用區不消費。這個模式的好處是,出現可用區故障時,可通過切換消費模式快速恢復整個消息層的消費。
這三種模式的設置和Topic間消息同步的開啟,不會做任何綁定。在多活過程中,我們和業務共同做場景梳理,包括梳理明晰涉及到哪些消息隊列,哪些相關下游在確定整個消費模式的制定等。
④數據訪問/存儲層
存儲層同樣由我們的中間件支持,我們提供了MySQL,TiDB以及Taishan(KV)三種Proxy,整個機制沒有區別,它具備多可用區路由的能力,并且能夠具備實例拓撲的自動發現和動態切換能力。
- GZone:對于同城雙活場景下數據需要全局存放的情況,即一主多從這樣的模式,服務在主可用區基本上能讀寫GZone的存儲,本可用區有可讀的從實例,在可用區也有從實例,通過Proxy將寫的路由回源到主機房。而在一些強一致的場景下,也提供了SQL Hint級別的配置或在連接串請求頭中增加一些master或者PRIMARY的配置,從而實現強制讀主的場景。
- RZone:在RZone單元化這一方面,業務要做數據分片,分片的數據需要完成雙向同步,本可用區的一個分片能夠實現讀寫操作的封閉。
- DTS同步組件:可以實現數據的雙向復制,目前整體延遲小于10秒,同時支持數據沖突檢測,發送沖突時支持暫停同步或者說異步把通知投遞到消息隊列,再由業務來處理沖突。
二、業務多活演進
1.多活業務劃分
在多活架構中通常會按業務域進行多活業務劃分,面向C端還是B端分別是兩種不同的業務形態。
2.B站的業務同城多活改造
- 單活:B站大部分業務最初也是單活模式,即服務只在單個可用區做部署,缺乏Proxy支持,緩存和DB大部分都是客戶端直連。業務發布接口需要在SLB和CDN上分別做配置,并進行規則發布。
- 讀多活:它是一個過渡方案。雖然我們的服務開始在另一個可用區做整個部署,但在流量層面,我們只能支持讀接口的接流,而且接口大部分都通過 CDN側或者SLB側進行流量的轉發,還有一些緩存或消息隊列的一些組件未完成多活改造,存在跨機房調用的情況。
- 同城多活:我們提供了各類組件的 Proxy接入支持,使業務在可用區二能支持處理寫的請求,并借助Proxy支持整個容災的自動切換。緩存的一致性也是這個方案里的一個重點,要求業務必須通過Canal+Job這樣的方式維護緩存的一致性,包括消息的生產消費都達成了可用區內的閉環。
至于GZS的組件,則由Invoker平臺和APIGW對服務接口的發布進行統一的管理。
我們認為,現階段去做單元化改造成本較高,收益可能并不大。所以基于同城多活的方案,衍生出異地多活的架構,目前我們正在驗證該異地多活方案。
華南可用區是我們相對遠端的一個可用區,用來承接一部分讀的流量,它整體的架構是對標可用區二讀多活的模式。在服務發現層面依舊通過Discovery的組建實現當前可用區的調用,核心依賴在華南可用區完成整個部署,一些弱依賴則可返回上海可用區做調用。
在數據存儲同步方面,原來的兩個上海可用區距離不遠,我們使用的基本是一些原生的同步組件,它本身也能滿足單向同步。實現遠端后,要繼續滿足DTS的單向同步能力。而緩存和消息隊列,則繼續遵循最終一致以及自產自銷的原則來實施。
三、多活管控與治理
1.多活元信息規則治理
我們初期在CDN上的一些規則偏向非標,有大量的正則寫法,所以我們在做第一步時就對多活元信息的規則進行了治理,APIGW接入時也應用了前綴路由的模式,以方便做后續的統一切流管理。另外,也保留了一部分非標的多活規則,能夠提供自定義的規則錄入,例如前端或Web端的規則。回源或緩存的規則等非多活規則就繼續存放于CDN層面。
2.Invoker平臺多活&強依賴降級&演練
Invoker平臺也有一些依賴,我們將其中的業務資源元信息存放在CMDB中,還有一些存放登錄態、鑒權和工單審批的系統。我們在建設Invoker的同時,對這個平臺做了GZone模式的部署。我們對它的核心依賴都做了故障演練,對每一個依賴也做了降級方案。之前也遇到過管理后臺在故障時無法登錄的情況,所以留了超管權限,并做了異地部署,以保證Invoker平臺在故障時可用。
3.多活流量管控
在多活編排接入流程化這一方面,基于跟業務做改造和做接入的經驗,總結了一些方法論,完成了平臺化和功能化。
1)編排、預檢與觀測
做業務接入時,首先要對多活業務進行定義,由此平臺側能讓我們基于CMDB選擇業務,定義它的多活類型,從而編排整個接入層的切量規則和數據層的切量規則。目前我們支持消息層的切換和東西流量的編排。在進行日常的切量演練或故障演練前,我們會做前置的檢查,例如容量巡檢、sos層面的監控、數據庫的連接池、業務在SLB平臺的限流配置等,要提前檢查其狀態,并預檢DB和KV主從同步的延遲情況。
2)切量
在切量的過程中,我們會觀測業務多活流量的變化與新引入的SLO體系的相關指標。在這個平臺做了集成后,最后一個環節則是將多活驗證的思路落實到平臺,例如要求多活流量在單可用區內做到閉環,而針對一些弱依賴則要求業務去做故障演練。
4.多活定義編排
多活定義編排是指,能夠選擇一個業務去定義它的多活模式,確定它是CZone、GZone還是RZone的方式,確定它的服務具體分布的地域位置和可用區。
在接入層,除了有APIGW比較標準的一些前置規則,也支持自定義規則的錄入,以及它在DCDN層面流量調度的比例。
我們在數據層面支持Taishan(KV)和DB的編排錄入,包括下游的消費任務,如Canal或 KV的同步任務這一部分的切換。
上圖是執行切量過程的界面,在切量申請時會選擇一個業務,然后選擇它的一個切流緯度,包括它要求的切流比例,選擇是否同時去切換我們的存儲,執行切流是切哪些規則,對切量對象選擇進行配置。
5.切流預檢與可視化
以往由人工完成的這一流程,如今被整合到平臺中,支持容量延遲和限流的預檢。在切流過程中,我們能觀測服務、緩存和DB等容量情況。
目前在做的一些接入如SLO觀測則包括鏈路的依賴、整體鏈路上下游的SLO情況等,我們也在做平臺側的接入。
6.多活有效性驗證
1)依賴展示
同城多活方案強調在機房內能夠實現讀寫流量的處理,以便在故障時快速恢復。因此在有效性驗證方面,比較注重依賴的排查。我們能在平臺側展示依賴,同時能展示服務的下游依賴和組件依賴,這方面用到了Trace的能力。
2)依賴檢查
針對需要確定哪些依賴是強依賴,哪些依賴是弱依賴的類似情況,我們會對依賴進行檢查和打標。
3)流量閉環
打標后,會進行流量閉環的檢查,通過打標和依賴發現這些信息,然后進行跨可用區調用的發現,直到確認核心依賴在可用區內是閉環的,弱依賴則要求業務排期做演練。
4)故障演練
這部分由框架SDK支持,它能夠實現依賴的自動發現和自動的故障演練,最終會輸出一份報告,確認是否都符合預期。若與預期不符,再進行改造和演練。
Q&A
Q1:同城雙活架構下應用層做了雙多活,基礎架構層還有必要雙活嗎?
A1:我覺得有必要。所有的技術組件在設計時就要考慮雙活模式,因為業務的多活基于組建本身的高可用,如之前介紹的Invoker組件的多活,它對于鑒權工單審批的依賴,我們都需要去考慮它的多活設計,以及在真正出現單可用區故障的時刻,我如何能登錄這個平臺去實現多活管控。
Q2:多活如何保證數據一致性?
A2:這還需要根據數據中心的分布和業務形態來進行方案的選擇。若是同城,只要考慮寫主庫讀從庫的模式,強一致性的需要強制讀寫主庫。若是比較遠距離異地多活,需要進行數據分片、單元化雙寫的改造,并且能夠接受部分數據的同步延遲,因為跨地域的耗時增加屬于物理層面,無法避免。只能根據一個合適的業務場景,適配相應的多活方案。關于緩存數據,前面也介紹了緩存一致性的幾種維護方式。
Q3:高可用多活的成本如何把控,過程中對ROI的考慮是怎樣的?
A3:要從以下兩個方面分析:一是收益。發生故障時,就會感知到多活的收益是有價值的。B站在21年經歷過一次比較大的故障,當時恢復最快的原因是已經做了多活的業務,哪怕是只做了讀多活的一些業務,也恢復得很快。可能因為B站讀場景比較多,所以對用戶感知層面讀場景的快速恢復能大大緩解用戶側的焦慮和客戶投訴的情況;
二是成本。一是注意資源增長,因為現在做的都是同城多活模式,同城的機房服務都是在線提供,所以它不存在資源的空好浪費。日常我們也會準備一些資源冗余來應對突發情況和高峰期,即換個思路,可以通過資源的彈性或混部來進行運維成本的增加。如Invoker平臺,它就是大大降低運維成本增加的工具,它能把一些人工完成的事情都轉化為平臺的功能層面,出現故障時能夠一鍵執行切流。所以要把收益和成本兩件事結合考慮。