logo

百度直播消息服务架构实践

作者:沙与沫2021.04.27 16:14浏览量:422

简介:直播业务的核心功能有两个,一个是音视频推拉流,另一个是直播间信息流的收发。

导读:直播业务的核心功能有两个,一个是实时音视频推拉流,另一个是直播间消息流的收发。本文主要介绍百度直播服务内的消息服务系统的设计实践和演化。

一、背景

直播间内用户聊天互动,形式上是常见的IM消息流;但直播消息流不仅仅是用户聊天。除用户聊天外,直播间内常见的用户送礼物、进场、点赞、去购买、主播推荐商品、申请连麦等互动行为的实时提醒,也是通过消息流下发的。此外,直播间关闭、直播流切换等特殊场景,也依赖消息流的实时下发。消息流可以认为是直播间内主播与用户间实时互动和直播间实时控制的基础能力。

二、直播消息1.0 - 奠定基础

如何构建直播的消息系统,又有哪些挑战需要解决,我们来梳理一下。

2.1 直播消息场景分析

直播间内聊天消息,经常被类比于群聊。群聊是大家比较熟悉的即时通讯场景,直播间内聊天和群聊,二者有相似性,但也有本质的区别。

对比二者的特点:

同时参与人数不同:群聊的参与人数上千人就是很大的群了;但对于高热度的大型直播场景,例如国庆、阅兵、春晚等,单直播间累计用户是百万甚至千万量级的集合,同时在线人数可达数百万人。
用户与群和直播间的关系不同:用户进群退群,是相对低频的操作,用户集合相对固定,用户进出的变更频度不会特别高;而用户进出直播间,是非常频繁的,高热度直播的单直播间每秒面临上万用户的进出变更。
持续时间不同:群聊建立后,聊天持续时间可能比较长,几天到数月都有;而直播间大部分持续不超过几个小时。

根据以上分析,提炼出两个核心问题:

问题一:直播间内用户的维护

单直播间每秒上万用户的进出变更;实际进入直播间峰值不超过2万QPS,退出也不超过2万QPS。
单直播间同时数百万用户在线
单直播间累计用户达千万量级

支持在线百万、累积千万两个集合,每秒4万QPS更新,有一定压力,但有支持高读写性能的存储应该可以解决,例如redis。

问题二:百万在线用户的消息下发

面对百万在线用户,上下行都有大量的消息,从直播用户端视角分析:

实时性:如果消息服务端做简单消峰处理,峰值消息的堆积,会造成整体消息延时增大,且延时可能产生很大的累积效应,消息与直播视频流在时间线上产生很大的偏差,影响用户观看直播时互动的实时性。
端体验和性能:端展示各类用户聊天和系统消息,一般一屏不超过10-20条;如果每秒有超过20条的消息下发,端上展示的消息基本会持续刷屏;再考虑到有礼物消息的特效等;大量的消息,对端的处理和展示,带来持续高负荷。所以,对于一个长时间观看直播的用户端来说,如果出现持续的大量消息,端的消息消费会有显著的性能压力,且过多消息会有累积效应。

由于问题一不难解决,以下主要讨论问题二。

2.2 直播消息设计目标

综合考虑直播业务场景,对于消息服务的需求目标如下:

实时性方面,端和端的消息要达到秒级;
性能方面,消息服务能支持同一直播间内百万以上用户同时在线下发;
而对于峰值的过多消息,丢弃是合理适当的处理方式;
基于合理的端用户体验,单直播间内每秒消息数假设不超过N条。

现在,问题的核心是,如何做到把不超过N条的消息,在S秒内,下发到直播间内的百万用户,假设N<=20,S<=2。

2.3 普通群聊压力分析

2.3.1 普通群聊消息收发分析


