作为一家泛娱乐公司,语音业务是比心最重要的业务之一,而其中最核心的场景就是基于语音房技术实现的聊天室产品。相比于一般的业务系统,语音房业务系统的技术难度会更高一些,其难点主要体现在以下三个方面:
技术栈复杂,除基于http接口实现的webserver技术外,语音房核心技术还包括基于长连接的IM技术,和基于音视频通信的RTC技术。
强实时要求,需要支持房间内用户多对多互动的场景,一般房间内所有用户对事件变更和信息刷新的感知,需要在一秒内完成。
单房间高并发,聊天室承载了大量的活动,单房间人数从几个人到几万人不等,且在活动期间突然涌入的用户量极大,技术上需要在流量波动的情况下保证业务系统的正常运转。
18年我刚加入公司时,比心语音房业务架构如下:
APP客户端除了实现语音房业务逻辑外,还集成了云信SDK以实现聊天室消息发送功能,集成了ZEGO SDK和TRTC SDK,实现聊天室多人实时连麦功能。聊天室收发消息都是通过云信SDK直接与云信服务器通信完成消息广播,连麦推拉流功能则是通过云商SDK和云商服务之间的音视频流传输来完成。
服务端整体部署在阿里云上,负载均衡采用阿里云SLB服务,业务网关和服务部署在ECS层,底层数据存储使用阿里云rds和redis服务。服务端发送的房间消息会通过调用云信http接口实现,客户端房的消息、推流地址会通过云商回调的方式来回传给业务服务备份记录。
虽然初版的语音房系统足以支持业务早期的需要,但当业务流量上涨,尤其是对单房间进行站外推广,或举行大型比赛时,这套架构很快就会遇到容量瓶颈,用户很容易就能感知到包括服务卡顿、功能异常、声音断断续续、消息公屏不刷消息、手机过热等等一系列问题。
为了解决系统容量,尤其是单房间用户量的限制,我们启动了万人聊天室项目,对系统架构进行了大规模的改造升级。
比心平台会定期举办“我是歌王”歌手大赛,在万人聊天室建设之前,每次活动都会出各种各样的问题,以下截图是19年S1赛季决赛当时的情况,可以看到决赛房间在线只有几百人的情况下就被挤爆了,无论是观众还是工作人员都无法正常使用。
提高系统容量,要从问题分析入手,问题的分类大致可以分为两类,内部问题和外部问题。内部问题主要是由内部系统本身存在的瓶颈引起,如线程池阻塞、慢SQL、缓存设计问题、异常处理等等;外部问题是指依赖于云商服务引起的问题,主要是消息丢弃和RTC稳定性。
而万人聊天室系统建设,其本质是对语音房系统的全链路资源做高并发场景下的高可用改造,核心思路是:
对架构中的每个资源做水平扩展评估
对于预期应该可水平扩展的资源,需要改造后具备快速水平扩展能力
对于预期无法水平扩展的资源,需要做好三点:第一,根据业务目标提前规划好资源容量;第二,改进资源的使用方式,提升依赖资源的服务性能;第三,控制好资源使用流量上限
按照这个思路,我们对语音房系统各环节进行评估和主要治理方案如下:
下面我们着重介绍几个核心改造事项。
我们使用的redis服务是阿里云redis单机服务,性能存在瓶颈,实际使用经验是CPU 100%时,请求的QPS大约是10W,同时带宽只有最大几十兆,对稍大一些的缓存数据是无法支撑太高的访问量的。
由于聊天室强实时的要求,我们对缓存的访问量非常大,当单房间人数增长时对缓存的请求量也会急速增长。原本系统中还有些对缓存使用不合理的地方,如循环访问、集合类型、无法回源等。下图是当时的某次活动,集群化改造还未完成,单房间达到5000人时的redis cpu监控:
为了将单机redis迁移到集群模式,我们经历了四步走:
可回源:首先进行缓存可回源改造,避免迁移集群、扩容集群时丢失缓存导致影响了系统的正确性。
改用法:然后要对使用方式改造,如用pipeline合并循环请求,用string类型代替集合类型缓存。
去热点:单房间易形成热点缓存,要对这部分热点缓存进行特殊限制,如对极高频访问的个别接口做1s本地缓存等。
迁集群:完成以上三步,即可将redis单机替换为集群版,至此既能保证系统的正确性,又提升了依赖缓存提供的服务性能。
改造后在单房间万人场景下,redis总体qps达到15万,集群单机最高CPU使用率和带宽占比均在25%以下:
对于IO密集型系统,实现高并发的关键之一,是基于存储实现的功能是否满足高并发要求,众所周知核心数据服务(我们使用mysql)一般会是高并发的瓶颈点,而我们之前系统的一个主要问题,就是mysql数据访问类,会被任意功能的service类引用,各功能对数据访问的方式不统一,高并发设计难以落地。
代码规范治理的核心,就是通过分层设计,消除水平依赖、封装数据操作类,单个数据操作类只能被一个service引用,由service来实现功能高并发要求,这样就能确保单功能模块满足高并发,上层逻辑类manager和controller可以根据需求通过service任意组装数据,不用担心有破坏高并发的风险。
随着业务迭代,房间信息越来越多,渲染房间内容的接口对下游的IO请求也越来越多,IO请求扩散情况无法得到有效控制,核心接口稳定性也会下降。下图是获取房间信息接口的某个调用链路,可以看到已经有多达二十几次的IO请求了,我在两年前梳理这个接口时,完整路径的IO依赖就已经超过了二十个:
应对不断增加的需求,常规方案是在接口中不断添加新的服务或数据依赖,IO扩散不可避免地会进入线性增长模式,如下图:
解决思路是采用读写分离的思路,将接口返回值进行分类,房间基础信息保持不变,扩展信息可以通过专门的扩展字段来存储,信息变更时主动写入,这样在读多写少的场景下可以有效降低IO扩散问题:
当一个服务过于庞大时,不但会影响研发效率和维护成本,还会极大增加出现全局故障的风险。因此我们对业务服务进行了领域拆分,解耦非相关功能以提升可维护性和研发效率;独立服务和存储资源以降低线上事故影响范围;沉淀基建系统以保障关键功能的稳定性和业务扩展性。改造情况如下:
对音视频云商的考量,主要分为两方面:质量和成本。质量包括:语音推拉流的成功率、卡顿情况、是否有配套的监控、平均延迟情况、声音的音质如何、在不同地区使用的情况。成本一般包含:实时音视频成本、CDN带宽成本、录制和文件存储成本等。除了这些“硬指标”外,判断一家云商是否应该接入还要考虑云商支持的情况,如售后服务、技术支持、问题响应、快速修复等方面表现如何。
比心在语音房场景下的音视频能力建设,主要包括:
RTC转推能力:以RTC推流转推CDN为基础,提供播放聊天室语音能力,有效降低成本,提升万人聊天室音视频功能稳定性。常用场景包括麦下听众拉流、列表页不进房播放等。
房间混流能力:将语音房多路推流混为一路流,并能支持音视频同时混流,有效降低用户拉流带宽,降低成本,解决麦下拉流音画同步问题等。常用场景包括:万人聊天室听众拉流、KTV音画同步演唱方案等。
灾备降级能力:支持双云商互备,拉流模式热切,粒度可控制到单房间,确保语音推拉流遇到突发问题时可降级。
录制和查询:支持云商和自建录制,确保音频内容可回溯,可通过自建录制降低成本。
其中,解决万人聊天室拉流稳定性和成本的最主要手段,是RTC转推和房间混流两项能力。
由于聊天室场景是用户N对N互动,单房间人数的增多会引起房间消息量指数级的上涨,现有架构存在的问题有:
客户端发送消息量过大,IM云商对客户端发送的消息无限制,房间消息超频会导致消息丢失。
非发言类消息(如申请上麦、发言光圈等)没有合理控制,消息量过大导致发言类消息(公屏文字、表情等)被大量丢弃,表现为公屏消息不刷新。
IM云商会根据所有房间的消息量进行超频丢弃,因此一个房间消息量超频,会导致所有活跃房间受到影响。
解决思路是在语音房架构上增加一层消息中心,具体步骤为:
收归客户端直发消息,统一走服务端投递给云商服务,避免云商不可控丢消息。
消息中心统一接收客户端、服务端消息,在投递云商服务前决定消息是否发送。
收归其他依赖云商IM实现的功能,将云商服务作为纯粹的底层长连通道使用 。
消息发送通道设计如下:
消息等级定义如下:
单房间每秒频控20条消息,其中高等级消息每秒频控10条消息,为了优先保证高等级消息,普通消息达到频控而高等级未达到时,高等级消息可以继续发送,极端情况下单房间每秒最多30条消息(10高等级消息+20普通消息)。
除了构建消息通道、限制房间消息吞吐量外,治理与监控也是消息中心重要的基础设施,主要包含几个能力:
消息白名单:高等级、中高等级消息白名单配置,用以保护高等级通道不被滥用。
消息全链路查询:获取消息发送各个环节,快速定位单个消息流向。
消息监控告警:基于阿里云sls服务,支持自定义查询和生成监控大盘和报警配置。
消息关键数据日报:高等级消息频控、总消息量波动等汇总。
万人聊天室项目主要完成了容器化改造、业务拆分、服务性能优化、数据存储层改造、消息中心建设这几方面的工作,升级后的架构如下:
完成升级后的聊天室产品,基本上解决了系统容量和可用性问题,最高峰承载了单房间2.6万人同时在线。2020年起平台不断签约明星入驻歌王比赛,以万人聊天室架构为基础的语音业务系统平稳支撑了所有活动。
聊天室作为一项有着高付费强互动特点的产品,最吸引用户的体验就是丰富有趣的礼物特效,也包括更高价值、更加稀有的礼物,因此用户送出礼物后展示礼物特效的功能,一直是我们最需要保障的线上核心功能之一。然而在两年前,礼物消息丢失问题一直是困扰我们的一个难题,bug一直报又不容易定位,个案问题居多,偶尔有高价值礼物特效丢失问题报过来,我们还需要额外给用户发放补偿,给业务带来损失。
礼物消息为什么会丢?这要从整个消息链路来分析。
代码有bug,消息没法出去。这一问题由于出自我们内部系统,相对容易排查,也比较好做兜底降级。
消息发出去了,但没投递成功。产生这一问题有两种可能:云商对消息体大小有限制,我们发送的消息体过大了会导致发送失败;云商服务端接口有频控限制,可能某一段时间消息超频导致了丢弃。这个问题一般不会做降级处理,但可以监控。
消息发出去了,投递成功了,但云商服务端没法给用户。这一问题产生是由于云商服务端内部实际也有限流和频控的策略,可能会丢消息。一旦发生就很难排查,没有办法兜底。
消息发出去了,投递成功了,发给用户了,但客户端没收到。由于客户端网络异常,长连接闪断是可能存在的,一旦发生就更难定位问题,也没有办法兜底。
客户端收到了,但消息逻辑没有正确执行。这个问题可以在端上加打点来排查问题,修复客户端bug来解决。
可以看出,1和5是内部系统问题,比较容易解决,而2-4走外部服务,没有办法做太多降级措施。如之前消息中心建设的思路,外部服务我们尽量简化逻辑,只当做底层长连接通道来使用,而要保证我们的重要消息发送到需要接收消息的客户端上,一个合理的思路是参考单聊和群聊IM,给重要用户增加消息ACK机制,并通过自建长连接通道发送,方便针对不同用户定制消息体。
消息必达方案的流程如下:
下图是我们自建长连接消息协议的设计:
我们基于netty实现了一套长连接服务,命名为Mercury,单台服务可以支持20万连接稳定运行,客户端随机选择节点建连,聊天室消息广播到所有节点,实际运行下来线上高峰期消息吞吐量可以达到10000+条/秒。整体架构如下:
基于自建长连接的消息中心,还有更多收益:
具备更大的消息吞吐量:避免了云商的接口限制,单房间消息频控由20条/s,提高到60条/s。
更准确的在线状态:区别于使用云商的定时离线校准,基于长连通道状态检测,业务可以实时获取用户建连和断开事件。
更灵活的消息定制:基于私有协议可以加入更多功能,如房间内点对点消息,有ack机制可以保障消息必达。
更低的成本:单台服务器20W连接稳定运行,每月节约15W以上。
集成了自建IM的整体架构,更新如下:
随着公司向泛娱乐社交方向转型,公司内部很多业务对语音房的需求越来越多,但从头搭建一套语音房系统,技术成本过高,由于其复杂的技术栈开发过程中会碰到各种困难。例如:
RTC、IM云商如何选择和接入?
如何准确获取用户在线状态?
连麦质量差如何优化?
语音、消息长连如何后台保活?
...
为了能更好地支持公司业务,我们基于语音房业务积累的经验,沉淀了一套可以快速支持业务搭建语音房产品的通用解决方案,整体设计思路如下:
新的基建系统命名为sona,sona的整体架构如下:
业务可以通过sona提供的标准接口来接入音视频和IM能力,搭建一个语音房产品只需要一周时间即可上线,目前在公司内部已经有小游戏、直播、K歌房、游戏开黑房等多个业务场景正在使用sona。语音房业务也基于sona进行了架构改造,大大提升了稳定性和研发效率。
最后,介绍一下我们目前在做的事情:sona开源。目前sona已经在比心的github仓库上开源,仓库地址:
https://github.com/BixinTech/sona
欢迎你访问我们的项目,有任何想交流的想法可以留言联系我们。