彈幕系統(tǒng)更新的血與淚
16年是直播浪潮興起的元年,許多互聯(lián)網(wǎng)公司的業(yè)務(wù)都開(kāi)始涉足直播內(nèi)容模塊。我目前所在公司接手的第一份工作,就是直播業(yè)務(wù)中的彈幕系統(tǒng)優(yōu)化。隨著公司直播業(yè)務(wù)的變化,彈幕系統(tǒng)從最初的版本到后來(lái)優(yōu)化了三四個(gè)版本,這個(gè)過(guò)程大概持續(xù)了一年的時(shí)間,本文將從我司早期的彈幕系統(tǒng)開(kāi)始給大家介紹整個(gè)更新過(guò)程的“血與淚 ”。
早期彈幕系統(tǒng)
一、基本狀況
1.由PHP + Gateway框架編寫
2.所有的Client ID存放在Redis里面
3.最初由三臺(tái)機(jī)器掛載在LVS系統(tǒng)后方提供服務(wù)
4.使用多進(jìn)程的方式,開(kāi)啟多個(gè)worker進(jìn)程來(lái)處理消息傳遞內(nèi)容
二、存在的問(wèn)題
1.內(nèi)存占用量巨大,單機(jī)(4核8G配置)承受500左右的Client就會(huì)達(dá)到內(nèi)存上限
2.每次發(fā)送消息的時(shí)候,每臺(tái)機(jī)器都需要從Redis里面拿取對(duì)應(yīng)房間的所有Client ID;并發(fā)高時(shí),Redis的單進(jìn)程處理效率和內(nèi)網(wǎng)帶寬就成為瓶頸 。
3.單機(jī)的并發(fā)處理能力被消息處理的worker進(jìn)程數(shù)量限制。同時(shí)開(kāi)啟過(guò)多的進(jìn)程,也是對(duì)系統(tǒng)資源的格外浪費(fèi)。
4.單房間超過(guò)2000人的時(shí)候,消息的延遲有可能會(huì)達(dá)到1分鐘左右,這是極其嚴(yán)重的問(wèn)題。
三、臨時(shí)改造
由于需要解決的問(wèn)題比較緊迫,所以快速做了一些邏輯上的改變和業(yè)務(wù)層面的取舍:
1.對(duì)Redis的實(shí)例進(jìn)行了拆分,使用了雙機(jī),單機(jī)4實(shí)例的方式,分散了Redis的壓力
2.對(duì)消息處理worker進(jìn)程的邏輯做了一些修改,限制了單位時(shí)間內(nèi)進(jìn)行廣播的消息數(shù)量,多余的消息會(huì)被丟棄 。
3.對(duì)于已經(jīng)完成了直播進(jìn)入點(diǎn)播狀態(tài)的房間,額外啟用了另外一套彈幕系統(tǒng)來(lái)進(jìn)行分流。
4.單個(gè)房間切成多個(gè)房間進(jìn)行消息處理。
四、改造之后的效果
1.Redis壓力大幅度降低
2.單機(jī)IO性能壓力降低
3.同樣數(shù)量的機(jī)器,可以承載更多的直播房間個(gè)數(shù)
但是,根本問(wèn)題并沒(méi)有得到解決。在臨時(shí)解決壓力問(wèn)題之后,我們需要花一些時(shí)間來(lái)重新對(duì)彈幕系統(tǒng)進(jìn)行分析,按照分析后的需求,對(duì)新的彈幕系統(tǒng)進(jìn)行重構(gòu)。
新的彈幕系統(tǒng)
一、新彈幕系統(tǒng)面臨的挑戰(zhàn)
1.單房間人數(shù)較高,依照我們公司直播情況,單房間5 – 10萬(wàn)人同時(shí)在線是會(huì)出現(xiàn)的。
2.由于直播內(nèi)容等情況造成的某時(shí)間段用戶暴漲。
3.需要盡可能實(shí)時(shí)到達(dá),延遲過(guò)高的話會(huì)大大降低互動(dòng)的實(shí)時(shí)性。
4.每一條消息,都要遞送大量的長(zhǎng)連接。
5.大量長(zhǎng)連接的維護(hù)機(jī)制。
6.在運(yùn)營(yíng)的過(guò)程中,需要處理用戶黑名單、IP黑名單、敏感詞等需求。
二、新的彈幕系統(tǒng)需求
1.由于內(nèi)存的管理對(duì)于PHP來(lái)說(shuō)算是一個(gè)短板,對(duì)于大并發(fā)且長(zhǎng)時(shí)間穩(wěn)定不需要經(jīng)常更新維護(hù)的系統(tǒng)來(lái)說(shuō),并非最好的選擇,因此選一門合適的語(yǔ)言是必須的。
2.分布式支持,可以快速的橫向擴(kuò)展,單房間人數(shù)可以支持到十萬(wàn)級(jí)別。
3.可以方便快捷的對(duì)系統(tǒng)進(jìn)行第三方消息的發(fā)送(例如禮物信息、系統(tǒng)通知等)。
4.盡量使用本地內(nèi)存管理來(lái)記錄房間內(nèi)客戶端連接,剩下大量的數(shù)據(jù)交互和查詢時(shí)間。
5.并發(fā)支持消息廣播,提高廣播效率。
三、新彈幕系統(tǒng)版本的改造方法
1.選擇當(dāng)前正紅且對(duì)高并發(fā)支持良好的Golang作為開(kāi)發(fā)語(yǔ)言。
2.使用開(kāi)發(fā)語(yǔ)言進(jìn)行客戶端連接的管理,且每臺(tái)機(jī)器只管理自己收到的連接請(qǐng)求。
3.使用并發(fā)的房間內(nèi)廣播邏輯,同時(shí)對(duì)多人進(jìn)行廣播。
新彈幕系統(tǒng)改造的相關(guān)經(jīng)驗(yàn)
下面先對(duì)一個(gè)模塊細(xì)節(jié)進(jìn)行分析,然后進(jìn)一步分析模塊上層的調(diào)度邏輯。
一、房間管理
- type RoomInfo struct {
- RoomID string //房間ID
- Lock *sync.Mutex //房間操作鎖
- Rows []*RowList //房間多行Slice
- Length uint64 //當(dāng)前房間總節(jié)點(diǎn)數(shù)
- LastChangeTime time.Time //最后一次更新時(shí)間
- }
- type RowList struct {
- Nodes []*Node //節(jié)點(diǎn)列表
- }
由于每個(gè)房間都有自己的ID,客戶端建立連接之后,就會(huì)被放到一個(gè)大廳房間里面。接著,客戶端自己提交RoomID上來(lái),連接會(huì)被重新連接到對(duì)應(yīng)的房間里面。 每個(gè)連接在建立之后,都會(huì)被包裝成一個(gè)Node,放到Rows里面。
- type Node struct {
- RoomID string
- ClientID int64
- Conn *websocket.Conn
- UpdateTime time.Time
- LastSendTime time.Time //最后一次發(fā)送消息時(shí)間
- IsAlive bool
- DisabledRead bool//是否已經(jīng)被關(guān)閉了發(fā)言權(quán)限
- }
每一個(gè)Node中,都有一個(gè)IsAlive來(lái)表示連接是否成功。如果連接斷開(kāi),或者因?yàn)槠渌驈?qiáng)制停止服務(wù)的話,會(huì)修改此標(biāo)記狀態(tài)。然后由定時(shí)的處理機(jī)制將此連接關(guān)閉并從內(nèi)存中清除。 Rows的本質(zhì)就是一組事先設(shè)定了長(zhǎng)度的Node Slice。
發(fā)送消息的時(shí)候,每一組slice使用一個(gè)協(xié)程來(lái)順序發(fā)送。同一房間內(nèi)的連接,就可以依照slice分組進(jìn)行并發(fā)發(fā)送。 發(fā)送的時(shí)候,會(huì)使用鎖將整個(gè)房間鎖住,以防止并發(fā)情況下同一連接混入兩條信息。
二、消息管理
- var messageChannel map[string]chan nodeMessage
- func init() {
- messageChannel = make(map[string]chan nodeMessage)
- }
- func sendMessageToChannel(roomId string, nm nodeMessage) error {
- //如果房間不存在,創(chuàng)建一個(gè)房間
- if c, ok := messageChannel[roomId]; ok {
- c
- } else {
- //創(chuàng)建房間通道
- messageChannel[roomId] = make(chan nodeMessage, 1024)
- messageChannel[roomId]
- //創(chuàng)建房間實(shí)例
- roomObj := &RoomInfo{}
- roomObj.RoomID = roomId
- roomObj.Rows = make([]*RowList, 0, 4)
- roomObj.Lock = &sync.Mutex{}
- //創(chuàng)建新的協(xié)程來(lái)監(jiān)控房間
- go daemonReciver(messageChannel[roomId], roomObj)
- go timerForClean(messageChannel[roomId])
- //如果是大廳的話,啟動(dòng)大廳清理協(xié)程
- if roomId == "" {
- go CleanHall(roomObj)
- }
- }
- return nil
- }
以上是關(guān)于彈幕信息傳遞的一部分代碼。 首先,每一個(gè)房間,都有自己的消息通道,所有的這些通道根據(jù)RoomID為key,記錄在一個(gè)叫做messageChannel的map里面。 每次收到消息的時(shí)候,都直接把消息丟到channel里面,就可以了。(后面由守護(hù)協(xié)程來(lái)處理)如果沒(méi)有房間通道的話,就建立房間的通道channel,并啟動(dòng)每個(gè)房間的一系列協(xié)程。
三、服務(wù)器管理
這里的方案比較簡(jiǎn)單,其實(shí)就是建立一個(gè)上一層的聊天室即一個(gè)房間,所有的服務(wù)器都會(huì)主動(dòng)連接到這里,每一個(gè)服務(wù)器收到的信息,就會(huì)在這個(gè)房間里面廣播到別的機(jī)器去。
四、守護(hù)協(xié)程們管理
守護(hù)協(xié)程處理很多瑣碎的事情,保證房間內(nèi)信息的正常分發(fā)以及房間連接的正常管理。各個(gè)守護(hù)協(xié)程的功能如下:
1.消息發(fā)送協(xié)程:每個(gè)房間配備一個(gè),從channel里面獲取到要發(fā)送到本房間的消息,然后在并發(fā)調(diào)用各個(gè)RowList的發(fā)送消息機(jī)制。
2.房間整理協(xié)程:因?yàn)闀?huì)有連接斷開(kāi)、房間更換等修改Node狀態(tài)的行為,所以定期會(huì)有房間整理協(xié)程來(lái)進(jìn)行節(jié)點(diǎn)整理,刪除當(dāng)前房間無(wú)關(guān)的節(jié)點(diǎn)等以提高消息的發(fā)送效率。
五、測(cè)試相關(guān)
運(yùn)行環(huán)境:云主機(jī)8核16G實(shí)例
操作系統(tǒng):Centos7(未進(jìn)行系統(tǒng)優(yōu)化或參數(shù)調(diào)整)
測(cè)試內(nèi)容:?jiǎn)螜C(jī)建立15000 websocket連接,并且發(fā)送消息,進(jìn)入指定房間(所有連接進(jìn)入同一房間)。一個(gè)客戶端進(jìn)入房間,發(fā)送一條消息,經(jīng)過(guò)敏感詞處理、IP和用戶黑名單處理,然后被廣播到所有節(jié)點(diǎn)。
測(cè)試結(jié)果:
CPU占用:保持在5%以下
內(nèi)存占用:2GB(包括操作系統(tǒng)本身開(kāi)銷)
網(wǎng)絡(luò)占用:峰值10Mb/s左右
發(fā)送效率:15000節(jié)點(diǎn)廣播,100ms – 110ms左右。
根據(jù)測(cè)試結(jié)果計(jì)算:
完全可以在8核16G的機(jī)器上,實(shí)現(xiàn)無(wú)壓力運(yùn)行50K并發(fā),峰值接近60 – 70K的處理能力。
六、更多分享
我目前正在嘗試把完成這套彈幕系統(tǒng)的基本功能開(kāi)源出來(lái)。已經(jīng)提取出來(lái)了一部分,當(dāng)前的地址為:https://github.com/logan-go/roomManager,感興趣的讀者可以通過(guò)鏈接查看。
小結(jié)
彈幕系統(tǒng)給視頻直播/點(diǎn)播增加了更多內(nèi)容的互動(dòng)娛樂(lè)性質(zhì),從最初的A站B站發(fā)展到現(xiàn)在各主流視頻網(wǎng)站APP。如何健康高效的管理彈幕系統(tǒng),也是當(dāng)下視頻行業(yè)需要重視的一門技術(shù)活。