ToB企服应用市场:ToB评测及商务社交产业平台

标题: 百万级群聊的操持实践 [打印本页]

作者: 民工心事    时间: 前天 15:05
标题: 百万级群聊的操持实践
作者:来自 vivo 互联网服务器团队- Cai Linfeng
 
本文先容了服务端在搭建 Web 版的百万人级别的群聊体系时,遇到的技术挑衅和解决思路,内容包括:通信方案选型、消息存储、消息有序性、消息可靠性、未读数统计。
 
一、弁言

 
如今IM群聊产品多种多样,有国民级的微信、QQ,企业级的钉钉、飞书,还有许多公司内部的IM工具,这些都是以客户端为主要载体,而且群聊人数通常都是有限制,微信正常群人数上限是500,QQ2000人,收费能到达3000人,这里固然有产品考量,但技术成本、资源成本也是很大的因素。而笔者业务场景上必要一个迭代更新快、轻量级(不依赖客户端)、单群百万群成员的纯H5的IM产品,本文将回顾实现一个百万人量级的群聊,服务器侧必要思量的操持要点,希望可以给到读者一些开导。
 
二、背景先容

 
不同的群聊产品,接纳的技术方案是不同的,为了理解接下来的技术选型,必要先相识下这群聊产品的特性。
 
三、通信技术

 
即时通信常见的通信技术有短轮询、长轮询、Server-Sent Events(SSE)、Websocket。短轮询和长轮询适用于及时性要求不高的场景,比如论坛的消息提醒。SSE 适用于服务器向客户端单向推送的场景,如及时新闻、股票行情。Websocket 适用于及时双向通信的场景,及时性好,且服务端、前端都有比较成熟的三方包,如 socket.io,以是这块在方案选择中是比较 easy 的,前后端使用 Websocket 来实实际时通信。
 
四、消息存储

 
群聊消息的保存方式,主流有2种方式:读扩散、写扩散。图1展示了它们的区别,区别就在于消息是写一次还是写N次,以及怎样读取。
 


图1
 
读扩散就是所有群成员共用一个群信箱,当一个群产生一条消息时,只必要写入这个群的信箱即可,所有群成员从这一个信箱里读取群消息。
长处是写入逻辑简朴,存储成本低,写入效率高。缺点是读取逻辑相对复杂,要通过消息表与其他业务表数据聚合;消息定制化处理复杂,必要额外的业务表;大概还有IO热门问题。
 
举个例子:
很常见的场景,展示用户对消息的已读未读状态,这个时间公共群信箱就无法满足要求,必须增加消息已读未读表来记录相关状态。还有效户对某条消息的删除状态,用户可以选择删除一条消息,但是其他人仍然可以看到它,此时也不恰当在公共群信箱里拓展,也必要用到另一张关系表,总而言之针对消息做用户特定功能时就会比写扩散复杂。
写扩散就是每个群成员拥有独立的信箱,每产生一条消息,必要写入所有群成员信箱,群成员各自从本身的信箱内读取群消息。长处是读取逻辑简朴,恰当消息定制化处理,不存在IO热门问题。缺点是写入效率低,且随着群成员数增加,效率低落;存储成本大。
 
以是当单群成员在万级以上时,用写扩散就明显不太合适了,写入效率太低,而且大概存在许多无效写入,不活跃的群成员也必须得有信箱,存储成本黑白常大的,因此接纳读扩散是比较合适的。
 
据相识,微信是接纳写扩散模式,微信群设定是500人上限,写扩散的缺点影响就比较小。
 
五、架构操持

 
5.1 团体架构

 
先来看看群聊的架构操持图,如图2所示:
 


图2
 
从用户登录到发送消息,再到群用户收到这条消息的体系流程如图3所示:
 


图3
 
 
5.2 路由计谋

 
用户应该连接到哪一台连接服务呢?这个过程重点思量如下2个问题:
 
保证平衡有如下几个算法:
 
5.3 重连机制

 
当应用在扩缩容或重启升级时,在该节点上的客户端怎么处理?由于操持有心跳机制,当心跳不通或监听连接断开时,就认为该节点有问题了,就实行重新连接;如果客户端正在发送消息,那么就必要将消息临时保存住,等待重新连接上后再次发送。
 
