本文由B站技術團隊比奇堡、Xd、三木森分享,有修訂和重新排版。
1、引言
本文要分享的是B站IM消息系統的新架構升級實踐總結,內容包括原架構的問題分析,新架構的整體設計以及具體的升級實現等。
B站技術團隊的其它技術文章:B站千萬級長連接實時消息系統的架構設計與實踐B站實時視頻直播技術實踐和音視頻知識入門B站基於微服務的API網關從0到1的演進之路
2、消息系統業務解讀
按業務全域現狀,在服務端角度分成客服系統、系統通知、互動通知和私信4個業務線,每個業務線內按現狀標識了服務分層。私信內分為用户單聊、bToC的批量私信、羣聊和應援團小助手四類,這四類細分私信沒有技術解耦,單聊和批量私信比較接近系統天花板。
私信單聊發送到觸達的pv轉化和uv轉化不足10%,有明顯通過業務優化提升觸達率的潛力。
3、消息系統中的私信業務
私信域內的幾個概念解釋:1)會話列表:按聊天人排序的列表。即B站首頁右上角信封一跳後看到的歷史聊天人列表,以及點擊未關注人等摺疊會話看到的同屬一類的聊天人列表。傳達對方賬號、最新私信和未讀數的信息。點擊一個會話後看到的是對聊歷史,也稱會話歷史。2)會話詳情:描述和一個聊天人會話狀態的原子概念,包括接收人uid、發送人uid、未讀數、會話狀態、會話排序位置等。3)會話歷史:按時間線對發送內容排序的列表。一份單聊會話歷史既屬於自己,也屬於另一個和自己的聊天的人。羣聊的會話歷史屬於該羣,不屬於某個成員。會話歷史是收件箱和消息內容合併後的結果。4)收件箱:將一次發送的時序位置映射到發送內容唯一id的kv存儲,可以讓服務端按時間序讀取一批發送內容唯一id。5)私信內容:一個包括髮送內容唯一id、原始輸入內容、消息狀態的原子概念。批量私信把同一個發送內容唯一id寫入每個收信人的收件箱裏。6)timeline模型:時間軸的抽象模型,模型包括消息體、已讀位點、最大位點、生產者、消費者等基本模塊,可以用於基於時間軸的數據同步、存儲和索引。私信涉及timeline模型的包括會話列表和會話歷史。7)讀擴散:pull模式。羣聊每條私信只往羣收件箱寫一次,讓成百上千的羣成員在自己的設備都看到,是典型的讀擴散。8)寫擴散:push模式。單聊每條私信既更新接收人會話也更新發送人會話,是輕微的寫擴散,無系統壓力。羣聊有另一個不一樣的特點,就是當羣成員發送消息後,需要通過長鏈接通知其他羣成員的在線設備,以及發送人其他的在線設備,這是一個寫擴散的技術模型,但是這個寫擴散是通知後即時銷燬的,並且具有過期時間,所以僅臨時佔用資源,並不對存儲造成壓力,且能有較好的併發量。私信核心概念關係表達:
4、消息系統問題1:會話慢
查詢當會話緩存過期時,Mysql是唯一回源,Mysql能承載的瞬時QPS受當時應用總連接數和sql平均響應速度的影響,連接數打滿時會給前端返回空會話列表。雖然可以增加POD數量、增大akso proxy連接數、優化sql和索引來作為短線方案,來提升瞬時請求Mysql容量,但是這種短線方案無法加快單次響應速度,mysql響應越來越慢的的問題依然在。另外增加POD數量也會降低發版速度。
會話Mysql使用用户uid%1000/100分庫,用户uid%100分表,table總量是1000。單表會話量在1kw-3.2kw。單個大up的會話積累了10W條以上,會話量最大的用户有0.2億條會話。單個Up的會話會落到一張表中,每張表都有比較嚴重的數據傾斜。如果考慮增加分庫分表的方案,sql查找條件依然需要用户uid,所以相當於傾斜數據要轉移到新的單表,問題沒有解決。另外,重新分庫分表過程中新舊table增量同步和遷移業務讀寫流量的複雜度也很大,有比較大的業務風險。Mysql的規格是48C 128G和32C 64G。由於會話數據量大,Mysql buffer_pool有限,數據比較容易從內存淘汰,然後mysql需要進行磁盤掃描並將需要的數據加載到內存進行運算,加之比較多的磁盤掃描數據,這時的響應一般在秒級別,接口會給前端返回超時錯誤,會話列表頁空白。為了適配業務發展,Mysql 會話表 已經添加了9個非聚集索引,如果通過增加索引使用業務需要,需要更大的Mysql資源,且解決不了冷數據慢查詢的問題。增加更多索引也會讓Mysql寫入更慢。
5、消息系統問題2:私信內容單表空間和寫性能接近天花板
每條私信內容都綁定私信自己的發號器生成的msgkey,即私信內容唯一id,該msgkey包含私信發送時的時間戳(消息ID生成可參閲讀《微信的海量IM聊天消息序列號生成實踐》)。讀寫私信內容Mysql之前先從msgkey解析出時間,用這個時間路由分庫分表。私信內容庫按季度分庫,分庫內按月度分表,單表數據量數億,數據量最大的用户日增私信351.9W條。按照曲率預測,25年全年數據量有近百億,如果繼續按照月度分表,分表規則不適應增長。當前該Mysql最大寫qps 790,特別活動時寫qps峯值預計是20k,但是為了保障Mysql服務整體的可靠,單庫寫流量我們需要控制在3000qps以下,無法滿足寫入量峯值時的需要。
此外,消息內容表結構包含了羣聊、單聊和應援團小助手全部的屬性,增加業務使用難度。絕大部分私信內容是單聊的。
6、消息系統問題3:服務端代碼耦合
B站的四類私信包括:1)單聊;2)羣聊;3)B端批量私信;4)應援團小助手。這些私信都需要實現發送和觸達兩條核心鏈路,四種私信核心鏈路的代碼邏輯和存儲耦合在一起,代碼複雜度隨着業務功能上線而不斷增加,熵增需要得到控制。從微服務這方面來説,實例和存儲耦合會帶來資源隨機競爭,當一方流量上漲,可能給對方的業務性能帶來不必要的影響,也會帶來不必要的變更傳導。
7、消息系統新架構的升級路徑
基於對私信現狀的論述,可以確定我們要優化的是一個數據密集型 >> 計算密集型,讀多寫少(首頁未讀數)、讀少寫多(會話)場景兼具的系統。同時需要擁有熱門C端產品的穩定性、擴展性和好的業務域解耦。針對讀多寫少和讀少寫多制定了針對的技術方案。具體的實施情況請繼續往下閲讀。
8、新架構的整體設計
結合B站業務現狀,我覺得比較合理的架構:
一個兼顧複雜列表查詢架構和IM架構的消息域框架,整體分四層:1)接入層:即toC的BFF和服務端網關;2)業務層:按複雜查詢設計系統,用於各種業務形態的支撐;3)平台層:按IM架構設計系統,目標是實時、有序的觸達用户,平台層可擴展;4)觸達層:對接長鏈和push。
9、新架構具體升級1:端上本地緩存降級
端上應該支持部分數據緩存,以確保極端情況下用户端可展示,可以是僅核心場景,比如支付小助手、官號通知,用户在任何情況下打開消息頁都不應該白屏。
10、新架構具體升級2:BFF架構升級
BFF網關吸收上浮的業務邏輯,控制需求向核心領域傳導。服務端基於業務領域的能力邊界,抽象出單聊、羣聊、系統通知、互動通知和消息設置共五個新服務,提升微服務健康度。
新服務剝離了歷史包袱,也解決一些在老服務難解的功能case,優化了用户體驗,比如消息頁不同類型消息的功能一致性;重新設計會話緩存結構和更新機制,優化Mysql索引,優化Mysql查詢語句,減少了一個量級的慢查詢。
11、新架構具體升級3:服務端可用性升級
11.1 概述服務端按四層拆分後,集中精力優化業務層和平台層。業務層:按複雜查詢設計系統,用於各種業務形態的支撐1)冷熱分離:多級緩存 redis(核心數據有過期)+taishan(有限明細數據)+mysql(全部數據);2)讀寫分離:95%以上覆雜查詢可以遷移到從庫讀。平台層:按IM架構設計系統,目標是實時、有序的觸達用户,平台層可擴展1)Timeline模型:依賴雪花發號器,成熟方案;2)讀寫擴散:單聊-寫擴散,羣聊-讀擴散。11.2 單聊會話1)緩存主動預熱:用户在首頁獲取未讀數是一個業務域內可以捕捉的事件,通過異步消費這個事件通知服務端創建會話緩存,提高用户查看會話的緩存命中率。鑑於大部分人打開B站並不會進私信,此處可以僅大UP預熱。大UP的uid集合可以在數平離線分析會話數據後寫入泰山表,這個泰山表更新時效是T+1。監控UP會話數量實時熱點,觸發突增閾值時,通過異步鏈路自動為熱點用户主動預熱會話列表緩存。對預熱成功率添加監控,並在數平離線任務失敗或者預熱失敗時做出業務告警,及時排查原因,避免功能失效。2)泰山和Mysql雙持久化:增加泰山存儲用户有限會話明細,作為redis未命中後的第一回源選擇,Mysql作為泰山之後的次選。基於用户翻頁長度分析後確定泰山存儲的有限會話的量級。
redis 存儲24小時數據,taishan 存儲 600條/用户(20頁),預設到的極端情況才會回源mysql從庫。對於ZSET和KV兩種數據結構,評估了各自讀寫性能的可靠性,符合業務預期。業務如果新增會話類型,可以跟本次新增泰山有限明細一樣,基於會話類型的具體規則新增泰山Key。
3)泰山長尾優化:查詢redis未命中時會優先回源泰山,考慮到泰山99分位線在50ms以下,而且Mysql多從實例都能承受來自C端的讀請求,所以採用比泰山報錯後降級Mysql稍微激進的對衝回源策略。在泰山出現“長尾”請求時,取得比較好的耗時優化效果。可以使用大倉提供的error group結合quit channel實現該回源策略,同時能避免協程泄漏。整個處理過程在業務響應和資源開銷中維持中間的平衡,等待泰山的時間可以靈活調整。
泰山最初沒有數據,可以在泰山未命中時進行被動加載,保證用户回訪時能命中。4)一致性保證:雖然我們重構了新服務,但是老服務也需要保留,用來處理未接入BFF的移動端老版本和web端請求,這些前端在更新會話時(比如ACK)請求到了老服務,新服務需要通過訂閲會話Mysql binlog異步更新本服務的redis和泰山。為了避免分區傾斜,訂閲binlog的dts任務使用id分區,這樣方便的是一條會話在topic的分區是固定的。為了避免兩次請求分別命中泰山和Mysql時給用户返回的數據不一樣,需要解決三大問題:a. 當出現分區rebalance需要避免重複消費;b. 當Mysql一條會話記錄在短時間內(秒級)多次更新,要保證binlog處理器不會逆時間序消費同一個會話的binlog,即跳過較早版本的binlog;c. 保證泰山寫入正確並且從Mysql低延遲同步。這三個問題都要保證最終一致性,具體解決方案是用redis lua腳本實現compare and swap,lua腳本具有原生的原子性優勢。dts每同步一條binlog都會攜帶毫秒級mtime,當binlog被採用時,mtime被記入redis10分鐘,如果下一條binlog的mtime大於redis記錄的mtime,這條binlog被採用,否則被丟棄。這個過程可以考慮使用gtid代替mtime,但這個存在的問題是每個從實例單獨維護自己的gtid,當特殊情況發生mysql主從切換,或者dts訂閲的從節點發生變更,gtid在CAS計算中變得不再可靠,所以我們選擇了使用mtime作為Mysql會話記錄的版本。通過消費路線高性能設計保證泰山異步更新的延遲在1秒以內,並在特殊情況延遲突破1s時有效告警。高性能消費路線中,每個庫的binlog分片到50個partition,業務提供不低於50個消費pod,單pod配置100併發數,按照寫泰山999分位線20ms計算,每秒可以消費 50100(1000/20)=250000 條,大約線上峯值8.3倍,考慮dts本身的max延遲在600~700毫秒,同步泰山和redis的延遲會在700毫秒至1秒以內,符合業務預期。
11.3 收件箱BFF已經從業務層和平台層將單聊讀收件箱獨立出來,本次升級主要是從存儲做增量解耦 ,存量單聊收件箱的讀流量可以訪問舊錶。 單聊新收件箱存儲採用redis+泰山的模式,redis提供熱數據,泰山提供全部數據並採用RANDOM讀模式,讓主副本都能分擔讀流量。
11.4 私信內容本次升級主要如下:1)單聊增量數據獨立存儲,按照單聊業務設計表結構,和羣聊、應援團小助手徹底解耦。2)寫Mysql升級為異步化操作,提高寫性能天花板,這種異步寫Mysql改造不會影響讀消息內容的可用性和設計。3)單聊分庫規則升級為月度分庫,單庫內分表為100張。 羣聊、應援團小助手和歷史單聊依然使用舊的分庫分表規則讀寫Mysql。業務需要對增量單聊私信路由分庫分表時,先從msgkey先解析出時間戳,找到用時間戳對應的月份分庫,然後用msgkey對100取餘找到分表。這種方案能達到按時間緯度的冷熱數據的分離,同時由於msgkey取餘的結果具有隨機性,平衡了每張表的讀寫流量。這樣預計2025年單表數據量能從9億下降到900萬。
11.5 批量私信日常通道:日常批量私信任務共用通道,共用配額。高優通道:主要通過將鏈路上topic partition擴容、消費POD擴容、POD內消費通道數擴容、緩存擴容、akso proxy連接數擴容,把平均發送速度從3500 人/秒提高到30000人/秒。這個通道可以特殊時期開給特殊業務使用。
12、本文小結
我們逐步發現技術升級不是一蹴而就的,它是一個逐步優化的過程。設計技術方案前設立合適和有一些挑戰的目標,但這個目標要控制成本,做好可行性。設計技術方案的時候,需要清楚現有架構與理想架構的差距和具體差異點,做多個方案選型,並確定一個,這個更多從技術團隊考慮。其次要保證功能在新老架構平穩過渡,保證業務的穩定性。後面持續關注新老架構的技術數據,持續優化,老架構要持續關注它的收斂替換。IM系統是一個老生常談的話題,也是融合眾多有趣技術難點的地方,歡迎感興趣的同行交流研討。
13、參考資料
[1] 淺談IM系統的架構設計[2] 簡述移動端IM開發的那些坑:架構設計、通信協議和客户端[3] 一套海量在線用户的移動端IM架構設計實踐分享(含詳細圖文)[4] 一套原創分佈式即時通訊(IM)系統理論架構方案[5] 從零到卓越:京東客服即時通訊系統的技術架構演進歷程[6] 蘑菇街即時通訊/IM服務器開發之架構選擇[7] 微信技術總監談架構:微信之道——大道至簡(演講全文)[8] 現代IM系統中聊天消息的同步和存儲方案探討[9] 子彈短信光鮮的背後:網易雲信首席架構師分享億級IM平台的技術實踐[10] 一套高可用、易伸縮、高併發的IM羣聊、單聊架構方案設計實踐[11] 從游擊隊到正規軍(一):馬蜂窩旅遊網的IM系統架構演進之路[12] 瓜子IM智能客服系統的數據架構設計(整理自現場演講,有配套PPT)[13] 阿里釘釘技術分享:企業級IM王者——釘釘在後端架構上的過人之處[14] 阿里技術分享:電商IM消息平台,在羣聊、直播場景下的技術實踐[15] 一套億級用户的IM架構技術乾貨(上篇):整體架構、服務拆分等[16] 從新手到專家:如何設計一套億級消息量的分佈式IM系統[17] 企業微信的IM架構設計揭秘:消息模型、萬人羣、已讀回執、消息撤回等[18] 融雲技術分享:全面揭秘億級IM消息的可靠投遞機制[19] 阿里IM技術分享(三):閒魚億級IM消息系統的架構演進之路[20] 基於實踐:一套百萬消息量小規模IM系統技術要點總結[21] 跟着源碼學IM(十):基於Netty,搭建高性能IM集羣(含技術思路+源碼)[22] 一套十萬級TPS的IM綜合消息系統的架構實踐與思考[23] 得物從0到1自研客服IM系統的技術實踐之路[24] 一套分佈式IM即時通訊系統的技術選型和架構設計[25] 微信團隊分享:來看看微信十年前的IM消息收發架構,你做到了嗎[26] 轉轉平台IM系統架構設計與實踐(一):整體架構設計[27] 支持百萬人超大羣聊的Web端IM架構設計與實踐[28] 轉轉客服IM聊天系統背後的技術挑戰和實踐分享
即時通訊技術學習:
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點此)
(本文已同步發佈於:http://www.52im.net/thread-4886-1-1.html)