△图1:群聊数据流及压力点
首先,具体分析一下普通群聊的消息收发流程:

  1. 对于群group-1,分配一个群公共消息信箱group-mbox-1;
  2. 群group-1内的用户user-1,由手机端APP-1上发出消息msg-1;
  3. 服务端接收到消息msg-1,检查user-1是否有权限,如有权限,将msg-1存储到群信箱group-mbox-1,生成相应msgID-1;
  4. 服务端查询group-1对应的用户列表groupUserList-1;
  5. 基于groupUserList-1拆分出所有独立群用户:user-1、user-2。。。user-n;
  6. 对于每一个用户user-i来说,需要查询用户user-i的所在设备device-i-1、device-i-2、device-i-m(因为一个账号可能登录多个设备);
  7. 对于每个设备device-i-j来说,长连接通道都会建立一个独立的长连接connect-j以服务于该设备;但由于connect-j是由端上APP-1连接到长连接服务的,具有动态性,所以,查询device-i-j与connect-j的对应关系时,需要依赖一个路由服务route来完成查询;
  8. 在查得connect-j后,可以通过connect-j下发msg-1的通知groupmsg-notify-1;
  9. 如果用户user-i正在使用device-i-j的手机端APP-1,用户user-i就可以立即从长连接connect-j上收到msg-1的通知groupmsg-notify-1;
  10. 在接收到groupmsg-notify-1后,手机端APP-1中的消息SDK根据端本地历史消息记录的最后一条消息latestMsg对应的消息ID即latestMsgID,来向服务端发起拉消息请求fetchMsg,拉取group-1中从latestMsgID+1到最新的所有消息;
  11. 服务端收到拉消息请求fetchMsg后,从group-mbox-1中取出latestMsgID+1到最新的所有消息,返回给端;如果消息过多,可能需要端分页拉取;
  12. 端APP-1拉取到group-1中从latestMsgID+1到最新的所有消息,可以做展示;在用户在会话中阅读后,需要设置所有新消息的已读状态或者会话已读状态。

2.3.2 普通群聊主要压力

如果完全重用普通群聊消息的下发通知到端拉取的全过程,对于user-1发的一条消息msg-1,如果需要支持一个实时百万量级的群消息,大概有以下几个每秒百万量级的挑战:

首先,秒级拆分出用户列表groupUserList-1,需要秒级读出百万的用户列表数据,对于存储和服务是第一个百万级挑战。

第二,对于拆分出群中的所有独立用户user-i,需要秒级查询出百万量级的device-i-j,对于存储和服务是第二个百万级挑战。

第三,对于所有device-i-j,通过动态路由服务route,需要秒级查询出百万量级的connect-j,对于存储和服务是第三个百万级挑战。

第四,对于通过长连接connect-j下发时,需要支持秒级下发百万量级的群消息通知groupmsg-notify-1到对应的connect-j上,对于长连接服务是个百万级的挑战。

第五,对于收到消息通知的所有端APP-1,需要支持百万QPS端从服务端拉取消息请求fetchMsg,对于消息信箱服务,这是也是一个百万量级的挑战;考虑到实际各端latestMsgID可能不同,可能的优化方式会更复杂一些,带来的性能影响会更大。

第六,如果在绝大多数用户是在线聊天的场景,设置已读状态也会有百万量级QPS对服务端的压力。

显然,完全重用群聊的消息流程,对消息服务和长连接服务带来的压力是巨大的。

2.3.3 普通群聊优化方案


△图2:群聊数据流优化后压力点
现在,我们来分析以上每个百万量级的挑战,是否有优化的空间。

对于①拆分用户列表和②查询用户对应设备,如果存储上将二者合并集中起来,也就是优化直播间内用户列表的存储,扩展设备信息,可以减少一次user->device的百万QPS查询,可以优化。

对于④下行通知和⑤端拉取fetchMsg的可靠消息拉取模式,考虑到直播消息允许部分折损丢弃,可以只做单向消息下发,而不做拉取,对于大部分连接保持在线的用户,也是可以接受的。所以可以优化,只保留下行通知(包含消息体),而舍弃端拉取。

对于⑥消息设置已读,直播场景下可以考虑简化舍弃。

如上优化后,减少了②⑤⑥三个百万量级压力请求,但还有①拆分用户列表③动态路由查询④长连接下发,这三个百万量级步骤需要处理。

