百萬線上的美拍直播彈幕系統架構實現

享飛的魚發表於2019-03-04

直播彈幕指直播間的使用者,禮物,評論,點贊等訊息,是直播間互動的重要手段。美拍直播彈幕系統從 2015 年 11 月到現在,經過了三個階段的演進,目前能支撐百萬使用者同時線上。比較好地詮釋了根據專案的發展階段,進行平衡演進的過程。這三個階段分別是快速上線,高可用保障體系建設,長連線演進。

一、快速上線

訊息模型

美拍直播彈幕系統在設計初期的核心要求是:快速上線,並能支撐百萬使用者同時線上。基於這兩點,我們策略是前中期 HTTP 輪詢方案,中後期替換為長連線方案。因此在業務團隊進行 HTTP 方案研發的同時,基礎研發團隊也緊鑼密鼓地開發長連線系統。

直播間訊息,相對於 IM 的場景,有其幾個特點

  • 訊息要求及時,過時的訊息對於使用者來說不重要;

  • 鬆散的群聊,使用者隨時進群,隨時退群;

  • 使用者進群后,離線期間(接聽電話)的訊息不需要重發;

對於使用者來說,在直播間有三個典型的操作:

  • 進入直播間,拉取正在觀看直播的使用者列表

  • 接收直播間持續接收彈幕訊息

  • 自己發訊息

我們把禮物,評論,使用者的資料都當做訊息來看待。經過考慮選擇了 Redis 的 sortedset 儲存訊息,訊息模型如下:

  • 使用者發訊息,通過 Zadd,其中 score 訊息的相對時間;

  • 接收直播間的訊息,通過 ZrangeByScore 操作,兩秒一次輪詢;

  • 進入直播間,獲取使用者的列表,通過 Zrange 操作來完成;

因此總的流程是

  • 寫訊息流程是: 前端機 -> Kafka -> 處理機 -> Redis

  • 讀訊息流程是: 前端 -> Redis

不過這裡有一個隱藏的併發問題:使用者可能丟訊息。

640.png

如上圖所示,某個使用者從第6號評論開始拉取,同時有兩個使用者在發表評論,分別是10,11號評論。如果11號評論先寫入,使用者剛好把6,7,8,9,11號拉走,使用者下次再拉取訊息,就從12號開始拉取,結果是:使用者沒有看到10號訊息。

為了解決這個問題,我們加上了兩個機制:

  • 在前端機,同一個直播間的同一種訊息型別,寫入 Kafka 的同一個 partition

  • 在處理機,同一個直播間的同一種訊息型別,通過 synchronized 保證寫入 Redis 的序列。

訊息模型及併發問題解決後,開發就比較順暢,系統很快就上線,達到預先預定目標。

上線後暴露問題的解決

上線後,隨著量的逐漸增加,系統陸續暴露出三個比較嚴重的問題,我們一一進行解決

問題一:訊息序列寫入 Redis,如果某個直播間訊息量很大,那麼訊息會堆積在 Kafka 中,訊息延遲較大。

解決辦法:

  • 訊息寫入流程:前端機-> Kafka -> 處理機 -> Redis

  • 前端機:如果延遲小,則只寫入一個 Kafka 的partion;如果延遲大,則這個直播的這種訊息型別寫入 Kafka 的多個partion。

  • 處理機:如果延遲小,加鎖序列寫入 Redis;如果延遲大,則取消鎖。因此有四種組合,四個檔位,分別是

    • 一個partion, 加鎖序列寫入 Redis, 最大併發度:1

    • 多個partition,加鎖序列寫入 Redis, 最大併發度:Kafka partion的個數

    • 一個partion, 不加鎖並行寫入 Redis, 最大併發度: 處理機的執行緒池個數

    • 多個partion, 不加鎖並行寫入 Redis,最大併發度: Kafka partition個數處理機執行緒池的個數

  • 延遲程度判斷:前端機寫入訊息時,打上訊息的統一時間戳,處理機拿到後,延遲時間 = 現在時間 – 時間戳;

  • 檔位選擇:自動選擇檔位,粒度:某個直播間的某個訊息型別

問題二:使用者輪詢最新訊息,需要進行 Redis 的 ZrangByScore 操作,redis slave 的效能瓶頸較大

