1

思考题:直播弹幕系统的设计

 2 years ago
source link: http://caiknife.github.io/blog/2018/11/25/zhi-bo-dan-mu-xi-tong-de-she-ji/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

思考题:直播弹幕系统的设计

Nov 25th, 2018

Posted by CaiKnife

Nov 25th, 2018弹幕系统, 架构, 架构设计

从来没有开发过弹幕系统,今天在被问到这个问题的时候有点不知所措,以为是传统的像 BiliBili 这样的弹幕视频网站一样,从存储系统中拉取已有弹幕数据交付给前端按时间顺序显示就好,但是事后重新思考这个的问题的时候,发现不对——绝对不是这么简单。当时我应该误解了对方的意思,没有及时作出沟通,一下子就懵了。

面试官想考察我的真正场景是——如何设计一个直播平台的弹幕系统

晚上回来赶紧再复盘一下这个问题。

我依稀记得面试官的问题是这样的:

不使用PUSH方式,不使用长连接的方案下,如何设计一个(直播平台的)弹幕系统,并且能够突出显示我自己发的弹幕。

在B站这样的弹幕网站里,除开直播频道之外,每个单独的视频应该都是把已有的弹幕都存储起来,而且由于B站每个视频的弹幕是有上限的,这样就保证了数据不会超载,所以最简单的方式就是可以直接使用 redis 的 list 来实现,单条数据存储的可能是像下面这样的数据结构:

{
    "member_id", // 用户ID
    "content", // 弹幕内容
    "offset_time", // 相对于视频时长的偏移时间,用于确定弹幕出现的位置
    "timestamp", // 用户真正发表弹幕的时间
    "extra" // 扩展字段,比如弹幕的效果(顶端,底端)、样式(颜色,字体大小)等等
}

当然如果稍做一些修改的话,也可以用 redis 的 sorted set 来实现。在这样的场景下,只需要在后端从存储中获取到每个视频对应的弹幕数据,排序好之后交给前端处理就好,甚至还可以不用后端做排序,让前端根据偏移时间自行做排序减少服务器的资源消耗。而要突出显示我自己的弹幕的话,只需要写完弹幕发送的时候,直接由前端处理先实时显示在屏幕上,然后再上报给后端接口存储起来就好。

但是,直播系统的弹幕和这上面的思路完全不一样!!!

直播间消息,相对于 IM 的场景,有其几个特点:1、消息要求及时,过时的消息对于用户来说不重要;2、松散的群聊,用户随时进群,随时退群;3、用户进群后,离线期间的消息不需要重发。

对于用户来说,在直播间有三个典型的操作:1、进入直播间,拉取正在观看直播的用户列表;2、接收直播间持续接收弹幕消息;3、自己发消息。

在这样的场景下,初步的设计可以做成这样——选择了 redis 的 sorted set 存储消息,基本操作如下:

用户发弹幕,通过 zAdd 添加数据,其中 score 是弹幕的发送时间;

接收直播间的消息,通过 zRangeByScore 操作,两秒一次轮询;

进入直播间,获取用户的列表,通过 zRange 操作来完成;

整个系统的流程应该是:

写流程是: 前端提交弹幕给后端 –> 后端将弹幕推入队列 –> 队列处理机进行处理 –> 存储到 redis

读流程是: 前端轮询请求后端 –> 后端使用 zRangeByScore查询 redis –> 前端按时间顺序显示弹幕

这个初步方案可能只能在直播人数较少的情况下起效,随着人数越来越多,瓶颈很快就能达到,会产生一些问题。

第一个问题——消息串行写入 redis,如果某个直播间消息量很大,那么消息会堆积在队列中,消息延迟较大。

这个问题需要使用合适的消息队列来进行处理,由于我目前使用的最多的消息队列只有基于 redis 的 resque 和基于 Golang 的 nsque。没有做过详尽的性能测试来确定这两种队列能处理多大的 QPS,如果可以的话那就自然最好;如果不行的话,那就要选择更高性能的比如 Kafka 或者其他的分布式消息队列。

第二个问题——用户轮询最新消息,需要进行 redis 的 zRangeByScore 操作,redis slave 的性能瓶颈较大。

解决这个问题可以额外增加一层缓存。后端每隔 1 秒左右取拉取一次直播间的弹幕,前端轮询数据时,从该缓存读取数据。弹幕的返回条数根据直播间的大小自动调整,小直播间返回允许时间跨度大一些的弹幕,大直播间则对时间跨度以及弹幕条数做更严格的限制。这里缓存与平常使用的本地缓存问题,有一个最大区别:成本问题。如果所有直播间的弹幕都进行缓存,假设同时有 1000 个直播间,每个直播间有5种弹幕类型,缓存每隔 1 秒拉取一次数据,40 台缓存处理机器,那么对 redis 的访问 QPS 是 1000 * 5 * 40 = 20W。成本太高,因此我们只有大直播间才自动开启缓存,小直播间不开启。

第三个问题——弹幕数据也支持回放,直播结束后,这些数据存放于 redis 中,在回放时,会与直播的数据竞争 redis 的 CPU 资源。

解决方案——直播结束后,数据备份到 MySQL;增加一组回放的 redis;增加回放的缓存。回放时,读取数据顺序是: 缓存 –> redis –> MySQL。缓存与回放 redis 都可以只存某个直播间某种弹幕类型的部分数据,有效控制容量;缓存与回放 redis 使用 sorted set数据结构,这样整个系统的数据结构都保持一致。

我个人能力有限,暂时只能想到这么多。除了上面这些之外,还需要考虑整个系统的高可用保障——包括机房部署、降级和熔断、全面的业务监控、轮询方案的优化等等。

暂时就这些,我的知识面还需要不断完善,业务场景还需要不断扩充。


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK