天天看點

哔哩哔哩直播通用榜單系統

作者:閃念基因

榜單系統的定位和業務價值

榜單遍布B站直播相關業務的各個角落,直播打賞、直播間互動、付費玩法、互動玩法、活動、主播PK、語聊房、人氣主播排名、高價值使用者排名、增值集卡、up主充電等等,在這衆多的業務場景中,我們能看到各種各樣的榜單。

榜單的存在,可以激發主播提升表演水準、提高表演品質的積極性,進而吸引更多的觀衆。觀衆也可以通過榜單展現的排名,了解其他人對主播的互動打賞情況,激勵他更加積極地參與互動或打賞,進而獲得認同感和存在感。通過榜單,主播又能獲得更高的收益和更多的曝光流量。總之,榜單是一道連接配接平台、主播、觀衆的重要橋梁,對提升整個直播的良好氛圍有着極大的作用。另外,使用者上榜的規則是多樣化的,確定消費打賞行為不會過度商業化,在引導觀衆理性消費和平台健康發展方面也起着積極的作用。

哔哩哔哩直播通用榜單系統

業務概覽

下面是榜單系統的産品業務架構圖,雖然忽略了很多細節,但從此圖可以對B站直播榜單系統的全貌有個初步的認識。

哔哩哔哩直播通用榜單系統

系統介紹

在文章開頭已經介紹到了直播業務中榜單的種類是非常多的。

從榜單成員角色看,有主播榜、使用者榜、道具榜、房間榜、廳榜、公會榜;

從競争排名的範圍看,有全站榜、分區榜、活動賽季榜、直播場次榜;

從結榜時間粒度看,可以分為長期榜、季度榜、月榜、周榜、日榜、小時榜;

從具體場景上看,一些業務還會對榜單進行“賽道”劃分,如地區榜、男生女生榜,“賽道”的含義由業務方自定義、自了解。

哔哩哔哩直播通用榜單系統

由此可見,榜單系統所承載的業務不僅僅是場景多,而且接入形态各異。是以從接入易用性和開發效率上講,對榜單系統的通用性和靈活性有着較高的要求。接來下我們就從榜單配置化、業務接入、面臨的挑戰等幾個方面介紹一下榜單系統。

榜單配置

經過這麼長時間的發展,榜單幾乎成了每次業務疊代的伴生需求。很多時候每上線一個新的業務需求,就會同時上線一個甚至多個新的榜單,榜單系統是以就成了日常業務需求疊代的“基礎元件”。

那我們如何快速配置上線一個新榜單呢?做法就是打開管理背景,配一配。

基礎配置

哔哩哔哩直播通用榜單系統

榜單ID會在新榜單配置時由系統自動生成,生成後不再改變。後續通過接口進行榜單資料的讀取、更新時,都需要指定榜單ID。榜單ID是榜單系統中的“硬通貨”。

榜單名稱就是業務辨別,友善辨識。

榜單系統使用Mysql作為持久化存儲,并使用redis kv作為榜單成員分數的緩存,而排行榜功能的實作則是依賴Redis zset的能力。排行榜的緩存TTL、榜單成員分數的緩存TTL根據需求設定,要兼顧考慮業務流量壓力和緩存叢集的記憶體壓力。排行榜長度限制也是必須設定的,也就是要限制zset的長度,在滿足産品需求的同時要避免在Redis存儲節點中産生大key,了解redis核心線程模型和事件模型的人應該都知道Redis中出現大key的危害。

研發側還可以根據實際業務情況來指定要使用的Mysql叢集和Redis叢集分别是哪一套,進而做出合理的存儲、流量隔離。畢竟有的榜單業務的讀寫QPS可能都不過百,而有的業務單榜單的日常讀取QPS可以超過10萬。不同業務之間的榜單資料量級也是有很大差别的,有的業務榜單資料量比較大,但生命周期不長,可以為其配置設定單獨的存儲,制定不同的定期清理歸檔政策。

積分開始時間、積分結束時間應該不需要多解釋了,其實就是榜單線上上的有效時間範圍,多用在自動化加分且為定時上線的榜單中。

score位數切配置設定置與上方的榜單排序方式是有關聯的。在實際需求中,會有諸如“分數相同時先獲得此分數的排在前面”、“分數相同時粉絲勳章等級高的排在前面”等要求的排序政策。我們知道,Redis zset使用雙精度浮點數( a double 64-bit floating point number)來表示成員的排序權重分值,而double類型的最大有效位數為16位。是以我們可以指定n位整數部分表示真實的業務分數,(16-n)位小數部分輔助二級排序。在某一時刻(記時間戳為ts)調用通用接口進行分數更新時,根據配置計算最終分值的方式有三種:

  1. 按時間正序排序。取入參score作為整數部分,取(999999999-ts)且按位數截斷的結果作為小數部分。
  2. 按時間倒序排序。取入參score作為整數部分,取ts且按位數截斷的結果作為小數部分。
  3. 自定義排序。取入參score作為整數部分,取入參subscore作為小數部分,即整數部分、小數部分都由業務計算。

