但行好事,莫问前程

挖了太多坑,一点点填回来

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

弹幕系统, 架构, 架构设计

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

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

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

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

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

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

1
2
3
4
5
6
7
{
    "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数据结构,这样整个系统的数据结构都保持一致。

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

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