写点什么

弹幕系统更新的血与泪

  • 2019-11-13
  • 本文字数:3220 字

    阅读完需:约 11 分钟

弹幕系统更新的血与泪

16 年是直播浪潮兴起的元年,许多互联网公司的业务都开始涉足直播内容模块。我目前所在公司接手的第一份工作,就是直播业务中的弹幕系统优化。随着公司直播业务的变化,弹幕系统从最初的版本到后来优化了三四个版本,这个过程大概持续了一年的时间,本文将从我司早期的弹幕系统开始给大家介绍整个更新过程的“血与泪 ”。

早期弹幕系统

一、基本状况

1.由 PHP + Gateway 框架编写。


2.所有的 Client ID 存放在 Redis 里面。


3.最初由三台机器挂载在 LVS 系统后方提供服务。


4.使用多进程的方式,开启多个 worker 进程来处理消息传递内容。

二、存在的问题

1.内存占用量巨大,单机(4 核 8 G 配置)承受 500 左右的 Client 就会达到内存上限。


2.每次发送消息的时候,每台机器都需要从 Redis 里面拿取对应房间的所有 Client ID;并发高时,Redis 的单进程处理效率和内网带宽就成为瓶颈 。


3.单机的并发处理能力被消息处理的 worker 进程数量限制。同时开启过多的进程,也是对系统资源的格外浪费。


4.单房间超过 2000 人的时候,消息的延迟有可能会达到 1 分钟左右,这是极其严重的问题。

三、临时改造

由于需要解决的问题比较紧迫,所以快速做了一些逻辑上的改变和业务层面的取舍:


1.对 Redis 的实例进行了拆分,使用了双机,单机 4 实例的方式,分散了 Redis 的压力。


2.对消息处理 worker 进程的逻辑做了一些修改,限制单位时间内进行广播的消息数量,多余的消息会被丢弃 。


3.对于已经完成了直播进入点播状态的房间,额外启用了另外一套弹幕系统来进行分流。


4.单个房间切成多个房间进行消息处理。

四、改造之后的效果

1.Redis 压力大幅度降低;


2.单机 IO 性能压力降低;


3.同样数量的机器,可以承载更多的直播房间个数。



但是,根本问题并没有得到解决。在临时解决压力问题之后,我们需要花一些时间来重新对弹幕系统进行分析,按照分析后的需求,对新的弹幕系统进行重构。

新的弹幕系统

一、新弹幕系统面临的挑战

1.单房间人数较高,依照我们公司直播情况,单房间 5 - 10 万人同时在线是会出现的。


2.由于直播内容等情况造成的某时间段用户暴涨。


3.需要尽可能实时到达,延迟过高的话会大大降低互动的实时性。


4.每一条消息,都要递送大量的长连接。


5.大量长连接的维护机制。


6.在运营的过程中,需要处理用户黑名单、IP 黑名单、敏感词等需求。

二、新的弹幕系统需求

1.由于内存的管理对于 PHP 来说算是一个短板,对于大并发且长时间稳定不需要经常更新维护的系统来说,并非最好的选择,因此选一门合适的语言是必须的。


2.分布式支持,可以快速的横向扩展,单房间人数可以支持到十万级别。


3.可以方便快捷的对系统进行第三方消息的发送(例如礼物信息、系统通知等)。


4.尽量使用本地内存管理来记录房间内客户端连接,剩下大量的数据交互和查询时间。


5.并发支持消息广播,提高广播效率。

三、新弹幕系统版本的改造方法

1.选择当前正红且对高并发支持良好的 Golang 作为开发语言。


2.使用开发语言进行客户端连接的管理,且每台机器只管理自己收到的连接请求。


3.使用并发的房间内广播逻辑,同时对多人进行广播。

新弹幕系统改造的相关经验

下面先对一个模块细节进行分析,然后进一步分析模块上层的调度逻辑。

一、房间管理

type RoomInfo struct {     RoomID         string      //房间ID     Lock           *sync.Mutex //房间操作锁     Rows           []*RowList  //房间多行Slice     Length         uint64      //当前房间总节点数     LastChangeTime time.Time   //最后一次更新时间 } type RowList struct {     Nodes []*Node //节点列表 }
复制代码


由于每个房间都有自己的 ID,客户端建立连接之后,就会被放到一个大厅房间里面。接着,客户端自己提交 RoomID 上来,连接会被重新连接到对应的房间里面。 每个连接在建立之后,都会被包装成一个 Node,放到 Rows 里面。


type Node struct {     RoomID       string     ClientID     int64     Conn         *websocket.Conn     UpdateTime   time.Time     LastSendTime time.Time //最后一次发送消息时间     IsAlive      bool     DisabledRead    bool//是否已经被关闭了发言权限}
复制代码


每一个 Node 中,都有一个 IsAlive 来表示连接是否成功。如果连接断开,或者因为其他原因强制停止服务的话,会修改此标记状态。然后由定时的处理机制将此连接关闭并从内存中清除。


Rows 的本质就是一组事先设定了长度的 Node Slice。发送消息的时候,每一组 slice 使用一个协程来顺序发送。同一房间内的链接,就可以依照 slice 分组进行并发发送。 发送的时候,会使用锁将整个房间锁住,以防止并发情况下同一连接混入两条信息。

二、消息管理

var messageChannel map[string]chan nodeMessage func init() {     messageChannel = make(map[string]chan nodeMessage) } func sendMessageToChannel(roomId string, nm nodeMessage) error {     //如果房间不存在,创建一个房间     if c, ok := messageChannel[roomId]; ok {         c <- nm     } else {         //创建房间通道         messageChannel[roomId] = make(chan nodeMessage, 1024)         messageChannel[roomId] <- nm         //创建房间实例         roomObj := &RoomInfo{}         roomObj.RoomID = roomId         roomObj.Rows = make([]*RowList, 0, 4)         roomObj.Lock = &sync.Mutex{}         //创建新的协程来监控房间         go daemonReciver(messageChannel[roomId], roomObj)         go timerForClean(messageChannel[roomId])         //如果是大厅的话,启动大厅清理协程         if roomId == "" {             go CleanHall(roomObj)         }     }     return nil }
复制代码


以上是关于弹幕信息传递的一部分代码。首先,每一个房间,都有自己的消息通道,所有的这些通道根据 RoomID 为 key,记录在一个叫做 messageChannel 的 map 里面。 每次收到消息的时候,都直接把消息丢到 channel 里面,就可以了。(后面由守护协程来处理)如果没有房间通道的话,就建立房间的通道 channel,并启动每个房间的一系列协程。

三、服务器管理

这里的方案比较简单,其实就是建立一个上一层的聊天室即一个房间,所有的服务器都会主动连接到这里,每一个服务器收到的信息,就会在这个房间里面广播到别的机器去。

四、守护协程们管理

守护协程处理很多琐碎的事情,保证房间内信息的正常分发以及房间连接的正常管理。各个守护协程的功能如下:


1.消息发送协程:每个房间配备一个,从 channel 里面获取到要发送到本房间的消息,然后在并发调用各个 RowList 的发送消息机制。


2.房间整理协程:因为会有连接断开、房间更换等修改 Node 状态的行为,所以定期会有房间整理协程来进行节点整理,删除当前房间无关的节点等以提高消息的发送效率。


五、测试相关

运行环境: 云主机 8 核 16 G 实例


操作系统: Centos 7(未进行系统优化或参数调整)


测试内容: 单机建立 15000 websocket 连接,并且发送消息,进入指定房间(所有连接进入同一房间)。一个客户端进入房间,发送一条消息,经过敏感词处理、IP 和用户黑名单处理,然后被广播到所有节点。


测试结果如下:


CPU 占用: 保持在 5% 以下


内存占用: 2GB(包括操作系统本身开销)


网络占用: 峰值 10Mb/s 左右


发送效率: 15000 节点广播,100ms - 110ms 左右。


根据测试结果计算:


完全可以在 8 核 16G 的机器上,实现无压力运行 50K 并发,峰值接近 60 - 70K 的处理能力。

六、更多分享

我目前正在尝试把完成这套弹幕系统的基本功能开源出来。已经提取出来了一部分,感兴趣的读者可以通过链接查看。


弹幕系统给视频直播/点播增加了更多内容的互动娱乐性质,从最初的 A 站 B 站发展到现在各主流视频网站 APP。如何健康高效的管理弹幕系统,也是当下视频行业需要重视的一门技术活。


本文转载自公众号 UCloud 技术(ID:ucloud_tech)。


原文链接:


https://mp.weixin.qq.com/s/CXp6Q2pxo3Pc7yQg4gM1vw


2019-11-13 11:571000

评论

发布
暂无评论
发现更多内容

数据灾备方案,华为云为何受众多企业信赖与选择?

平平无奇爱好科技

面试还不懂JVM调优,看这篇就够了

程序员小毕

程序员 面试 后端 架构师 jvm调优

华为云虚拟专用网络VPN常见问题解答

YG科技

率失真函数的性质

timerring

信息论与编码

从原理聊JVM(二):从串行收集器到分区收集开创者G1

京东科技开发者

G1 JVM 垃圾回收器 java 企业号 4 月 PK 榜

华为云桌面随需而至,让办公数字化触手可及

平平无奇爱好科技

摆脱终端束缚,华为云桌面助力企业数字化转型

平平无奇爱好科技

灵活高效,华为云桌面实现随时随地办公

YG科技

浅谈华为云CDN在互联网领域的应用场景落地

YG科技

华为云大数据BI解决方案,助力企业数字化运营

轶天下事

企业上云,华为云桌面兼顾效率与安全

平平无奇爱好科技

中国信通院“OSCAR开源生态建设沙龙”成功召开

中国IDC圈

开源

七大关键技术,华为云数据库GaussD承载金融级核心系统

平平无奇爱好科技

云计算,

华为云CDN赋能企业数字化转型

YG科技

复旦MOSS大模型开源了「中国版ChatGPT」,Github和Hugging Face同时上线

肥晨

三周年连更

THREE.JS实现炫酷的3D简历网站

知心宝贝

前端 后端 3D ThreeJS 三周年连更

从源码全面解析 ArrayBlockingQueue 的来龙去脉

Java 源码 ArrayBlockingQueue

NFT加密钱包交易系统开发搭建技术

薇電13242772558

NFT

PHP 中数组是如何灵活支持多数据类型的?

架构精进之路

php 数组 后端 三周年连更

华为云OBS,助力企业海量、安全、高可靠、低成本数据存储

轶天下事

云计算,

Tars-Cpp 协程实现分析

vivo互联网技术

协程 TARS

Guava的EventBus事件机制实现

Java你猿哥

Java 源码 ssm Guava EventBus

华为云CDN加速,赋能企业数字化转型升级

YG科技

4月26日-30日,KaiwuDB 在数字中国等你!

KaiwuDB

数字中国 KaiwuDB

企业数据的最后防线——华为云数据灾备

YG科技

云计算,

华为云桌面,如何为企业构建新型工作方式

平平无奇爱好科技

面试官:介绍一下什么是缓存雪崩、缓存击穿、缓存穿透?

Java redis 缓存穿透 缓存击穿 缓存雪崩

Matlab实现粒子群算法

Shine

三周年连更

华为云大数据BI方案为房地产行业数字化发展赋能

轶天下事

云计算,

AI降临,前端启用面壁计划

京东科技开发者

人工智能 AI 前端 企业号 4 月 PK 榜

NPM 实用命令与快捷方式

SEAL安全

JavaScript npm 企业号 4 月 PK 榜

弹幕系统更新的血与泪_文化 & 方法_大妖_InfoQ精选文章