引言
房間系統是直播業務的“基石”,開播和看播兩大體系都是圍繞房間場景展開。
房間系統架構也經歷一系列的升級和挑戰,從房間讀多活、混沌流量治理、熱點發現、多級緩存等,支撐了S11破千萬PCU的流量洪峰沖擊。
為了應對業務更大的挑戰,基于CQRS思想,分離大流量的用戶高讀場景(Query)和注重數據強一致性的開播創建房間等寫場景(Command)。對于用戶端可以無狀態無限制的擴容服務副本,做到支持更大線上用戶同時在線的目標。
背景
直播業務的技術服務體系也實踐過從單體到微服務化的演進之路,以技術視角看微服務體現單一職責和關注分離的思想,從大單體應用的進程模塊拓展到分布式的應用服務模塊化。同時微服務也是有額外成本的,微服務的拆分思路不僅僅是技術層面,更多會取決于組織和團隊(以及組織如何去看待業務)。
如康威定律所說:
Organizations which design systems[…] are constrAIned to produce designs which are copies of the communication structures of these organizations.
設計系統的組織,其產生的設計和架構等價于組織間的溝通結構。
單體架構到微服務架構是這個定律很好的體現。直播業務從一開始劃分了三個大的macro service domain,分別是用戶、主播、營收。房間服務被劃分在了主播這個Domain內,因其是主播創建房間、開播推流的基礎載體,沒有直播內容供給整個直播業務都無從談起。
將單塊架構先解耦成三塊大的業務域,每個團隊開發,測試和發布自己負責的服務,互不干擾,系統效率得到提升,滿足了一個階段內業務快速發展對于技術的要求。
本文后續將主播域(關注內容生產)簡稱為B端,將用戶域(關注流量消費)簡稱為C端,方便理解。
現狀分析
大單體應用拆分之初,房間服務是從中拆出的單個php服務room-service,不過服務中仍然耦合了過多的業務邏輯,從業務重要性/讀寫輕重/前后臺區分/面向用戶區分等幾個角度上考慮都不應該繼續在這個服務中繼續迭代業務邏輯。
隨著B站Golang微服務化演進,從room服務中陸續拆分出了幾個新的微服務:
- xroom/daoanchor:房間主體服務
- xanchor:主播業務服務
- xroom-management:房間管控服務
- xroom-extend:房間擴展信息服務
從組織視角拆分是合理有效的,但是從技術視角去觀察,房間服務既需要滿足B端開播場景的數據強一致性要求,也需要承擔來自C端用戶“推薦頁”、“上下滑列表頁”、“進房”等業務場景的高QPS。
xroom作為底層服務,常態化晚高峰需要承擔35W+ QPS,單接口最大QPS 18W,大流量Query通過服務熱點主動探測+Local Cache+依賴緩存組件抗壓,redis主集群QPS達到百萬級別,盡量去減少DB層面回源的請求量級。
房間讀多活架構和多級緩存方案實施后,又需要一些措施能夠去主動探測發現偶發的數據不一致問題。
房間服務時常需要應對保障“關鍵事件開播管控”和“高熱賽事直播”兩種不同的業務場景,前者更關注房間開播平穩管控及時有效,后者關注高流量用戶進房,技術服務上的降級緩存/兜底邏輯/熔斷策略也會有差異。
If the same data model is not able to satisfy the read and write patterns of a system effectively, then it makes sense to decouple the two schemas by Applying CQRS.
如果同一個數據模型不能有效滿足系統的讀寫模式,那么通過應用CQRS來解耦這兩個架構是有意義的。
通過CQRS我們可以切實分離大流量的讀場景和注重實時性和一致性的多寫場景。尤其對于C端大流量來說,可以無狀態擴容服務節點。理論上可以達到無限擴展目標,這對于千萬在線的直播是尤其重要的。
The second scenario in which CQRS is helpful is in separating the read load from the write load.
第二種 CQRS 有用的情況是將讀取負載與寫入負載分開。
CQRS架構模式適用性
主播是房間的所有者,對房間有管理權力,能夠改變直播間的狀態與屬性。
觀看用戶則是房間內容的消費者,B和C視角下都會有一個叫做“房間”的內容承載體。從面向用戶和權責分離角度來說,CQRS是比較好的一種思想來指導房間服務體系和業務域的拆分演進。
拆分目的是減少B端和C端之間領域穿插,雙方更加聚焦各自的業務領域,最終閉環從而提升架構穩定性規避系統性風險,并提升各自業務域內的組織效率。
業務拆分共識
圍繞房間實體,業務上有生產和消費的邏輯需要盤點,從BFF/基礎房間服務/直播biz服務三層進行BC Domain的拆分。
- B/C兩端,都具有完整業務領域,領域內有各自相對獨立的業務上下文
- App客戶端/Web前端,屬于多個領域共用的“視窗”
- C端有兩路數據流,一路是看端領域閉環的數據流,第二路數據流是B端的數據流(開關播上下文、房間狀態變化上下文)
圖片
根據GRASP(General Responsibility Assignment Software Pattern)中的信息專家(Information Expert)模式,數據應該放在需要經常使用它的地方,同時某一個功能在哪里實現,取決于數據哪里。換句話講,數據+功能=領域。
架構演進目標
房間核心系統耦合了消費端和生產端的邏輯,基于CQRS理論需要將服務和數據庫完全解耦,承擔高流量的xroom/dananchor服務劃分為C端業務域服務,新的B端服務閉環接受內容生產的讀寫請求和后臺管控聚合請求(寫多讀少)。
B端核心房間數據變更通過領域事件消息通知給到C端,C端關注數據的最終一致性,期間會有數據對賬腳本主動發現數據不一致并自動修復。
總體方向按照C&B職能原則來拆分,過渡階段允許歷史請求寫請求C服務,C proxy to B service。
圖片
最終目標是完全解耦,通過領域事件數據流來同步必要信息。
圖片
沒有銀彈
我們享受到了CQRS帶來的便利,相反的也要解決引入它帶來的“副作用”,這些副作用在直播領域下,表現的最核心無疑是開關播狀態的延遲,但是由于用戶和主播的天然隔離,反而不需要兩邊完全實時。
主播開播后,需要推流等一些列的動作,直到用戶可以看到主播的直播畫面,這個過程中很自然,符合人的直覺,而在架構層面我們通過引入消息中間件來同步數據,本身耗時在毫秒級別,這相比前面的自然過程幾乎可以忽略,但是我們的技術架構上服務和數據完全拆開了。
同時因為引入了更多的數據交互環節,請求拓撲變得更加復雜,每個環節的數據正確性排查變得更困難。我們通過平臺提供的Tracing+Metrics+Logging來進行問題輔助定位,雙寫+對賬腳本保障過渡階段數據最終一致性,灰度階段控制讀寫流量各自單獨放量驗證。
為了應對CQRS架構帶來的復雜性確實需要額外引入數據服務腳本等方式去做保障,這部分的思路更偏向于架構設計中的“風險驅動”。
執行落地過程
對于當前比較成熟的業務系統去做拆分,是一件比較有挑戰的事情。我們先從橫縱兩個角度看下房間服務所在的層級位置。
橫向技術架構分層
- 面向用戶的終端設備:App粉版、Web端、開播App等
- CDN -> SLB -> APIGW:內容分發邊緣加速,LB層與統一網關
- BFF:Backend for Frontend,根據終端渠道區分的業務網關入口,eg:app-room / web-room / app-interface
- Biz Service:業務邏輯服務
- Domain Service / Fundamental Service:業務域服務/基礎服務,eg:Room Domain Service / Account Service
縱向部署隔離
- Region:eg sh/bj
- Zone:eg sh001/sh002/sh003,每個Zone單元內的流量應盡可能閉環(讀多活寫回源 -> 讀寫多活、BFF failback cross Zone可選策略)
- Cluster/Group:group1/group2/染色group,不同group可以設置服務發線上的weight權重
- AppID:每個應用的服務發現naming id
圖片
可以看到房間服務有眾多的請求上游,必須在讀寫切分過程中,保障好數據的一致性(B端業務域內強一致性,C端業務域內最終一致性)和服務的可用性(底層服務抖動會有放大效應)。
當然,上游業務服務fanout過多讀流量到下游服務也是需要治理的,這在另一個議題中去開展了。
在具體的實現過程中,我們將整個拆分劃分成三大階段。
圖片
- 數據對齊階段
本階段目標是把B端的數據庫從C端復制,并且保持數據一致。并且此階段可以拆分成增量對齊階段和存量對齊階段。
增量對齊,將新數據的創建和更新通過雙寫同步。
存量對齊,通過同步JOB將C端DB的存量數據同步到B端新DB,并且需要一種對賬系統去針對全量數據進行周期性的對比,來確保數據一致性。
- 數據同步階段
針對數據一致性問題和業務上數據實時性問題,需要對相應的實時場景進行改造。B端構建新的房間領域事件消息,并同步到C端。
此階段完成C&B分寫邏輯,通過在DAO層控制BC表級和字段級的寫入控制,將寫操作分流到B和C的各自服務內,并且通過消息事件,來同步數據變更。此階段完成了數據拆分和同步的目標。
If the choice is made to keep the updates asynchronous, the entire system is forced to deal with the fallout of eventual consistency.
如果選擇保持更新為異步的,整個系統將不得不處理最終一致性所帶來的后果。
- 最終閉環階段
此階段作為收尾,我們將上游的調用梳理出來,并且改造讀取各自領域真正的依賴服務,最終達到完全解耦的目標。
核心設計
數據拆分 Data Division
當前直播的核心實體數據庫,BC屬于公用的狀態,每張表和每個字端按照上述的原則是可以劃分出BC屬性的。所以我們一步到位,顆粒度到最小單位字段級別,確保完全解耦。
BC拆分后短時間內兩套獨立數據庫的表字段可以保持相同,長期根據業務迭代節奏不同,兩邊可以變為異構shcema模式。同時要注意的是,業務數據層面切分后,需要聯動大數據層面同步hive表的變更,單獨重新建模或數據任務換源,這點是比較容易遺漏的。
領域事件驅動 Domain Event-Driven
BC房間業務各自劃分業務域邊界后,域之間的數據同步應通過領域事件驅動+觀察者模式去實現;域內的核心業務邏輯,可以走應用服務編排。
本次實踐過程中重新梳理制定了“房間狀態變更”事件,basic room info + extra info,滿足訂閱者服務對房間業務域核心字段的要求。
跨微服務的事件機制要總體考慮事件構建、發布和訂閱、事件數據持久化、消息中間件,甚至事件數據持久化時還可能需要考慮引入分布式事務機制等。完整實踐下來還是成本還是比較高的,實施者應考慮結合業務場景和對數據的實時性/一致性要求來決定實踐到哪一步。
Tip1:訂閱者需要實現消費冪等,業務場景如果有訴求需要額外實現數據版本協議(eg:稿件系統BC CQRS拆分,B端稿件數據被重新編輯審核,C端已開放的版本仍然可以瀏覽,即使用了數據版本協議字段)。
Tip2:如果訂閱者有實現接收Message后反向callback query的模式(更適合去保障最終一致性),需要關注query的數據源是來自主庫or從庫,不然會有因主從同步時延導致的數據不一致case。
Tip3:絕對不要將核心DB的binlog消息暴露為Domain Message,一是暴露了過多細節字段下游并不一定都需要訂閱關心,要做很多filter邏輯,二是核心DB的字段變更將需要牽動所有下游,不利于變更。
灰度控制 Gray Scale Control
核心服務變更依賴一個比較完備的灰度發布方案,基于分布式KV組件(服務可以近實時地獲取到KV系統中配置的開關變更)我們設計了從BFF網關到服務的開關,來控制字段的外顯和關鍵Topic發送。
灰度策略有:功能總體關閉、白名單模式放量、百分位/千分位放量、功能全量打開,服務發布觀察遵從這個流程,從APIGW+服務染色發布引入流量+功能KV開關做到謹慎放量。
可觀測性建設 Observability Construction
新的架構落地只是起點,真正的考驗剛剛開始。我們必須為架構的穩定性,可用性負責。
保障整個CQRS系統,需要“配套設施”,其中的首要利器就是做可觀測性建設(Observability Construction),基于現有的基架能力,我們搭建了直播CQRS監控大盤,從生產方到消費方,全鏈路監控核心指標。
其中CQRS中的數據同步的相關指標,在數據保證數據最終一致性的背景下,尤其重要。整個實時性由三個部分組成,pub時間,網絡傳輸耗時,sub處理數據,其中在我們的CQRS大盤中,就包含B端業務pub的時間監控,和C端sub業務處理的時間監控,目前網絡傳輸耗時在毫秒級別,并且這塊指標也已經在灰度階段。
圖片
系統魯棒性 System Robustness
CQRS的引入幫我們解耦了截然不同兩種場景的系統,但是也確實引入了mq,從全局視角看又增加了一個依賴,所以系統的復雜度是增加的。為了增強系統架構的魯棒性,我們考慮到引入另外一種備選手段來做數據同步,通過直連服務接口調用的方式,這塊我們使用了我站自研的railgun消息處理組件。當兩種本身可用性就很高的方法互為補充時,那么出現問題的可能,相當于兩個系統同時出問題的概率,這種概率是極低的。
在整個CQRS數據鏈路上,我們還針對一些寫場景做了異步重試來系統自愈,抵抗服務可用性的長尾不可用,另外我們也考慮到異常場景下,雖然降級到http調用同步數據,但是存量消息恢復時,數據不是最新的,所以加入過時消息走回源,保障數據正確性的設計,來盡可能讓系統在各個環節的抗風險能力提升。
數據對賬腳本 Data Verify Job
有一種比較常見的方式,即流式對賬,依靠我們數據流監控組件去實現,在設定一個經驗值的時間窗口閾值內,對兩邊數據源的流式binlog做對比。這種對賬方式比較適合終態業務對賬,而我們實時直播屬于反復跳變場景,目前我們利用最簡單有效的方式,連接雙方從庫,以B端庫為準進行數據對賬,并且滿足30s內數據一致比較,來兼容數據最終一致性,當對賬腳本發現不一致后,通過日志+主動告警+機器人等手段,配合自動化修復任務做自愈的設計,從而cover住大多數異常case,做到平常0職守。
線上事故響應SOP Incident Response SOP
上文的系統魯棒性設計,最大程度保障服務的健壯穩定,以及上文兜底的數據對賬機制,最大程度客觀地幫助系統發現異常,而線上永遠有我們意想不到的情況,所以我們設計了一套線上事故響應機制,來應對“意外”。
首先我們從CQRS和BC服務的角度,預設配置了不同領域的關鍵日志或者指標告警,而且劃分了不同的緊急程度。二是我們提前管理規劃了告警組成員,覆蓋兩邊領域的一線研發,并且配置不同的通知渠道,可以讓最合適的同學最快地感知異常。三是我們從不同角度預設了我們可以枚舉異常現象,再去枚舉不同現象發生的根因,再輸出可以解決的方案list,所以基于這套sop,配合我站alchemy平臺tracing鏈路追蹤能力可以迅速定位故障點,以最快速度執行預設標準步驟,達到最快恢復可用性的目的。
生產配套 Production Support
一個安全的生產系統是需要一整套的“生產配套”體系,可以快速定位排障。這塊我們借鑒了很多類似系統,參考了醫院體系的”問診臺“,目前發育出開播互動問診臺生產配套,提升問題排障效率幾乎80%。
圖片
技術項目管理
最后想聊聊技術項目的價值和實施周期。技術項目有些時候由于不會帶來明顯的業務增量價值,往往會被質問“為什么要做如此變更,不做這個變更業務難道不能用嗎?”諸如此類的靈魂拷問。
每個階段技術建設需要有一條經過設計的baseline,這條線應該略快于業務發展的基線一步。建設落后,技術跟不上業務,如同沙地之上建高樓,業務連續性會受到技術系統穩定性可用性的lost而直接受損。
建設過快,又有Over Design/Over Engineering的問題,所以略快過一步是合適的,保留了彈性擴展的余地,可以在需要時適配業務快速調整。
架構師和Tech Leader需要協同階段性review當前技術建設baseline和業務的適配情況,并決定是否投入有效資源進行技術架構迭代。
技術項目從立項之日起,就需要更嚴格于業務項目的管理機制。業務項目的業務目標(試錯/AB實驗/明確性收益延展)往往不由工程師來制定,而技術項目的目標感也是需要從開始就建立起來的,這有助于關鍵行為路徑拆解,并在項目收尾階段進行目標&結果比對。
技術項目要有階段性Milestone管理,技術立項 -> (原型方案討論) -> 技術方案確定 -> 技術實施(大項目應分階段實施,過程指標也被Track) -> 測試/驗證方案(測試用例收集&review) -> 發布方案 -> 線上驗收方案 -> (線上問題處理預案) -> 項目結果復盤。
立項、技術方案確認等階段需要有正式官宣(儀式感/目標感/參與感),避免流于私下的技術Topic探討,導致無法被正式地投入資源到實施階段。
End
最后,該項目實踐上仍然有諸多細節無法在文章中一一展示,文本在撰寫過程中也難免犯錯,希望大家可以指正,歡迎大家可以一起來進行技術交流。
參考
- https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs
- https://kislayverma.com/software-architecture/architecture-pattern-cqrs/?fileGuid=0IWvR8dLbi0m7fi4
- https://en.wikipedia.org/wiki/GRASP_(object-oriented_design)