解決辦法:

  • 本地快取,前端機每隔1秒左右取拉取一次直播間的訊息,使用者到前端機輪詢資料時,從本地快取讀取資料;

  • 訊息的返回條數根據直播間的大小自動調整,小直播間返回允許時間跨度大一些的訊息,大直播間則對時間跨度以及訊息條數做更嚴格的限制。

解釋:這裡本地快取與平常使用的本地快取問題,有一個最大區別:成本問題。

如果所有直播間的訊息都進行快取,假設同時有1000個直播間,每個直播間5種訊息型別,本地快取每隔1秒拉取一次資料,40臺前端機,那麼對 Redis 的訪問QPS是 1000 * 5 * 40 = 20萬。成本太高,因此我們只有大直播間才自動開啟本地快取,小直播間不開啟。

問題三:彈幕資料也支援回放,直播結束後,這些資料存放於 Redis 中,在回放時,會與直播的資料競爭 Redis 的 cpu 資源。

解決辦法:

  • 直播結束後,資料備份到 mysql;

  • 增加一組回放的 Redis;

  • 前端機增加回放的 local cache;

解釋:回放時,讀取資料順序是: local cache -> Redis -> mysql。localcache 與回放 Redis 都可以只存某個直播某種訊息型別的部分資料,有效控制容量;local cache與回放 Redis 使用SortedSet資料結構,這樣整個系統的資料結構都保持一致。

二、高可用保障

同城雙機房部署

分為主機房和從機房,寫入都在主機房,讀取則由兩個機房分擔。從而有效保證單機房故障時,能快速恢復。

豐富的降級手段

6402.jpg

全鏈路的業務監控

6403.jpg

高可用保障建設完成後,迎來了 TFBOYS 在美拍的四場直播,這四場直播峰值同時線上人數達到近百萬,共 2860萬人次觀看,2980萬評論,26.23億次點贊,直播期間,系統穩定執行,成功抗住壓力。

使用長連線替換短連線輪詢方案

長連線整體架構圖如下

6404.jpg

詳細說明:

  • 客戶端在使用長連線前,會呼叫路由服務,獲取連線層IP,路由層特性:a. 可以按照百分比灰度;b. 可以對 uid,deviceId,版本進行黑白名單設定。黑名單:不允許使用長連線;白名單:即使長連線關閉或者不在灰度範圍內,也允許使用長連線。這兩個特性保證了我們長短連線切換的順利進行;

  • 客戶端的特性:a. 同時支援長連線和短連線,可根據路由服務的配置來決定;b. 自動降級,如果長連線同時三次連線不上,自動降級為短連線;c. 自動上報長連線效能資料;

  • 連線層只負責與客戶端保持長連線,沒有任何推送的業務邏輯。從而大大減少重啟的次數,從而保持使用者連線的穩定;

  • 推送層儲存使用者與直播間的訂閱關係,負責具體推送。整個連線層與推送層與直播間業務無關,不需要感知到業務的變化;

  • 長連線業務模組用於使用者進入直播間的驗證工作;

  • 服務端之間的通訊使用基礎研發團隊研發的tardis框架來進行服務的呼叫,該框架基於 gRPC,使用 etcd 做服務發現;

長連線訊息模型

我們採用了訂閱推送模型,下圖為基本的介紹

6405.jpg

舉例說明:使用者1訂閱了A直播,A直播有新的訊息

  • 推送層查詢訂閱關係後,知道有使用者1訂閱了A直播,同時知道使用者1在連線層1這個節點上,那麼就會告知連線層有新的訊息

  • 連線層1收到告知訊息後,會等待一小段時間(毫秒級),再拉取一次使用者1的訊息,然後推送給使用者1.

如果是大直播間(訂閱使用者多),那麼推送層與連線層的告知/拉取模型,就會自動降級為廣播模型。如下圖所示

6406.jpg

我們經歷客戶端三個版本的迭代,實現了兩端(Android 與 iOS)長連線對短連線的替換,因為有灰度和黑白名單的支援,替換非常平穩,使用者無感知。

總結與展望

回顧了系統的發展過程,達到了原定的前中期使用輪詢,中後期使用長連線的預定目標,實踐了原定的平衡演進的原則。從發展來看,未來計劃要做的事情有

  • 針對某些地區會存在連線時間長的情況。我們如何讓長連線更靠近使用者。

  • 訊息模型的進一步演進。

相關文章