对于①拆分用户列表,支持百万量级用户列表查询,比较常规的思路是支持基于群groupID的批量查询,例如一次可以查出100个用户,1万QPS查询就可以支持到百万;基于群groupID把用户数据的存储,分散到多个主从实例和分片上,控制好打散粒度不出现热点,基本能做到,只是存储资源可能消耗较多。

对于③动态路由查询,表面上看,面临的问题与①类似,但却有些不同。因为群的用户列表,是基于群groupID做key,建立一个表或多个打散的表;而device-i-j的查询是完全分散的,也是需要批量查询能力,但是完全分散的设备信息查询,不能只针对特定key做优化,需要动态路由服务支持整体上达到百万QPS的查询性能。

对于④长连接服务下发,由于长连接服务不依赖外部的存储服务,如果整体要支持百万量级的下发能力,若长连接单实例能支持1万的下发能力,整体上100个实例就能支持到百万量级下发。

基于以上分析,支持百万量级的消息下发,初见曙光。似乎只要优化好用户列表、动态路由的存储/查询和长连接的容量扩容,但所有的前提是需要消耗大量存储和机器资源。
考虑到直播业务的实际情况,现实不容乐观:

一方面,平时没有热点直播时,可能单场直播并发在线用户数峰值不超过1万人,甚至不到1000;在业务初期,整体直播在线用户峰值可能也不超过10万。这就意味着,为了支持百万量级的峰值,资源整体上有几十倍的冗余。

另一方面,如果突然来了一场热度非常高的直播,可能需要支持的不只是100万量级消息下发,可能是500万以上的量级(例如国庆阅兵、春晚等)。这样的话,每次大型直播得提前预估可能的在线用户峰值,如果超过当前设计容量,需要对①用户列表③动态路由查询④长连接服务,分别扩容和压测;或者在可接受的情况下,做服务降级或拒绝服务。

而实际上,在线用户峰值量级很难估计准确,这样会造成实际资源利用率很低,扩缩容的操作频繁,运维成本高。是否选择这个方案,也是很令人纠结。

2.3.4 普通群聊多群组方案

也有人提过拆分多个群组的方案,例如,如果一个群组最多支持1万用户,开100个群就可以支持一百万用户;再建立一个虚拟群,将这100个群关联起来,似乎可行。

但如果仔细分析,会发现以上提到的几个问题①拆分用户列表③动态路由查询④长连接下发,高压力依然存在,还是不可避免。

除此之外,多群组还会引入其他问题:

问题一:多群组消息不同步。如果两个用户在一起看直播,而所属群不同,看到的消息会完全不同。

问题二:直播场景用户是动态进出的,也就是说群组成员非常不稳定,在线用户峰值波动也比较大。如果是根据在线人数增长,动态新开群组,可能第一个群用户已经很多了,第二个群刚开始用户比较少;或者,在峰值期间开了比较多的群,随着热度降低用户离开,用户变得分散,一些群的用户可能较稀少,聊天互动较少,这时需要缩容合并群。如何平衡多个群的用户,达到好的业务效果,也是比较难做的。

基于以上分析,我们并没有选择多群组方案。

2.4 组播mcast方案

支持实时高并发百万量级同时在线用户的直播消息架构,组播mcast方案的提出及演化。

2.4.1 跳出原有框架思考

是否要采用以上基于群聊的优化方案,还是可以另辟蹊径?

先暂时抛开群收发消息流程,对于消息下发来说,如果一定要说一个步骤是必不可少的,那一定是长连接下发这步了。没有通过长连接下发,消息就无法最终到达用户;当然有人说轮询拉取也可以替代长连接下发,来获取消息,但显然轮询拉取的性能压力和实时性与长连接下发相比差很多,故不在讨论范围。

如果能简化为,给长连接服务下发消息时指定一个类似的groupID,长连接服务能直接拆分到所有群组用户相关的长连接connect-j,就可以省略掉用户列表拆分和动态路由查询的百万量级查询。

