本文由网易云信资深服务器开发工程师曹佳俊分享,原题“深度剖析“圈组”消息系统设计 | “圈组”技术系列文章”,为了提升内容品质,本文有修订和删节。
1、引言
鉴于实时社群产品Discord在IM垂直应用领域的爆火,类似的需求越来越多,云信的“圈组”就是针对这种应用场景的技术产品。“圈组”产品发布后获得了很大的关注,很多云信用户在接入SDK的同时对于“圈组”的底层技术细节和原理也非常关注,为此我们决定推出“圈组”相关的技术文章,分享云信在“圈组”技术设计上的一些思考和实践。本文是序列文章的第2篇,将要分享的是云信的实时社群产品“圈组”(“圈组”云信的类Discord产品实现方案)的消息系统技术设计实践。
技术交流:
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)(本文已同步发布于:http://www.52im.net/thread-4321-1-1.html)
2、系列文章
本文是系列文章中的第 2 篇:
《实时社群技术专题(一):支持百万人超级群聊,一文读懂社群产品Discord》
《实时社群技术专题(二):百万级成员实时社群技术实现(消息系统篇)》(* 本文)
《实时社群技术专题(三):百万级成员实时社群技术实现(关系系统篇)》(稍后发布...)
3、作者介绍
曹佳俊:网易云信资深服务器开发工程师,毕业于中国科学院,硕士毕业后加入网易,负责云信 IM/RTC 信令等业务的服务器开发。专注于即时通讯、RTC 信令以及相关中间件等技术,是云信开源项目 Camellia 的作者。
4、“圈组”的技术特点
在介绍“圈组”的技术细节之前,我们先了解一下圈组的技术特点。
“圈组”产品最大的特点是什么?
1)首先:是 server/channel 的二级结构;
2)其次:是构建在二级结构之上的大规模社群(单个 server 数十万甚至上百万成员);
3)以及:使用复杂的身份组系统来管理如此规模的社群组织和成员。
那么对于这样一个新颖的 IM 系统,在技术上应该如何实现呢?
5、“圈组”和传统IM群组的技术差异
5.1概述
一种简单的思路是改造已有的 IM 系统,对于“圈组”这样的类 Discord 社群,第一个思路是拓展我们的群组功能,猛一看在很多方面确实挺像的。
我们做了个简单的对比:
从上面的表格可以看到,“圈组”和群组最大的不同:
1)是容量的区别;
2)是二级结构。
其他的诸如身份组、个性化推送策略,似乎只要适配的做一下就可以了。那么是不是只要想办法提升一下群组的容量,再在业务层封装一下二级结构就可以了呢?答案显然是否定的,或者至少说基于群组去扩展不是一个很好的想法。
5.2二级结构的差异
首先是二级结构。在类 Discord 的二级结构中,成员的管理在 server 层,而 channel 成员是继承自 server 的,而且在 channel 之上还有很多可见性的配置(我们的“圈组”提供了黑白名单机制,而Discord 则提供了查看频道权限)。在这种机制之下,任何 server 层面的成员变动,都可能影响全部或者部分频道的成员列表。面对这种复杂的结构,群组有两种思路去实现:1)一种是 N 个群,逻辑上隶属于同一个 server;2)一种是一个群映射为一个 server。不管哪种方式,先不说消息投递这块的逻辑,仅成员管理上逻辑的耦合和交织的复杂性,足以劝退任何人。
5.3容量的差异
常规IM群组的容量一般只有数百,最多可以扩展到数千。对于IM群组成员的管理,我们一般采取全量+增量同步相结合的方案,客户端和服务器映射到相同的群组镜像(群信息+群成员等)。此时很多操作,例如群成员的展示、检索,消息的艾特等,都可以基于纯客户端进行。而“圈组”要求几十万甚至上百万的容量,显然客户端无法一次性获取到所有成员,如果你一次性加入多个 server,那成员的数量将更加膨胀。因此在“圈组”这种大规模社群的设计中,很多逻辑都会转向云端,此时不管是 SDK 还是服务器,均需要修改原有的设计逻辑。
5.4消息规模差异
此外,大规模社群带来的是消息爆炸。在原有的IM群组设计中,假设一个人同时加入了 1000 个群,那么这 1000 个群内的所有消息均会在第一时间下发给给客户端。但是在一般的业务场景中,不会所有的群都同时活跃,假设这 1000 个群变成了 1000 个服务器/频道,作为一种社群组织,同时活跃的可能性将大大增加,而且每个服务器/频道的人数远远超过普通的群组,叠加之后带来的消息爆炸现象在原有的群组体系中将带来极大的压力。压力包括多方面:1)首先是海量消息的存储压力;2)其次是海量消息在线广播/离线消息推送带来的带宽和服务器压力;3)以及客户端在面对大量消息冲击时如何有效地接受和合理的展示。
5.5小结
除了容量、二级结构、消息规模,包括身份组、成员管理、个性化推送策略等等都存在巨大差异。是否真的适合在群组中添加这些复杂逻辑呢,强行绑定在一起会不会既没有一个好用的类 Discord 平台,也使得原始的群组功能繁杂,反而降低了易用性呢?经过上面的一些分析,我们基本可以得出一个结论:在已有的群组基础上扩展来实现一个类 Discord 功能的社群,显然不是一个很好的思路。那么还有其他“捷径”吗?IM聊天室也是一个潜在的选项,聊天室的一大特点就是支持超大规模同时在线(参见《千万级实时直播弹幕的技术实践》),容量似乎已经不是问题,但是当考虑添加其他一些强社交关系的特性时(如成员、身份组等)就显得有点为难了,聊天室本身就是来去自如的一个开放空间,这个和圈组的产品本身定位互相冲突的。因此基于聊天室扩展的方案也基本 pass 掉了。
6、“圈组”的技术难点
基于上述种种的思考和讨论,最终选择脱离已有 IM 体系,从零研发一套全新的社群方案“圈组”,“圈组”不是一个简单的 IM 功能,而是一套可以独立运行的 IM 系统。经过上面的讨论,相信大家对“圈组”本身的技术特点和难点也有所理解。可以归纳为以下几点:1)二级结构下成员无上限的社交关系系统设计;2)超大社群下消息系统设计;3)复杂高效的身份组系统设计;
7、“圈组”技术实现之整体架构
“圈组”整体架构:
上面展示了“圈组”服务整体的架构。可以看到整个“圈组”服务是一个分层的架构:1)首先是接入层,包括 LBS 服务和长链接服务器以及 API 网关,对应客户端 SDK 和用户服务器;2)后面是网络层,包括大网 WE-CAN 和协议路由服务;3)其次是服务层,划分了多个服务模块,每个模块都包括多个微服务;4)最后是基础设施。
8、“圈组”技术实现之消息系统架构
这其中和消息系统相关联的包括接入层、网络层、以及后端的登录/订阅/消息/检索等模块。基本架构如下:
消息系统中第一个要讨论的点就是消息的存储和分发方式,包括在线广播、离线推送、历史消息三个维度。下面几节我们将对消息系统中各模块分别展开介绍。
9、“圈组”消息系统技术实现1:在线广播
对于一般的IM群组来说,在线广播的一般过程是这样的:依次查询群组里的所有人的在线状态,如果在线,则将消息发送给对应的长链接服务器。显然这种机制无法复制到“圈组”,因为在“圈组”的一个服务器里可能存在超过 100w 的人。此外:IM聊天室的广播模式也不能直接复用,因为在聊天室架构中,每个长链接映射到一个聊天室,因此当你登录到某个聊天室的时候,你只会收到该聊天室的消息。而对于“圈组”来说,每个用户会同时加入多个服务器/频道,而且会同时收到多个服务器/频道的消息。
针对“圈组”的上述特点:我们设计了消息订阅模式,也就是用户登录之后,需要订阅感兴趣的相关服务器/频道,服务器会记录下这个订阅信息。当有新消息的时候,服务器通过订阅关系(而不是在线状态)查询到需要广播的列表,通过这种方式就不再需要遍历服务器/频道里的所有用户。但是当一个服务器/频道里在线人数非常多的时候,这个订阅关系仍然是巨大的。为此:我们设计了一种两层订阅模型,即所有的订阅关系会保存在长链接服务器上(QChatLink/QChatWebLink),同时长链接服务器会定时发送心跳给后端的订阅服务器,心跳信息相比原始的订阅信息会大大简化,比如长链接服务器上会记录账号 A 订阅了某个频道 A 的消息,如果有 1w 个账号,则有 1w 条订阅记录,而心跳信息里只会上报有 1w 个人订阅了某个频道 A 的消息,具体的账号列表则被精简掉了。当一条消息需要广播时,消息服务会访问订阅服务,获取到该服务器/频道被订阅的长链接服务器列表,并依次给该列表中的长链接服务器发送消息下发通知,长链接服务器收到通知后会根据订阅详情再广播给所有客户端。此外:我们还提供了多种订阅类型,当你非常关心某个频道消息时(比如页面正停留在该频道),此时你可以订阅该频道的消息。对于其他频道,如果你仅仅需要知道该频道有多少条未读消息(或者有无未读消息),则可以选择订阅该频道的未读计数(或者未读状态),此时服务下发时仅会广播精简的消息体用于维护客户端未读计数,并且当未读计数达到一定阈值之后(比如 99+),服务器可以选择不再下发任何通知消息而不影响用户体验。通过上文介绍的消息订阅模型,极大地提高了超大型的圈组频道/服务器消息在线广播的效率,降低了服务器压力。除此之外:我们还设计了针对小型频道的特殊策略,对于小型频道,即使不订阅,服务器也会下发消息通知给频道里所有人,从而减轻端侧消息订阅模型的维护成本。针对消息订阅机制本身,后续我们也会根据不同的业务场景,提供更多一站式的策略来帮助降低接入成本,提升整体的易用性。
10、“圈组”消息系统技术实现2:离线推送
在强社交的场景下,离线消息推送对于维持用户粘性+提升产品体验有很大的作用。从技术角度看的话,主要解决2个问题:1)第一个是超大型服务器/频道的消息推送的效率问题;2)另一个是提供足够丰富的推送策略来帮助 C 端用户,避免被过量的推送消息给打扰。针对第一个问题,我们针对不同规模的服务器/频道采取了不同的策略:1)对于小型频道:采用类似于群组的消息推送模型;2)对于大型频道:对于每一条需要推送的消息,会根据目标用户的 ID 进行任务分片,多个节点并行操作,提高推送效率。此外:分片会采用一致性策略,保证单个用户固定为某些节点,从而提高缓存命中效率。针对第二个问题,推送策略可以用以下几句话来描述:1)既关注促活,又保证不打扰;2)大型 server 是游乐场,只推送与用户相关的重要消息(如 @消息);3)小型 server 是与朋友相处的小天地,支持消息的全部推送。并且:未来用户还可以自定义消息的高低优先级,并搭配不同的推送配置(如不同的免打扰配置等),如下图所示。
11、“圈组”消息系统技术实现3:历史消息
历史消息的存储在“圈组”的场景中也需要一些特别的设计。同样以传统IM群组为例,一般来说消息的存储方式有两种,写扩散和读扩散。在小型的IM群组或者多人会话中,写扩散模式可以简化设计,但是当群组规模扩大到一定程度(如万人群),读扩散就成了选择。而对于“圈组”这种单个服务器可能上百万人的“群组”中,除了常规的读扩散之外,我们还设计了多级缓存的结构来应对海量的读请求。基本的存储架构大致如下:
消息的存储主要包括两部分:1)一部分是消息本身;2)一部分是未读计数。首先是写入:对于上述两者,我们都会使用中心化的缓存服务器来存储最近的数据,并使用异步+批量+聚合等手段,通过 MQ 异步落库,从而平衡写入效率(单条写入性能低)和写入读取延迟(异步写入有延迟)的问题,并且针对不同数据类型的特点,我们也选择了不同的存储方案(历史消息使用分布式时间序列数据库,未读计数使用分布式 k-v 数据库),最大化地提升消息存储和查询的性能和效率。有写就有读,针对读取操作:1)所有最近的消息和未读计数均会存储在中心化缓存中,并通过先进先出和缓存过期等不同的策略来确保缓存中存储的永远是最新和最热的数据;2)对于消息 ID 和消息内容本身,中心化缓存中也会有不同的数据结构和过期策略,来平衡缓存命中率和缓存容量消耗;3)当缓存过期了,如果有关联的读写请求,将会触发缓存的重建,以保证缓存的命中率始终保持在较高水位;4)当有高频的读请求,还会触发热点 cache 的检测,并将一部分读请求下沉到各个计算节点的内存中,以应对突发流量的冲击。上述针对“圈组”的特别设计,消息存储系统可以应对几十数百人的小型圈组频道,也可以从容应对上百万的超大型频道。
12、相关资料
[1] IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?
[2] 网易云信技术分享:IM中的万人群聊技术方案实践总结
[3] 企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等
[4] 融云IM技术分享:万人群聊消息投递方案的思考和实践
[5] 微信直播聊天室单房间1500万在线的消息架构演进之路
[6] 百万人在线的直播间实时聊天消息分发技术实践
[7] 千万级实时直播弹幕的技术实践
[8] 深度解密钉钉即时消息服务DTIM的技术设计
[9] 深度揭密RocketMQ在钉钉IM系统中的应用实践
[10] 一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等
[11] 一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等
[12] 从新手到专家:如何设计一套亿级消息量的分布式IM系统
(本文已同步发布于:http://www.52im.net/thread-4321-1-1.html)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。