5.4 线程计谋

 
将连接服务里的IO线程与业务线程隔离,提升团体性能,原因如下:
 
5.5 有状态链接

 
在如许的场景中不像 HTTP 那样是无状态的,必要明白知道各个客户端和连接的关系。比如必要向客户端广播群消息时,首先得知道客户端的连接会话保存在哪个连接服务节点上,自然这里必要引入第三方中心件来存储这个关系。通过由连接服务主动上报给群组服务来实现,上报时机是客户端接入和断开连接服务以及周期性的定时任务。
 
5.6 群组路由

 
假想如许一个场景:必要给群所有成员推送一条消息怎么做?通过群编号去前面的路由 Redis 获取对应群的连接服务组,再通过 HTTP 方式调用连接服务,通过连接服务上的长连接会话进行真正的消息下发。
 
5.7 消息流转

 
连接服务直接接收用户的上行消息,思量到消息量大概非常大,在连接服务里做业务显然不合适,这里完全可以选择 Kafka 来解耦,将所有的上行消息直接丢到 Kafka 就不管了,消息由群组服务来处理。
 
六、消息顺序

 
6.1 乱序现象

 
为什么要讲消息顺序,来看一个场景。假设群里有效户A、用户B、用户C、用户D,下面以 ABCD 代替,假设A发送了3条消息,顺序分别是 msg1、msg2、msg3,但B、C、D看到的消息顺序不一致,如图4所示:


图4
 
这时B、C、D肯定会以为A在胡言乱语了,如许的产品用户必定是不喜欢的,因此必须要保证所有接收方看到的消息展示顺序是一致的。
 
6.2 原因分析

 
以是先相识下消息发送的宏观过程:
 
在上面的过程中,都大概产生顺序问题,简要分析几点原因:
 
 
6.3 解决方案

 
6.3.1 单用户保持有序

 
通过上面的分析可以知道,实在无法保证或是无法衡量不同用户之间的消息顺序,那么只需保证同一个用户的消息是有序的,保证上下文语义,以是可以得出一个比较朴素的实现方式:以服务端数据库的唯一自增ID为标尺来衡量消息的时序,然后让同一个用户的消息处理串行化。那么就可以通过以下几个技术本领配合来解决:
 
 
6.3.2 推拉联合

 
到这里基本解决了同一个用户的消息可以按照他本身发出的顺序入库的问题,即解决了消息发送流程里第一、二步。
 
第三、四步存在的问题是如许的:
A发送了 msg1、msg2、msg3,B发送了 msg4、msg5、msg6,最终服务端的入库顺序是msg1、msg2、msg4、msg3、msg5、msg6,那除了A和B其他人的消息顺序必要按照入库顺序来展示,而这里的问题是服务端考量推送吞吐量,在推送环节是并发的,即大概 msg4 比 msg1 先推送到用户端上,如果按照推送顺序追加来展示,那么就与预期不符了,每个人看到的消息顺序都大概不一致,如果用户端按照消息的id大小进行比较插入的话,用户体验将会比较奇怪,突然会在2个消息中心出现一条消息。以是这里接纳推拉联合方式来解决这个问题,具体步骤如下:
 
 


图5
 


图6
 
举例,图5表示服务端的消息顺序,图6表示用户端拉取消息时当地消息队列和提醒队列的变化逻辑。
 
 
通过推拉联合的方式可以保证所有效户收到的消息展示顺序一致。细心的读者大概会有疑问,如果聊天信息流里有本身发送的消息,那么大概与其他的人看到的不一致,这是由于本身的消息展示不依赖拉取,必要即时展示,给用户立刻发送成功的体验,同时其他人也大概也在发送,最终大概比他先入库,为了不出现信息流中心插入消息的用户体验,只能将他人的新消息追加在本身的消息后面。以是如果作为发送者,消息顺序大概不一致,但是作为纯接收者,大家的消息顺序都是一样的。
 
七、消息可靠性

 
在IM体系中,消息的可靠性同样非常紧张,它主要体如今:
 
 
7.1 消息不丢失操持

 
 
