1、引言
在视频直播场景中,弹幕交互、与主播的聊天、各种业务指令等等,组成了普通用户与主播之间的互动方式。
从技术的角度来看,这些实时互动手段,底层逻辑都是实时聊天消息或指令的分发,技术架构类比于IM应用的话,那就相当于IM聊天室功能。
本文将主要从高可用、弹性扩缩容、用户管理、消息分发、客户端优化等角度,分享直播间海量聊天消息的架构设计技术难点的实践经验。
2直播间的主要功能和技术特征
如今的视频直播间早已不单纯是视频流媒体技术问题,它还包含了用户可感知的多类型消息发送和管理、用户管理等任务。在万物皆可直播的当下,超大型直播场景屡见不鲜,甚至出现了人数无上限的场景,面对如此海量实时消息和指令的并发挑战,带来的技术难度已非常规手段所能解决。
我们先来归纳一下如今的典型视频直播间,相较于传统直播间所包含的主要功能特征、技术特征等。
丰富的消息类型和进阶功能:
- 1)可发送文字、语音、图片等传统聊天功能;
- 2)可实现点赞、礼物等非传统聊天功能的消息类型;
- 3)可管理内容安全,包括敏感词设置,聊天内容反垃圾处理等。
聊天管理功能:
- 1)用户管理:包括创建、加入、销毁、禁言、查询、封禁(踢人)等;
- 2)用户白名单:白名单用户处于被保护状态不会被自动踢出,且发送消息优先级别最高;
- 3)消息管理:包括消息优先级、消息分发控制等;
- 4)实时统计及消息路由等能力。
人数上限和行为特征:
- 1)人数没有上限:一些大型直播场景,如春晚、国庆大阅兵等,直播间累计观看动辄上千万人次,同时观看人数也可达数百万;
- 2)用户进退行为:用户进出直播间非常频繁,高热度直播间的人员进出秒并发可能上万,这对服务支撑用户上下线以及用户管理的能力提出了非常大的挑战。
海量消息并发:
- 1)消息并发量大:直播聊天室人数没有明显上限,带来了海量并发消息的问题(一个百万人数的聊天室,消息的上行已是巨量,消息分发量更是几何级上升);
- 2)消息实时性高:如果服务器只做消息的消峰处理,峰值消息的堆积会造成整体消息延时增大。
针对上述第 2) 点,延时的累积效应会导致消息与直播视频流在时间线上产生偏差,进而影响用户观看直播时互动的实时性。所以,服务器的海量消息快速分发能力十分重要。
3直播间聊天室的架构设计
高可用系统需要支持服务故障自动转移、服务精准熔断降级、服务治理、服务限流、服务可回滚、服务自动扩容 / 缩容等能力。
以服务高可用为目标的直播间聊天室系统架构如下:
如上图所示,系统架构主要分三层:
- 1)连接层:主要管理服务跟客户端的长链接;
- 2)存储层:当前使用的是 Redis,作为二级缓存,主要存储聊天室的信息(比如人员列表、黑白名单、封禁列表等,服务更新或重启时,可以从 Redis 中加载出聊天室的备份信息);
- 3)业务层:这是整个聊天室的核心,为了实现跨机房容灾,将服务部署在多个可用区,并根据能力和职责,将其分为聊天室服务和消息服务。
聊天室服务和消息服务的具体职责:
- 1)聊天室服务:主要负责处理管理类请求,比如聊天室人员的进出、封禁 / 禁言、上行消息处理审核等;
- 2)消息服务:主要缓存本节点需要处理的用户信息以及消息队列信息,并负责聊天室消息的分发。
在海量用户高并发场景下,消息分发能力将决定着系统的性能。以一个百万级用户量的直播间聊天室为例,一条上行消息对应的是百万倍的分发。这种情况下,海量消息的分发,依靠单台服务器是无法实现的。
我们的优化思路是:将一个聊天室的人员分拆到不同的消息服务上,在聊天室服务收到消息后向消息服务扩散,再由消息服务分发给用户。
以百万在线的直播间聊天室为例:假设聊天室消息服务共 200 台,那平均每台消息服务管理 5000 人左右,每台消息服务在分发消息时只需要给落在本台服务器上的用户分发即可。
服务落点的选择逻辑:
- 1)在聊天室服务中:聊天室的上行信令是依据聊天室 ID 使用一致性哈希算法来选择节点的;
- 2)在消息服务中:依据用户 ID 使用一致性哈希算法来决定用户具体落在哪个消息服务。
一致性哈希选择的落点相对固定,可以将聊天室的行为汇聚到一个节点上,极大提升服务的缓存命中率。
聊天室人员进出、黑 / 白名单设置以及消息发送时的判断等处理直接访问内存即可,无须每次都访问第三方缓存,从而提高了聊天室的响应速度和分发速度。
最后:Zookeeper 在架构中主要用来做服务发现,各服务实例均注册到 Zookeeper。
4直播间聊天室的扩缩容能力
5.1概述
随着直播这种形式被越来越多人接受,直播间聊天室面对人数激增致使服务器压力逐步增大的情况越来越多。所以,在服务压力逐步增大 / 减少的过程中能否进行平滑的扩 / 缩容非常重要。
在服务的自动扩缩容方面,业内提供的方案大体一致:即通过压力测试了解单台服务器的瓶颈点 → 通过对业务数据的监控来判断是否需要进行扩缩 → 触发设定的条件后报警并自动进行扩缩容。
鉴于直播间聊天室的强业务性,具体执行中应该保证在扩缩容中整体聊天室业务不受影响。
5.2聊天室服务扩缩容
聊天室服务在进行扩缩容时,我们通过 Redis 来加载成员列表、封禁 / 黑白名单等信息。
需要注意的是:在聊天室进行自动销毁时,需先判断当前聊天室是否应该是本节点的。如果不是,跳过销毁逻辑,避免 Redis 中的数据因为销毁逻辑而丢失。
聊天室服务扩缩容方案细节如下图所示:
5.3消息服务扩缩容
消息服务在进行扩缩容时,大部分成员需要按照一致性哈希的原则路由到新的消息服务节点上。这个过程会打破当前的人员平衡,并做一次整体的人员转移。
1)在扩容时:我们根据聊天室的活跃程度逐步转移人员。
2)在有消息时:[消息服务会遍历缓存在本节点上的所有用户进行消息的通知拉取,在此过程中判断此用户是否属于这台节点(如果不是,将此用户同步加入到属于他的节点)。
3)在拉消息时:用户在拉取消息时,如果本机缓存列表中没有该用户,消息服务会向聊天室服务发送请求确认此用户是否在聊天室中(如果在则同步加入到消息服务,不在则直接丢掉)。
4)在缩容时:消息服务会从公共 Redis 获得全部成员,并根据落点计算将本节点用户筛选出来并放入用户管理列表中。
6、海量用户的上下线和管理
聊天室服务:管理了所有人员的进出,人员的列表变动也会异步存入 Redis 中。
消息服务:则维护属于自己的聊天室人员,用户在主动加入和退出房间时,需要根据一致性哈希算出落点后同步给对应的消息服务。
聊天室获得消息后:聊天室服务广播给所有聊天室消息服务,由消息服务进行消息的通知拉取。消息服务会检测用户的消息拉取情况,在聊天室活跃的情况下,30s 内人员没有进行拉取或者累计 30 条消息没有拉取,消息服务会判断当前用户已经离线,然后踢出此人,并且同步给聊天室服务对此成员做下线处理。
7、海量聊天消息的分发策略
直播间聊天室服务的消息分发及拉取方案如下图:
关注+后台私信扣1,免费分享2022最新最全学习提升资料包,资料内容包括《Andoird音视频开发必备手册+音视频最新学习视频+大厂面试真题+2022最新学习路线图+项目实战源码》(C/C++,Linux,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)等等
消息通知的拉取
在上图中:用户 A 在聊天室中发送一条消息,首先由聊天室服务处理,聊天室服务将消息同步到各消息服务节点,消息服务向本节点缓存的所有成员下发通知拉取(图中服务器向用户 B 和用户 Z 下发了通知)。
在消息分发过程中,server 做了通知合并。
通知拉取的详细流程为:
- 1)客户端成功加入聊天,将所有成员加入到待通知队列中(如已存在则更新通知消息时间);
- 2)下发线程,轮训获取待通知队列;
- 3)向队列中用户下发通知拉取。
通过这个流程可保障下发线程一轮只会向同一用户发送一个通知拉取(即多个消息会合并为一个通知拉取),有效提升了服务端性能且降低了客户端与服务端的网络消耗。
7.2消息的拉取
用户的消息拉取流程如下图:
如上图所示,用户 B 收到通知后向服务端发送拉取消息请求,该请求最终将由消息节点 1 进行处理,消息节点 1 将根据客户端传递的最后一条消息时间戳,从消息队列中返回消息列表(参考下图 )。
客户端拉取消息示例:
用户端本地最大时间为 1585224100000,从 server 端可以拉取到比这个数大的两条消息。
7.3消息控速
服务器应对海量消息时,需要做消息的控速处理。
这是因为:在直播间聊天室中,大量用户在同一时段发送的海量消息,一般情况下内容基本相同。如果将所有消息全部分发给客户端,客户端很可能出现卡顿、消息延迟等问题,严重影响用户体验。
所以服务器对消息的上下行都做了限速处理。
消息控速原理:
具体的限速控制策略如下:
- 1)服务器上行限速控制(丢弃)策略:针对单个聊天室的消息上行的限速控制,我们默认为 200 条 / 秒,可根据业务需要调整。达到限速后发送的消息将在聊天室服务丢弃,不再向各消息服务节点同步;
- 2)服务器下行限速(丢弃)策略:服务端的下行限速控制,主要是根据消息环形队列的长度进行控制,达到最大值后最“老”的消息将被淘汰丢弃。
每次下发通知拉取后服务端将该用户标记为拉取中,用户实际拉取消息后移除该标记。
如果产生新消息时用户有拉取中标记:
- 1)距设置标记时间在 2 秒内,则不会下发通知(降低客户端压力,丢弃通知未丢弃消息);
- 2)超过 2 秒则继续下发通知(连续多次通知未拉取则触发用户踢出策略,不在此赘述)。
因此:消息是否被丢弃取决于客户端拉取速度(受客户端性能、网络影响),客户端及时拉取消息则没有被丢弃的消息。
8、直播间聊天室的消息优先级
消息控速的核心是对消息的取舍,这就需要对消息做优先级划分。
划分逻辑大致如下:
- 1)白名单消息:这类消息最为重要,级别最高,一般系统类通知或者管理类信息会设置为白名单消息;
- 2)高优先级消息:仅次于白名单消息,没有特殊设置过的消息都为高优先级;
- 3)低优先级消息:最低优先级的消息,这类消息大多是一些文字类消息。
具体如何划分,应该是可以开放出方便的接口进行设置的。
服务器对三种消息执行不同的限速策略,在高并发时,低优先级消息被丢弃的概率最大。
服务器将三种消息分别存储在三个消息桶中:客户端在拉取消息时按照白名单消息 > 高优先级消息 > 低优先级消息的顺序拉取。
9、客户端针对大量消息的接收和渲染优化
9.1消息的接收优化
在消息同步机制方面,如果直播间聊天室每收到一条消息都直接下发到客户端,无疑会给客户端带来极大性能挑战。特别是在每秒几千或上万条消息的并发场景下,持续的消息处理会占用客户端有限的资源,影响用户其它方面的互动。
考虑到以上问题,为聊天室单独设计了通知拉取机制,由服务端进行一系列分频限速聚合等控制后,再通知客户端拉取。
具体分为以下几步:
- 1)客户端成功加入聊天室;
- 2)服务端下发通知拉取信令;
- 3)客户端根据本地存储的消息最大时间戳,去服务端拉取消息。
这里需要注意的是:首次加入直播间聊天室时,本地并没有有效时间戳,此时会传 0 给服务拉取最近 50 条消息并存库。后续再次拉取时才会传递数据库里存储的消息的最大时间戳,进行差量拉取。
客户端拉取到消息后:会进行排重处理,然后将排重后的数据上抛业务层,以避免上层重复显示。
另外:直播间聊天室中的消息即时性较强,直播结束或用户退出聊天室后,之前拉取的消息大部分不需要再次查看,因此在用户退出聊天室时,会清除数据库中该聊天室的所有消息,以节约存储空间。
9.2消息的渲染优化
在消息渲染方面,客户端也通过一系列优化保证在直播间聊天室大量消息刷屏的场景下仍有不俗的表现。
以Andriod端为例,具体的措施有:
- 1)采用 MVVM 机制:将业务处理和 UI 刷新严格区分。每收到一条消息,都在 ViewModel 的子线程将所有业务处理好,并将页面刷新需要的数据准备完毕后,才通知页面刷新;
- 2)降低主线程负担:精确使用 LiveData 的 setValue() 和 postValue() 方法:已经在主线程的事件通过 setValue() 方式通知 View 刷新,以避免过多的 postValue() 造成主线程负担过重;
- 3)减少非必要刷新:比如在消息列表滑动时,并不需要将接收到的新消息刷新出来,仅进行提示即可;
- 4)识别数据的更新:通过谷歌的数据对比工具 DiffUtil 识别数据是否有更新,仅更新有变更的部分数据;
- 5)控制全局刷新次数:尽量通过局部刷新进行 UI 更新。
通过以上机制:从压测结果看,在中端手机上,直播间聊天室中每秒 400 条消息时,消息列表仍然表现流畅,没有卡顿。
10、针对传统聊天消息外的自定义属性优化
10.1概述
在直播间聊天室场景中,除了传统的聊天消息收发以外,业务层经常需要有自己的一些业务属性,如在语音直播聊天室场景中的主播麦位信息、角色管理等,还有狼人杀等卡牌类游戏场景中记录用户的角色和牌局状态等。
相对于传统聊天消息,自定义属性有必达和时效的要求,比如麦位、角色等信息需要实时同步给聊天室的所有成员,然后客户端再根据自定义属性刷新本地的业务。
10.2自定义属性的存储
自定义属性是以 key 和 value 的形式进行传递和存储的。自定义属性的操作行为主要有两种:即设置和删除。
服务器存储自定义属性也分两部分:
- 1)全量的自定义属性集合;
- 2)自定义属性集合变更记录。
自定义属性存储结构如下图所示:
针对这两份数据,应该提供两种查询接口,分别是查询全量数据和查询增量数据。这两种接口的组合应用可以极大提升聊天室服务的属性查询响应和自定义分发能力。
10.3自定义属性的拉取
内存中的全量数据,主要给从未拉取过自定义属性的成员使用。刚进入聊天室的成员,直接拉取全量自定义属性数据然后展示即可。
对于已经拉取过全量数据的成员来说,若每次都拉取全量数据,客户端想获得本次的修改内容,就需要比对客户端的全量自定义属性与服务器端的全量自定义属性,无论比对行为放在哪一端,都会增加一定的计算压力。
所以:为了实现增量数据的同步,构建一份属性变更记录集合十分必要。这样:大部分成员在收到自定义属性有变更来拉取时,都可以获得增量数据。
属性变更记录采用的是一个有序的 map 集合:key 为变更时间戳,value 里存着变更的类型以及自定义属性内容,这个有序的 map 提供了这段时间内所有的自定义属性的动作。
自定义属性的分发逻辑与消息一致:均为通知拉取。即客户端在收到自定义属性变更拉取的通知后,带着自己本地最大自定义属性的时间戳来拉取。比如:如果客户端传的时间戳为 4,则会拉取到时间戳为 5 和时间戳为 6 的两条记录。客户端拉取到增量内容后在本地进行回放,然后对自己本地的自定义属性进行修改和渲染。