这样的话,消息下发的压力将主要由长连接服务来承受,服务端也不需要对多个系统扩容,直播消息的优化可能会大为简化。

根据这个思路,相当于在长连接服务中,对连接connect也建立群组的概念。基于连接组的设想,我们设计了一套长连接的组播mcast机制。

2.4.2 长连接组播mcast基本概念

每个长连接组播mcast有全局唯一的标识mcastID。
长连接组播mcast支持创建、删除、修改、查询等管理操作。
长连接组播mcast是若干长连接在线用户的连接connect的集合。
一个用户user-i在设备device-i-j上,对于特定应用APP-k来说,建立唯一的一个长连接connect-j-k;(此处暂时不区别登录用户和非登录用户)。
长连接组播mcast与组内长连接connect-j-k的关系维护,不需要额外的独立存储,是维护在每个长连接服务的实例上。

2.4.3 长连接组播mcast的路由概念

组播mcast-m的路由route-m,是一个长连接服务实例的集合LcsList,记录了所有加入mcast-m的长连接connect-i所在长连接服务实例lcs-j。

2.4.4 长连接组播mcast路由的记录维护

加入组播mcast:

客户端调用消息sdk加入mcast-m。
消息sdk通过长连接,发出上行请求mcastJoin(mcast-m)。
业务层收到来自长连接实例lcs-i上的连接connect-i的mcastJoin请求,校验mcast-m的合法性。
业务层请求路由层建立基于组播mcast-m的组播路由mcastRoute-m,将长连接实例lcs-i加入组播路由mcastRoute-m中。
业务层请求长连接服务层,请求mcastJoin所在长连接实例lcs-i,将请求所在连接connect-i加入到mcastConnectList-m中。

离开组播mcast,与加入组播mcast基本类似,由客户端调用消息sdk离开mcast-m,发出上行请求mcastLeave(mcast-m),长连接服务端更新路由和mcastConnectList-m信息。

2.4.5 组播mcast消息推送


△图3:组播mcast数据流及压力点
基于组播mcast的长连接消息推送过程,是一个1:M * 1:N的扩散放大过程,具体过程描述如下:

  1. 一条消息msg-1推送,目的地是ID为mcast-m组播;
  2. 后端业务模块根据目的mcast-m,做一致性hash选择出mcast路由分发模块实例mcastRouter- i,发送msg-1到mcastRouter-i;
  3. mcast分发路由模块实例mcastRouter-i,根据mcast-m的组播路由mcastRoute-m,查找所对应的接入实例路由记录列表mcastLcsList-m,拆分出mcast-m所有的长连接接入实例lcs-1..lcs-M,分别并发发送msg-1到长连接实例上;
  4. 一个长连接服务实例lcs-j,收到消息msg-1推送后,根据组播mcast-m查找组播连接列表mcastConnectList-m,查出mcast-m内所有的连接connect-m-1..connect-m-N,并发推送msg-1到消息客户端sdk-m-1..sdk-m-N;
  5. 消息客户端sdk-m-o收到msg-1后,递交给上层业务(例如直播sdk)。

2.4.6 组播mcast机制的性能评估

现在分析一下以上的组播mcast机制的性能压力:

  1. 组播mcast的路由维护,主要压力在于mcastJoin和mcastLeave,而Join的量级峰值请求很难超过2万qps;访问压力比百万低两个数量级。
  2. 组播mcast的消息推送流程,在一级路由mcastRoute拆分到长连接实例时,一般在几十到百量级,成本很低。
  3. 组播mcast在长连接单实例内的消息推送,是单进程内的多连接并发发送,经优化后线上实测,在单实例保持25W长连接的情况下,单实例压测可达8Wqps的mcast稳定下发,保守按5Wqps容量评估;多个长连接实例间,是完全的并发,可以较容易的水平扩容。
  4. 综上可知,对于100Wqps的下发,20个长连接实例就可以完全负荷(20*5W=100W),且有一定裕量。如果500Wqps的下发,也不超过100实例;1000W的下发,如果以8W单实例较大的负荷承载,125实例就可以支持。

相关文章推荐

发表评论