最後再将整數部分和小數部分進行字面拼接作為zset成員分值。如果有榜單成員的最終分值還是相同時,就遵循zset的内部實作,按照榜單成員key的二進制字典序進行排列了。

榜單次元配置

在榜單上線之前,還需要配置榜單的加分次元、分榜的時間粒度等。

哔哩哔哩直播通用榜單系統

通用次元配置中的次元項,是基于直播業務中的常用次元進行預設,勾選了加分次元就意味着此榜單會橫向進行分榜。可以舉例說明:

  1. 若不勾選任何選項,則沒有橫向分榜的需求,即全站一張榜。比如主播人氣榜,是全站所有的主播的人氣排名,隻需要一個榜單執行個體。
  2. 若勾選了主播,則表示針對每個主播會有一個榜單的執行個體。比如主播人氣應援榜,是給某一特定主播助力人氣值的所有使用者的排名。
  3. 若勾選了主播、場次ID兩個選項,則表示針對每個主播的每一個直播場次會有一個榜單的執行個體,比如主播進行的每次一次直播的打賞榜單,就可以這麼實作。
  4. 拓展次元,是當預設的次元不能夠滿足邏輯需求時,留給業務方自定義加分次元時使用的,業務方自了解。進行榜單更新、榜單讀取時,業務方自己使用統一的算法算出次元值。

若配置了時間次元,每個榜單執行個體會根據配置的時間粒度再進行縱向分榜,時間範圍為自然時間,辨別就是自然時間段的起始時間。比如選擇了自然天,則每天的0點時會自動切換到新的一天的榜單。實作方式也不複雜,就是根據每次加分行為的時間戳,算出目前時刻歸屬于哪個自然時間段,就會知道往哪個分榜上加分。榜單的讀取亦然,根據請求中指定的時間戳也能算出應該讀取哪一個分榜。

可以想到,每次針對榜單系統的寫入、讀取,都會有一個根據時間戳進行格式化計算時間的操作,而時間的格式化計算又比較耗費性能。是以這裡有個優化點,我們可以将自然時間的起止時間戳和它對應的格式化結果進行緩存,不必每次都進行一次計算,隻有當時間超出了緩存的有效範圍時才進行重新計算,在一些開源的高性能日志庫中也有類似的做法。

自動化加配置設定置

顧名思義,自動化加分就是當有特定的事件發生時,配置了此自動化加分的榜單都會被觸發更新。

哔哩哔哩直播通用榜單系統
哔哩哔哩直播通用榜單系統

自動化加分的由來

直播相關的業務代碼中,大量使用了MQ訂閱上遊業務消息,若每一次業務接入都要單獨注冊一遍消費者、實作一遍消費邏輯代碼,會做大量的相似性工作。是以為了提升開發效率,降低各個業務方接入MQ的成本,部門内實作了一個行為系統,所有的上遊消息都由行為系統統一訂閱消費,然後調用下遊各業務方的RPC接口進行行為事件投遞。當然這裡引入了另一個不可忽視的問題,但在本文不作重點讨論。

哔哩哔哩直播通用榜單系統

榜單服務作為行為系統的下遊,當有行為事件發生時,榜單RPC接口被調用,行為事件會扇出到所有打開了對應開關的榜單上。這樣業務方隻需要在管理背景配置一個榜單,就可以在需要的地方直接展示榜單了,還是非常友善的。

當然,不是所有的業務都适合自動化加分的鍊路,它們需要在自己的業務層邏輯中計算好分數,再主動調用榜單系統的通用接口進行榜單更新。

具體的加分流程的邏輯後文再介紹。

過濾規則配置

在榜單系統的資料處理流程中,內建了通用化的過濾配置子產品,具體配置頁面見下圖。

哔哩哔哩直播通用榜單系統

過濾規則是随着業務發展擴充出來的,适用面相對窄一點。一級、二級分區過濾表示隻有當行為事件發生在特定的直播分區才會進行加分,否則不加分。取反則是反向過濾。

若行為事件是使用者購買禮物消息,還可以指定隻有特定的商品才給此榜單加分,在一些活動中常用。

特殊榜單加分則是為特殊的業務邏輯hook,可以了解為在通用化鍊路中為特殊業務放置了一個回調的鈎子,也是在活動中常用。也可以了解為在通用化鍊路中耦合了一些業務邏輯,現在已經不推薦正常業務使用了。

資料存儲