7.2 消息不重复操持

 
八、未读数统计

 
为了提醒用户有新消息,必要给用户展示新消息提醒标识,产品操持上一般有小红点、具体的数值2种方式。具体数值比小红点要复杂,这里分析下具体数值的处理方式,还必要分为初始打开群和已打开群2个场景。
 
已打开群:可以完全依赖用户端当地统计,用户端获取到新消息后,就将未读数累计加1,等点进去查看后,清空未读数统计,这个比较简朴。
 
初始打开群:由于用户端接纳H5开发,用户端没有缓存,没有能力缓存最近的已读消息游标,因此这里完全必要服务端来统计,在打开群时下发最新的聊天信息流和未读数,下面具体讲下这个场景下该怎么操持。
既然由服务端统计未读数,那么少不了要保存用户在某个群里已经读到哪个消息,雷同一个游标,用户已读消息,游标往前走。用户已读消息存储表操持如图7所示:
 


图7
 
游标offset接纳定时更新计谋,连接服务会记录用户最近一次拉取到的消息ID,定时异步上报批量用户到群组服务更新 offset。
该表第一行表示用户1在 id=89 的群里,最新的已读消息是id=1022消息,那么可以通过下面的SQL来统计他在这个群里的未读数:select count(1) from msg_info where groupId = 89 and id > 1022。但是事情并没这么简朴,一个用户有许多群,每个群都要展示未读数,因此要求未读数统计的程序效率要高,否则用户体验就很差,很明显这个 SQL 的耗时波动很大,取决于 offset 的位置,如果很靠后,SQL 实行时间会非常长。笔者通过2个计谋来优化这个场景:
 
 


图8
 
如上图8所示,每个群都会构建一个长度为100,score 和 member 都是消息ID,可以通过 zrevrank 命令得到某个 offset 的排名值,该值可以换算成未读数。比如:用户1在群89的未读消息数,'zrevrank 89 1022' = 2,也就是有2条未读数。用户2在群89的未读数,'zrevrank 89 890' = nil,那么未读数就是99+。同时消息新增、删除都必要同步维护该数据结构,失效或不存在时从 MySQL 初始化。
 
九、超大群计谋

 
前面提到,操持目标是在同一个群里能支撑百万人,从架构上可以看到,连接服务处于流量最前端,以是它的承载力直接决定了同时在线用户的上限。
影响它的因素有:
 
9.1 消息风暴

 
当同时在线用户数非常多,例如百万时,碰面临如下几个问题:
 
 
9.2 消息压缩

 
如果某一个时刻,推送消息的数量比较大,且群同时在线人数比较多的时间,连接服务层的机房出口带宽就会成为消息推送的瓶颈。
做个计算,百万人在线,必要5台连接服务,一条消息1KB,一般环境下,5台连接服务集群都是部署在同一个机房,那么这个机房的带宽就是1000000*1KB=1GB,如果多几个超大群,那么对机房的带宽要求就更高,以是怎样有效的控制每一个消息的大小、压缩每一个消息的大小,是必要思索的问题。
经过测试,使用 protobuf 数据交换格式,平均每一个消息可以节省43%的字节大小,可以大大节省机房出口带宽。
 
9.3 块消息

 
超大群里,消息推送的频率很高,每一条消息推送都必要进行一次IO体系调用,显然会影响服务器性能,可以接纳将多个消息进行合并推送。
主要思路:以群为维度,累计一段时间内的消息,如果到达阈值,就立刻合并推送,否则就以匀速的时间间隔将在这个时间段内新增的消息进行推送。
时间间隔是1秒,阈值是10,如果500毫秒内新增了10条消息,就合并推送这10条消息,时间周期重置;如果1秒内只新增了8条消息,那么1秒后合并推送这8条消息。如许做的好处如下:
 
 
十、总结

 
在本文中,笔者先容了从零开始搭建一个生产级百万级群聊的一些关键要点和实践履历,包括通信方案选型、消息存储、消息顺序、消息可靠性、高并发等方面,但仍有许多技术操持未涉及,比如冷热群、高低消息通道会放在将来的规划里。IM开发业界没有同一的尺度,不同的产品有恰当本身的技术方案,希望本文能够带给读者更好地理解和应用这些技术实践,为构建高性能、高可靠性的群聊体系提供一定的参考。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4