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