結合基礎設施情況及自身的業務特點,在資料存儲選型上,榜單系統了分别選擇了mysql和redis作為持久化存儲系統和緩存系統。特别地,也依賴了redis的zset提供的能力來實作排行榜清單。榜單系統在存儲結構設計上,是和榜單加分次元配置的設計緊密關聯、互相對應的。

MYSQL存儲

通用榜單系統的mysql存儲表的結構也是統一的,最主要的一些字段如下:

CREATE TABLE `some_rank` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `rank_id` int(11) NOT NULL DEFAULT '0' COMMENT '榜單ID',
  `type_id` varchar(100) NOT NULL DEFAULT '' COMMENT '子榜辨別',
  `item_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '榜單成員辨別',
  `score` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '榜單成員積分',
  // ......
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='xxx榜單資料表'           

對于id、rank_id、score、rank、score這些字段,從名字及注釋可以非常容易的了解其含義,就沒必要多做解釋了。我們把重點放在type_id、item_id這兩個字段上。

type_id字段類型是verchar,其值的計算是和榜單“次元項”配置是對應的。代碼實作中封裝了固定邏輯,根據勾選的加分次元的字段name的字典序進行拼裝組合,得出type_id字段的值。榜單的加分次元固定後,算法結果也是相對穩定的。

比如某個業務榜單的加分次元配置如下:

哔哩哔哩直播通用榜單系統

那麼在計算type_id時,就按照“${錨定的子榜起始時間點}_${主播uid}_${其他}”的字元串形式進行拼接。錨定的子榜起始時間點會根據加分或查詢請求中的timestamp進行計算。

舉例,某次加分Request請求體及字段注釋如下:

{
  "rank_id": 12345, //榜單ID
  "item_id": 110000653, //榜單清單的一個成員ID,此請求傳入的為使用者的UID
  "score": 1980, //本次請求所增加的分數值
  "dimensions": { //加分次元必要的參數
    "ruid": 110000260, //主播的UID,也就是加分通用次元中“主播”選項對應的參數及value
    "timestamp": 1713165315, //加分行為的時間戳,用于錨定按時間切分的子榜
    // 其他可能需要的次元參數
  }
  // 其他必傳參數,如必要的幂等key、業務辨別等等
}           

此加分行為攜帶的時間戳是1713165315,即2024-04-15 15:15:15,而時間次元配置為1個自然月,那麼本次加分行為歸屬的自然月的起始時間就是2024-04-01 00:00:00,時間戳即1711900800,是以本次加分請求所計算的type_id就是“1711900800_110000260”,item_id字段所傳的值即使用者的uid。

那麼,本次加分行為可以解讀為:使用者(uid:110000653)在主播(uid:110000260)本月的榜單上貢獻了1980分,對應的資料行(主要字段)就如下所示。

哔哩哔哩直播通用榜單系統

rank_id、type_id、item_id就是資料行的一個主鍵。

同理,假使此榜單配置的時間次元為1自然日,那麼本次加分行為歸屬自然日的起始時間就是2024-04-15 00:00:00,時間戳即1713110400,那麼本次加分請求所計算的type_id就是“1713110400_110000260”。

由此我們也可以看出,每一個單獨的加分請求都要求傳遞一個行為發生的業務方時間戳,送禮加分行為攜帶的就是使用者送禮時的實時時間戳,彈幕加分行為攜帶的就是使用者發彈幕時的實時時間戳。這樣,即使出現了偶發的網絡抖動、MQ消息延遲等異常情況,也不會出現“昨天的送禮卻給今天的子榜加了分”的事情發生,哪怕出現了消費遊标重置、MQ rebalance等,當然這也需要幂等保證不會重複加分。

Redis緩存

Redis緩存榜單清單時,zset的key也由rank_id、type_id參與構成,這樣加分、查詢也都會根據時間戳錨定到正确的key。這裡不再贅述。

榜單更新

介紹完了如何快速配置上線一個榜單,那麼榜單配置完成之後,業務代碼又是如何對榜單進行更新的。下面提供一張榜單加分的流程圖作為說明。

哔哩哔哩直播通用榜單系統

流程圖展示了加分的大概步驟和簡要的說明,這裡再就幾個關鍵的點需要注意。

自動化加分時,如果有多個榜單都打開了對應的自動化加分開關,那麼一次行為觸發對多個榜單進行更新。如果出現某個榜單更新失敗,内部會自動針對這個榜單進行重試。

如果上遊MQ重置了遊标或觸發rebalance,消息不會因為被重複消費而導緻重複加分。

有一些榜單對使用者的感覺實時性要求較高,業務代碼中會根據需要,依賴長連系統的能力将最新的榜單資料廣播到端上,這樣使用者在不重新進房或者不主動重新整理榜單的情況下也能看到最新的榜單資料。

業務挑戰

本文開頭就提到了榜單系統應用場景的廣泛,也就意味着榜單系統的讀寫流量是非常高的。對一些榜單,觀衆能及時看到上榜或排名的變化是很有必要的,特别是消費打賞行為。榜單在平台營運中也扮演着重要的角色,比如對主播、使用者的激勵發放、活動結果的獎勵結算,有不少是依賴其在榜單上的排名作為依據。

是以榜單資料的準确性、系統的健壯性、接口的高效性也是重要的健康名額。

寫入性能

就目前來講,榜單寫入出現的性能瓶頸,多是來自自動化加分鍊路中使用者互動行為事件,比如持續觀播、點贊、發彈幕,這類行為一般是持續、海量的,且相對于使用者的消費打賞行為來說對體驗不敏感。

還有第二種情況就是當有大型賽事或活動時,就會出現單點熱門直播間,大量的寫入會造成存儲分片(redis、mysql)的吞吐壓力,可能會出現寫入逾時導緻資料積壓或者最終丢失,部分場景可能會出現重試雪崩。

針對第一種情況,榜單系統引入了一層”慢隊列“。将互動行為事件的加分請求進行預處理後,不直接更新至存儲,而是投遞到慢隊列中。慢隊列MQ接入公司的異步事件處理平台,利用平台聚合能力,将相同寫入次元的資料聚合之後再寫入存儲,大大降低存儲壓力。

哔哩哔哩直播通用榜單系統

* 圖檔來自異步事件處理平台說明文檔

對于第二種情況,我們多是采用降級政策,這類直播間很多時候并不需要展示不必要的榜單,可以針對整個榜單進行降級,不寫入不展示即可。也可以針對某些事件行為進行降級,比如當命中賽事直播間時,互動行為将降級為不加分。

讀取性能

榜單的讀取流量基本都打到redis叢集,通過redis叢集是可以扛住日常壓力的。較多出現讀取性能瓶頸的依然是一些熱門直播間,日常大v直播、大型賽事直播、大型晚會或活動直播等,這種情況下往往是對redis叢集中的單個node壓力過大。對于高熱直播間,我們通過增加二級記憶體緩存的方式進行了解決。

  • 接入CDN熱門房間SDK。這是公司内的一個公共元件,通過這個元件提供的配置化能力,我們可以知道目前直播間是否判定為熱門直播間。如果達到了熱門直播間的門檻值,系統會在記憶體中将榜單進行緩存,降低對redis叢集中單個存儲node壓力。
  • 開發了熱點探測元件。通過對榜單zset的通路情況進行記錄,将熱點zset key進行記憶體級緩存。
  • 配置化強制熱門房間。必要時手工指定某些直播間為熱門直播間,強制進行記憶體緩存。

加入二級緩存肯定會帶來一定實時性的犧牲,但都會在業務可接受的範圍内進行。

現狀下的架構

這裡引入一個内部的簡化版架構示意圖,僅供參考。

哔哩哔哩直播通用榜單系統

後續規劃

代碼品質的進一步提升

任何系統的設計,都脫離不了實際的适用場景,需要在各方面進行折衷取舍。現狀下的榜單系統其實就是“邊跑邊換輪子”的産物,比如還有很多業務邏輯還耦合在通用鍊路中。之前有過一些分層設計理念,也試圖引入更合理的設計模型來增強可擴充性、可維護性,但都并沒有完全的落地,需要繼續推進。

日志治理

目前榜單系統的日志量非常大,最多時每天将近20T。其中不乏噪音日志、含義重複、大結構輸出等不合理的做法,需要進行梳理治理。

存儲治理

  1. 老舊資料、過期資料較多,有很多不再使用的榜單遺留的配置、存儲表、無TTL緩存等,且mysql資料沒有自動過期的能力,可以建設自動化告警、定制化歸檔的能力。
  2. 雖有叢集隔離,但有的業務榜單在代碼中通過hard code交叉使用多個叢集,所謂的“核心”、“非核心”資料隔離存儲的優勢也喪失,總體感覺使用有些混亂。
  3. 缺少快捷的可視化、業務體感能力,想要檢視榜單系統目前存儲叢集的使用情況,需要分别翻看多個mysql、redis叢集的多個平台的監控,如容量監控、通路流量監控等等,然後再彙總評估。

榜單系統新架構

基于目前榜單系統的一些痛點

  • 更合理的架構分層、領域間更清晰的邊界劃分;榜單核心加業務層支撐,可擴充、可測試的自洽閉環。
  • 加分行為處理的實時性、資料一緻性的優化提升。
  • 更加科學的存儲選型。

作者:業務線

來源-微信公衆号:哔哩哔哩技術

出處:https://mp.weixin.qq.com/s/XCWM1OFDKXBGxa7TebrrzA