Jack Jiang

我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
posts - 467, comments - 13, trackbacks - 0, articles - 0

本文由融云技术团队原创分享,原题“技术实践丨万人群聊的消息分发控速方案”,为使文章更好理解,内容有修订。

1、引言

传统意义上的IM群聊,通常都是像微信这样的500人群,或者QQ的2000人群(QQ有3000人群,但那是单独收费的,也就意味着它并非无门槛标配,能用上的人并不多)。

自从国外某号称“世界上最安全的IM”搞出万人群聊之后,万人群迅速被国内的使用者们接受。伴随着移动互联网的发展,即时通讯服务被广泛应用于各个行业(以经不再局限于传统IM社交应用领域),随着业务快速发展,传统百人、千人上限的群聊已经无法满足很多业务场景需求,所以万人甚至十万人的超大群也算是相伴而生、顺应潮流。 

▲ “纸飞机”的万人群(开发人员颤抖中...)

IM群聊一直是IM应用中比较有难度的热点技术之一,通常意义的群聊,无非就是500人群、1000人群、2000人群这样,技术实现上比单聊要复杂不少。然而对于万人群聊(甚至十万人群聊)来说,相比百人、千人群聊,技术实现上那几乎是另一个技术维度的事情,难度要高很多。

本文根据融云技术团队的实践经验,总结了万人群聊消息投递方案的一些思考和实践,希望能给你带来启发。

学习交流:

- 即时通讯/推送技术开发交流5群:215477170 [推荐]

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK

(本文同步发布于:http://www.52im.net/thread-3687-1-1.html

2、相关文章

万人群聊有关的技术文章还可读一读以下这篇:

  1. 网易云信技术分享:IM中的万人群聊技术方案实践总结
  2. 企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等
  3. 阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处

融云技术团队分享的其它文章:

  1. 融云技术分享:融云安卓端IM产品的网络链路保活技术实践
  2. 融云技术分享:全面揭秘亿级IM消息的可靠投递机制
  3. 融云技术分享:基于WebRTC的实时音视频首帧显示时间优化实践
  4. IM消息ID技术专题(三):解密融云IM产品的聊天消息ID生成策略

3、超大群面临的技术挑战

与百人群、千人群相比,万人、甚至十万人超大群,大幅提升了群的触达人数,对于很多业务场景来说,好处不言而喻。

然而单群成员如此之大,给 IM 系统的流量冲击非常巨大,技术难度可想而之。我们先来分析一下超大群的技术挑战。

以一个万人群的模型为例:

  • 1)如果群中有人发了消息,那么这条消息需要按照 1:9999 的比例进行分发投递,如果我们按照常规消息的处理流程,那么消息处理服务压力巨大;
  • 2)消息量大的情况下,服务端向客户端直推消息的处理速度将会成为系统瓶颈,而一旦用户的消息下发队列造成了挤压,会影响到正常的消息分发,也会导致服务缓存使用量激增;
  • 3)在微服务架构中,服务以及存储(DB,缓存)之间的 QPS 和网络流量也会急剧增高;
  • 4)以群为单位的消息缓存,内存和存储开销较大(消息体的存储被放大了万倍)。

基于这些技术挑战,要想真正达成超大群的技术目标,势必要做特定的技术优化来应对。

4、一般群聊的消息投递模型

先来看看普通群聊的消息投递模型。

我们的普通群聊消息投递模型如下图所示:

如上图所示,当用户在普通群里发了一条消息后,投递路径是:

  • 1)消息先到群组服务;
  • 2)然后通过群组服务缓存的群关系,锁定这条消息最终需要分发的目标用户;
  • 3)再根据一定的策略分发到消息服务上;
  • 4)消息服务再根据用户的在线状态和消息状态来判断这条消息是直推、通知拉取还是转 Push,最终投递给目标用户。

普通群聊的消息投递,正像您期待的那样,基本上大家的实现手段都大差不差。然而对于万人群来说,这显然还不够。

下面来看看我们针对万人群聊消息投递的技术优化手段。

5、万人群聊消息投递优化手段1:控速

针对万人群的消息投递,我们的一个主要手段就是控速。

如上图所示。

首先:我们会根据服务器的核数来建立多个群消息分发队列,这些队列我们设置了不同的休眠时间以及不同的消费线程数。

通俗来讲,可以将队列这样划分为快、中、慢等队列。

其次:我们根据群成员数量的大小来将所有群映射到相应的队列中。

规则是:

  • 1)小群映射到快队列中;
  • 2)大群映射到相应的慢队列中。

然后:小群由于人数少,对服务的影响很小,所以服务利用快队列快速的将群消息分发出去,而大群群消息则利用慢队列的相对高延时来起到控速的作用。

6、万人群聊消息投递优化手段2:合并

在本文第3节中提到的万人群聊所面临的技术挑战,最主要的挑战其实就是消息进行扩散分发投递后,消息被克隆出N条,消息流量瞬间被放大。

举个例子:当一条群消息发送到 IM 服务器后,需要从群组服务投递给消息服务,如果每一个群成员都投递一次,并且投递的群消息内容是一致的话,那肯定会造成相应的资源浪费和服务压力。

那么针对这种情况,我们的解决方案就是进行消息合并投递。

原理就是:服务落点计算中我们使用的是一致性哈希,群成员落点相对固定,所以落点一致的群成员我们可以合并成一次请求进行投递,这样就大幅提高了投递效率同时减少了服务的压力。

下图是云信团队分享的万人群消息合并投递逻辑:

▲ 上图引用自《IM中的万人群聊技术方案实践总结

如上图所示,云信团队的万人群消息合并投递方案是:按Link分组路由消息,同一Link上的全部群成员只需要路由一条消息即可。

7、十万、百万级的超大群处理方案

在实际群聊业务中,还有一种业务场景是超大规模群,这种群的群人数达到了数十万甚至上百万。

这种群如果按照上述的投投递方案,势必仍会造成消息节点的巨大压力。

比如我们有一个十万人的群,消息节点五台,消息服务处理消息的上限是一秒钟 4000 条,那每台消息节点大约会分到 2 万条群消息,这已大大超出了消息节点的处理能力。

所以为了避免上述问题,我们会将群成员上线超过3000的群识别为万人群、超级群,这种级别的群可以根据服务器数量和服务器配置相应做调整针对这种超级群会用特殊的队列来处理群消息的投递。

这个特殊的队列1秒钟往后端消息服务投递的消息数是消息服务处理上限的一半(留相应的能力处理其他消息),如果单台消息服务处理的 QPS 上限是 4000,那群组服务一秒往单台消息服务最多投递 2000 条。

8、写在最后

未来,我们也会针对群消息进行引用投递,对于大群里发的消息体比较大的消息,我们给群成员只分发和缓存消息的索引,比如 MessageID。等群成员真正拉取群消息时再从将消息组装好给客户端分发下去。这样做会节省分发的流量以及存储的空间。

随着互联网的发展,群组业务的模型和压力也在不停地扩展,后续可能还会遇到更多的挑战,当然也会不断迭代出更优的处理方式来应对。

附录:更多IM群聊技术文章

快速裂变:见证微信强大后台架构从0到1的演进历程(一)

如何保证IM实时消息的“时序性”与“一致性”?

IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?

IM群聊消息如此复杂,如何保证不丢不重?

微信后台团队:微信后台异步消息队列的优化升级实践分享

移动端IM中大规模群消息的推送如何保证效率、实时性?

现代IM系统中聊天消息的同步和存储方案探讨

关于IM即时通讯群聊消息的乱序问题讨论

IM群聊消息的已读回执功能该怎么实现?

IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?

一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

[技术脑洞] 如果把14亿中国人拉到一个微信群里技术上能实现吗?

IM群聊机制,除了循环去发消息还有什么方式?如何优化?

网易云信技术分享:IM中的万人群聊技术方案实践总结

阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处

IM群聊消息的已读未读功能在存储空间方面的实现思路探讨

直播系统聊天技术(一):百万在线的美拍直播弹幕系统的实时推送技术实践之路

直播系统聊天技术(二):阿里电商IM消息平台,在群聊、直播场景下的技术实践

直播系统聊天技术(三):微信直播聊天室单房间1500万在线的消息架构演进之路

直播系统聊天技术(四):百度直播的海量用户实时消息系统架构演进实践

企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等

融云IM技术分享:万人群聊消息投递方案的思考和实践

>> 更多同类文章 ……

本文已同步发布于“即时通讯技术圈”公众号。

同步发布链接是:http://www.52im.net/thread-3687-1-1.html

posted @ 2021-08-30 11:34 Jack Jiang 阅读(194) | 评论 (0)编辑 收藏

     摘要: 本文由微医云技术团队前端工程师张宇航分享,原题“从0到1打造一个 WebRTC 应用”,有修订和改动。1、引言去年初,突如其来的新冠肺炎疫情让线下就医渠道几乎被切断,在此背景下,在线问诊模式快速解决了大量急需就医病患的燃眉之急。而作为在线问诊中重要的一环——医患之间的视频问诊正是应用了实时音视频技术才得以实现。众所周之,实时音视频聊天技术门槛很高,一...  阅读全文

posted @ 2021-08-24 12:24 Jack Jiang 阅读(307) | 评论 (0)编辑 收藏

本文由徐宁发表于腾讯大讲堂,原题“程序员如何把你关注的内容推送到你眼前?揭秘信息流推荐背后的系统设计”,有改动和修订。

1、引言

信息推流(以下简称“Feed流”)这种功能在我们手机APP中几乎无处不在(尤其是社交/社群产品中),最常用的就是微信朋友圈、新浪微博等。

对Feed流的定义,可以简单理解为只要大拇指不停地往下划手机屏幕,就有一条条的信息不断涌现出来。就像给牲畜喂饲料一样,只要它吃光了就要不断再往里加,故此得名Feed(饲养)。

大多数带有Feed流功能的产品都包含两种Feed流:

  • 1)一种是基于算法:即动态算法推荐,比如今日头条、抖音短视频;
  • 2)一种是基于关注:即社交/好友关系,比如微信、知乎。

例如下图中的微博和知乎,顶栏的页卡都包含“关注”和“推荐”这两种:

如上图中这两种Feed流,它们背后用到的技术差别会比较大。不同于“推荐”页卡那种千人千面算法推荐的方式,通常“关注”页卡所展示的内容先后顺序都有固定的规则,最常见的规则是基于时间线来排序,也就是展示“我关注的人所发的帖子、动态、心情,根据发布时间从晚到早依次排列”。

本文将重点讨论的是“关注”功能对应的技术实现:先总结常用的基于时间线Feed流的后台技术实现方案,再结合具体的业务场景,根据实际需求在基本设计思路上做一些灵活的运用。

学习交流:

- 即时通讯/推送技术开发交流5群:215477170 [推荐]

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK

(本文同步发布于:http://www.52im.net/thread-3675-1-1.html

2、本文作者

徐宁:腾讯应用开发工程师,腾讯学院讲师,毕业于上海交通大学。目前负责腾讯智慧零售业务的后端开发工作,有丰富的视频直播,自动化营销系统开发经验。

3、Feed流技术实现方案1:读扩散

读扩散也称为“拉模式”,这应该是最符合我们认知直觉的一种技术实现方式。

原理如下图:

如上图所示:每一个内容发布者都有一个自己的发件箱(“我发布的内容”),每当我们发出一个新帖子,都存入自己的发件箱中。当我们的粉丝来阅读时,系统首先需要拿到粉丝关注的所有人,然后遍历所有发布者的发件箱,取出他们所发布的帖子,然后依据发布时间排序,展示给阅读者。

这种设计:阅读者读一次Feed流,后台会扩散为N次读操作(N等于关注的人数)以及一次聚合操作,因此称为读扩散。每次读Feed流相当于去关注者的收件箱主动拉取帖子,因此也得名——拉模式。

这种模式:

  • 1)好处是:底层存储简单,没有空间浪费;
  • 2)坏处是:每次读操作会非常重,操作非常多。

设想一下:如果我关注的人数非常多,遍历一遍我所关注的所有人,并且再聚合一下,这个系统开销会非常大,时延上可能达到无法忍受的地步。

因此:读扩散主要适用系统中阅读者关注的人没那么多,并且刷Feed流并不频繁的场景。

拉模式还有一个比较大的缺点:就是分页不方便,我们刷微博或朋友圈,肯定是随着大拇指在屏幕不断划动,内容一页一页的从后台拉取。如果不做其他优化,只采用实时聚合的方式,下滑到比较靠后的页码时会非常麻烦。

4、Feed流技术实现方案2:写扩散

据统计:大多数Feed流产品的读写比大概在100:1,也就是说大部分情况都是刷Feed流看别人发的朋友圈和微博,只有很少情况是自己亲自发一条朋友圈或微博给别人看。

因此:读扩散那种很重的读逻辑并不适合大多数场景。

我们宁愿让发帖的过程复杂一些,也不愿影响用户读Feed流的体验,因此稍微改造一下前面方案就有了写扩散。写扩散也称为“推模式”,这种模式会对拉模式的一些缺点做改进。

原理如下图:

如上图所示:系统中每个用户除了有发件箱,也会有自己的收件箱。当发布者发表一篇帖子的时候,除了往自己发件箱记录一下之外,还会遍历发布者的所有粉丝,往这些粉丝的收件箱也投放一份相同内容。这样阅读者来读Feed流时,直接从自己的收件箱读取即可。

这种设计:每次发表帖子,都会扩散为M次写操作(M等于自己的粉丝数),因此成为写扩散。每篇帖子都会主动推送到所有粉丝的收件箱,因此也得名推模式。

这种模式可想而知:发一篇帖子,背后会涉及到很多次的写操作。通常为了发帖人的用户体验,当发布的帖子写到自己发件箱时,就可以返回发布成功。后台另外起一个异步任务,不慌不忙地往粉丝收件箱投递帖子即可。

写扩散的好处在于通过数据冗余(一篇帖子会被存储M份副本),提升了阅读者的用户体验。通常适当的数据冗余不是什么问题,但是到了微博明星这里,完全行不通。比如目前微博粉丝量Top2的谢娜与何炅,两个人微博粉丝过亿。

设想一下:如果单纯采用推模式,那每次谢娜何炅发一条微博,微博后台都要地震一次。一篇微博导致后台上亿次写操作,这显然是不可行的。

另外:由于写扩散是异步操作,写的太慢会导致帖子发出去半天,有些粉丝依然没能看见,这种体验也不太好。

通常写扩散适用于好友量不大的情况,比如微信朋友圈正是写扩散模式。每一名微信用户的好友上限为5000人,也就是说你发一条朋友圈最多也就扩散到5000次写操作,如果异步任务性能好一些,完全没有问题。

关于微信朋友圈的技术资料:

5、Feed流技术实现方案2:读写混合模式

读写混合也可以称作“推拉结合”,这种方式可以兼具读扩散和写扩散的优点。

我们首先来总结一下读扩散和写扩散的优缺点:

见上图:仔细比较一下读扩散与写扩散的优缺点,不难发现两者的适用场景是互补的。

因此:在设计后台存储的时候,我们如果能够区分一下场景,在不同场景下选择最适合的方案,并且动态调整策略,就实现了读写混合模式。

原理如下图:

以微博为例:当何炅这种粉丝量超大的人发帖时,将帖子写入何炅的发件箱,另外提取出来何炅粉丝当中比较活跃的那一批(这已经可以筛掉大部分了),将何炅的帖子写入他们的收件箱。当一个粉丝量很小的路人甲发帖时,采用写扩散方式,遍历他的所有粉丝并将帖子写入粉丝收件箱。

对于那些活跃用户登录刷Feed流时:他直接从自己的收件箱读取帖子即可,保证了活跃用户的体验。

当一个非活跃的用户突然登录刷Feed流时:

  • 1)一方面需要读他的收件箱;
  • 2)另一面需要遍历他所关注的大V用户的发件箱提取帖子,并且做一下聚合展示。

在展示完后:系统还需要有个任务来判断是否有必要将该用户升级为活跃用户。

因为有读扩散的场景存在,因此即使是混合模式,每个阅读者所能关注的人数也要设置上限,例如新浪微博限制每个账号最多可以关注2000人。

如果不设上限:设想一下有一位用户把微博所有账号全部关注了,那他打开关注列表会读取到微博全站所有帖子,一旦出现读扩散,系统必然崩溃(即使是写扩散,他的收件箱也无法容纳这么多的微博)。

读写混合模式下,系统需要做两个判断:

  • 1)哪些用户属于大V,我们可以将粉丝量作为一个判断指标;
  • 2)哪些用户属于活跃粉丝,这个判断标准可以是最近一次登录时间等。

这两处判断标准就需要在系统发展过程中动态地识别和调整,没有固定公式了。

可以看出:读写结合模式综合了两种模式的优点,属于最佳方案。

然而他的缺点是:系统机制非常复杂,给程序员带来无数烦恼。通常在项目初期,只有一两个开发人员,用户规模也很小的时候,一步到位地采用这种混合模式还是要慎重,容易出bug。当项目规模逐渐发展到新浪微博的水平,有一个大团队专门来做Feed流时,读写混合模式才是必须的。

6、Feed流中的分页问题

上面几节已经叙述了基于时间线的几种Feed流常见设计方案,但实操起来会比理论要麻烦许多。

接下来专门讨论一个Feed流技术方案中的痛点——Feed流的分页。

不管是读扩散还是写扩散,Feed流本质上是一个动态列表,列表内容会随着时间不断变化。传统的前端分页参数使用page_size和page_num,分表表示每页几条,以及当前是第几页。

对于一个动态列表会有如下问题:

如上图所示:在T1时刻读取了第一页,T2时刻有人新发表了“内容11”,在T3时刻如果来拉取第二页,会导致错位出现,“内容6”在第一页和第二页都被返回了。事实上,但凡两页之间出现内容的添加或删除,都会导致错位问题。

为了解决这一问题:通常Feed流的分页入参不会使用page_size和page_num,而是使用last_id来记录上一页最后一条内容的id。前端读取下一页的时候,必须将last_id作为入参,后台直接找到last_id对应数据,再往后偏移page_size条数据,返回给前端,这样就避免了错位问题。

如下图:

采用last_id的方案有一个重要条件:就是last_id本身这条数据不可以被硬删除。

设想一下:

  • 1)上图中T1时刻返回5条数据,last_id为内容6;
  • 2)T2时刻内容6被发布者删除;
  • 3)那么T3时刻再来请求第二页,我们根本找不到last_id对应的数据了,也就无法确认分页偏移量。

通常碰到删除的场景:我们采用软删除方式,只是在内容上置一个标志位,表示内容已删除。

由于已经删除的内容不应该再返回给前端,因此软删除模式下,找到last_id并往后偏移page_size条,如果其中有被删除的数据会导致获得足够的数据条数给前端。

这里一个解决方案是找不够继续再往下找,另一种方案是与前端协商,允许返回条数少于page_size条,page_size只是个建议值。甚至大家约定好了以后,可以不要page_size参数。

7、Feed流技术方案在某直播应用中的实践

7.1 需求背景

本节将结合实际业务,分享一下实际场景中碰到的一个非常特殊的Feed流设计方案。

xx 直播是一款直播带货工具,主播可以创建一场未来时刻的直播,到时间后开播卖货,直播结束后,主播的粉丝可以查看直播回放。

这样,每个直播场次就有三种状态——预告中(创建一场直播但还未开播)、直播中、回放。

作为观众,我可以关注多位主播,这样从粉丝视角来看,也会有个直播场次的Feed流页面。

这个Feed流最特殊的地方在于它的排序规则:

解释一下这个Feed流排序规则:

  • 1)我关注的所有主播:正在直播中的场次排在最前;预告中的场次排中间;回放场次排最后;
  • 2)多场次都在直播中的:按开播时间从晚到早排序;
  • 3)多场次都在预告中的:按预计开播时间从早到晚排序;
  • 4)多场次都在回放的:按直播结束时间从晚到早排序。

7.2 问题分析

本需求最复杂的点在于Feed流内容融入的“状态”因素,状态的转变会直接导致Feed流顺序不同。

为了更清晰解释一下对排序的影响,我们可以用下图详细说明:

上图中:展示了4个主播的5个直播场次,作为观众,当我在T1时刻打开页面,看到的顺序是场次3在最上方,其余场次均在预告状态,按照预计开播时间从早到晚展示。当我在T2时刻打开页面,场次5在最上方,其余有三场在预告状态排在中间,场次3已经结束了所以排在最后。以此类推,直到所有直播都结束,所有场次最终的状态都会变为回放。

这里需要注意一点:如果我在T1时刻打开第一页,然后盯着页面不动,一直盯到T4时刻再下划到第二页,这时上一页的last_id,即分页偏移量很有可能因为直播状态变化而不知道飞到了什么位置,这会导致严重的错位问题,以及直播状态展示不统一的问题(第一页展示的是T1时刻的直播状态,第二页展示的是T4时刻的直播状态)。

7.3 解决方案

直播系统是个单向关系链,和微博有些类似,每个观众会关注少量主播,每个主播会可能有非常多的关注者。

由于有状态变化的存在,写扩散几乎无法实现。

因为:如果采用写扩散的方式,每次主播创建直播、直播开播、直播结束这三个事件发生时导致的场次状态变化,会扩散为非常多次的写操作,不仅操作复杂,时延上也无法接受。

微博之所以可以写扩散:就是因为一条微博发出后,这篇微博就不会再有任何影响排序的状态转变。

而在我们场景中:“预告中”与“直播中”是两个中间态,而“回放”状态才是所有直播的最终归宿,一旦进入回放,这场直播也就不会再有状态转变。因此“直播中”与“预告中”状态可以采用读扩散方式,“回放”状态采取写扩散方式。

最终的方案如下图所示:

如上图:会影响直播状态的三种事件(创建直播、开播、结束直播)全部采用监听队列异步处理。

我们为每一位主播维护一个直播中+预告中状态的优先级队列:

  • 1)每当监听到有主播创建直播时,将直播场次加入队列中,得分为开播的时间戳的相反数(负数);
  • 2)每当监听到有主播开播时,把这场直播在队列中的得分修改为开播时间(正数);
  • 3)每当监听到有主播结束直播,则异步地将播放信息投递到每个观众的回放队列中。

这里有一个小技巧:前文提到,直播中状态按照开播时间从大到小排序,而预告中状态则按照开播时间从小到大排序,因此如果将预告中状态的得分全部取开播时间相反数,那排序同样就成为了从大到小。这样的转化可以保证直播中与预告中同处于一个队列排序。预告中得分全都为负数,直播中得分全都为正数,最后聚合时可以保证所有直播中全都自然排在预告中前面。

另外:前文还提到的另一个问题是T1时刻拉取第一页,T4时刻拉取第二页,导致第一页和第二页直播间状态不统一。

解决这个问题的办法是通过快照方式:当观众来拉取第一页Feed流时,我们依据当前时间,将全部直播中和预告中状态的场次建立一份快照,使用一个session_id标识,每次前端分页拉取时,我们直接从快照中读取即可。如果快照中读取完毕,证明该观众的直播中和预告中场次全部读完,剩下的则使用回放队列进行补充。

照此一来,我们的Feed流系统,前端分页拉取的参数一共有4个:

每当碰到session_id和last_id为空,则证明用户想要读取第一页,需要重新构建快照。

这里还有一个衍生问题:session_id的如何取值?

答案是:

  • 1)如果不考虑同一个观众在多端登录的情况,其实每一位观众维护一个快照id即可,也就是直接将系统用户id设为session_id;
  • 2)如果考虑多端登录的情况,则session_id中必须包含每个端的信息,以避免多端快照相互影响;
  • 3)如果不心疼内存,也可以每次随机一个字符串作为session_id,并设置一个足够长的过期时间,让快照自然过期。

以上设计:其实系统计算量最大的时刻就是拉取第一页,构建快照的开销。

目前的线上数据,对于只关注不到10个主播的观众(这也是大多数场景),拉取第一页的QPS可以达到1.5万。如果将第二页以后的请求也算进来,Feed流的综合QPS可以达到更高水平,支撑目前的用户规模已经绰绰有余。如果我们拉取第一页时只获取到前10条即可直接返回,将构建快照操作改为异步,也许QPS可以更高一些,这可能是后续的优化点。

8、本文小结

几乎所有基于时间线和关注关系的Feed流都逃不开三种基本设计模式:

  • 1)读扩散;
  • 2)写扩散;
  • 3)读写混合。

具体到实际业务中,可能会有更复杂的场景,比如本文所说的:

  • 1)状态流转影响排序;
  • 2)微博朋友圈场景中会有广告接入、特别关注、热点话题等可能影响到Feed流排序的因素。

这些场景就只能根据业务需求,做相对应的变通了。

附录:更多社交应用架构设计文章

  1. 浅谈IM系统的架构设计
  2. 简述移动端IM开发的那些坑:架构设计、通信协议和客户端
  3. 一套海量在线用户的移动端IM架构设计实践分享(含详细图文)
  4. 一套原创分布式即时通讯(IM)系统理论架构方案
  5. 从零到卓越:京东客服即时通讯系统的技术架构演进历程
  6. 蘑菇街即时通讯/IM服务器开发之架构选择
  7. 腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT
  8. 微信后台基于时间序的海量数据冷热分级架构设计实践
  9. 微信技术总监谈架构:微信之道——大道至简(演讲全文)
  10. 如何解读《微信技术总监谈架构:微信之道——大道至简》
  11. 快速裂变:见证微信强大后台架构从0到1的演进历程(一)
  12. 移动端IM中大规模群消息的推送如何保证效率、实时性?
  13. 现代IM系统中聊天消息的同步和存储方案探讨
  14. 微信朋友圈千亿访问量背后的技术挑战和实践总结
  15. 以微博类应用场景为例,总结海量社交系统的架构设计步骤
  16. 子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践
  17. 知乎技术分享:从单机到2000万QPS并发的Redis高性能缓存实践之路
  18. 微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)
  19. 微信技术分享:微信的海量IM聊天消息序列号生成实践(容灾方案篇)
  20. 一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践
  21. 社交软件红包技术解密(一):全面解密QQ红包技术方案——架构、技术实现等
  22. 社交软件红包技术解密(二):解密微信摇一摇红包从0到1的技术演进
  23. 社交软件红包技术解密(三):微信摇一摇红包雨背后的技术细节
  24. 社交软件红包技术解密(四):微信红包系统是如何应对高并发的
  25. 社交软件红包技术解密(五):微信红包系统是如何实现高可用性的
  26. 社交软件红包技术解密(六):微信红包系统的存储层架构演进实践
  27. 社交软件红包技术解密(七):支付宝红包的海量高并发技术实践
  28. 社交软件红包技术解密(八):全面解密微博红包技术方案
  29. 社交软件红包技术解密(九):谈谈手Q红包的功能逻辑、容灾、运维、架构等
  30. 从游击队到正规军(一):马蜂窝旅游网的IM系统架构演进之路
  31. 从游击队到正规军(二):马蜂窝旅游网的IM客户端架构演进和实践总结
  32. 从游击队到正规军(三):基于Go的马蜂窝旅游网分布式IM系统技术实践
  33. 瓜子IM智能客服系统的数据架构设计(整理自现场演讲,有配套PPT)
  34. 阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处
  35. 微信后台基于时间序的新一代海量数据存储架构的设计实践
  36. 阿里技术分享:电商IM消息平台,在群聊、直播场景下的技术实践
  37. 一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等
  38. 一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等
  39. 从新手到专家:如何设计一套亿级消息量的分布式IM系统
  40. 企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等
  41. 融云技术分享:全面揭秘亿级IM消息的可靠投递机制
  42. IM开发技术学习:揭秘微信朋友圈这种信息推流背后的系统设计
  43. >> 更多同类文章 ……

本文已同步发布于“即时通讯技术圈”公众号。

同步发布链接是:http://www.52im.net/thread-3675-1-1.html

posted @ 2021-08-17 15:34 Jack Jiang 阅读(179) | 评论 (0)编辑 收藏

     摘要: 本文由美团技术团队分享,作者“健午、佳猛、陆凯、冯江”,原题“美团终端消息投递服务Pike的演进之路”,有修订。1、引言传统意义上来说,实时消息推送通常都是IM即时通讯这类应用的技术范畴,随着移动端互联网的普及,人人拥有手机、随时都是“在线”已属常态,于是消息的实时触达能力获得了广泛的需求,已经不再局限于IM即时通讯这类应用中...  阅读全文

posted @ 2021-08-09 15:36 Jack Jiang 阅读(238) | 评论 (0)编辑 收藏

1、引言

在IM客户端的使用场景中,基于本地数据的全文检索功能扮演着重要的角色,最常用的比如:查找聊天记录、联系人,就像下图这样。

▲ 微信的聊天记录查找功能

类似于IM中的聊天记录查找、联系人搜索这类功能,有了全文检索能力后,确实能大大提高内容查找的效率,不然,让用户手动翻找,确实降低了用户体验。

本文将具体来聊聊网易云信是如何实现IM客户端全文检索能力的,希望能带给你启发。

学习交流:

- 即时通讯/推送技术开发交流5群:215477170 [推荐]

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK

(本文同步发布于:http://www.52im.net/thread-3651-1-1.html

2、关于作者

李宁:网易云信高级前端开发工程师,负责音视频 IM SDK 的应用开发、组件化开发及解决方案开发,对 React、PaaS 组件化设计、多平台的开发与编译有丰富的实战经验。

3、相关文章

IM客户端全文检索相关文章:

  1. 微信手机端的本地数据全文检索优化之路
  2. 微信团队分享:微信移动端的全文检索多音字问题解决方案

网易技术团队分享的其它文章:

  1. 网易视频云技术分享:音频处理与压缩技术快速入门
  2. 网易云信实时视频直播在TCP数据传输层的一些优化思路
  3. 网易云信技术分享:IM中的万人群聊技术方案实践总结
  4. Web端即时通讯实践干货:如何让你的WebSocket断网重连更快速?
  5. 子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践

4、什么是全文检索

所谓全文检索,就是要在大量内容中找到包含某个单词出现位置的技术。

在传统的关系型数据库中,只能通过 LIKE 条件查询来实现,这样有几个弊端:

  • 1)无法使用数据库索引,需要遍历全表,性能较差;
  • 2)搜索效果差,只能首尾位模糊匹配,无法实现复杂的搜索需求;
  • 3)无法得到内容与搜索条件的相关性。

我们在 IM 的 iOS、安卓以及桌面端中都实现了基于 SQLite 等库的本地数据全文检索功能,但是在 Web 端和 Electron 上缺少了这部分功能。

因为在 Web 端,由于浏览器环境限制,能使用的本地存储数据库只有 IndexDB,暂不在讨论的范围内。但在 Electron 上,虽然也是内置了 Chromium 的内核,但是因为可以使用 Node.js 的能力,于是乎选择的范围就多了一些。本文内容我们具体以基于Electron的IM客户端为例,来讨论全文检索技术实现(技术思路是相通的,并不局限于具体什么端)。

PS:如果你不了解什么是Electron技术,读一下这篇《快速了解Electron:新一代基于Web的跨平台桌面技术》。

我们先来具体看下该如何实现全文检索。

要实现全文检索,离不开以下两个知识点:

  • 1)倒排索引;
  • 2)分词。

这两个技术是实现全文检索的技术以及难点,其实现的过程相对比较复杂,在聊全文索引的实现前,我们具体学习一下这两个技术的原理。

5、全文检索知识点1:倒排索引

先简单介绍下倒排索引,倒排索引的概念区别于正排索引:

  • 1)正排索引:是以文档对象的唯一 ID 作为索引,以文档内容作为记录的结构;
  • 2)倒排索引:是以文档内容中的单词作为索引,将包含该词的文档 ID 作为记录的结构。

以倒排索引库 search-index 举个实际的例子:

在我们的 IM 中,每条消息对象都有 idClient 作为唯一 ID,接下来我们输入「今天天气真好」,将其每个中文单独分词(分词的概念我们在下文会详细分享),于是输入变成了「今」、「天」、「天」、「气」、「真」、「好」。再通过 search-index 的 PUT 方法将其写入库中。

最后看下上述例子存储内容的结构:

如是图所示:可以看到倒排索引的结构,key 是分词后的单个中文、value 是包含该中文消息对象的 idClient 组成的数组。

当然:search-index 除了以上这些内容,还有一些其他内容,例如 Weight、Count 以及正排的数据等,这些是为了排序、分页、按字段搜索等功能而存在的,本文就不再细细展开了。

6、全文检索知识点2:分词

6.1 基本概念

分词就是将原先一条消息的内容,根据语义切分成多个单字或词句,考虑到中文分词的效果以及需要在 Node 上运行,我们选择了 Nodejieba 作为基础分词库。

以下是 jieba 分词的流程图:

以“去北京大学玩”为例,我们选择其中最为重要的几个模块分析一下。

6.2 加载词典

jieba 分词会在初始化时先加载词典,大致内容如下:

6.3 构建前缀词典

接下来会根据该词典构建前缀词典,结构如下:

其中:“北京大”作为“北京大学”的前缀,它的词频是0,这是为了便于后续构建 DAG 图。

6.4 构建 DAG 图

DAG 图是 Directed Acyclic Graph 的缩写,即有向无环图。

基于前缀词典,对输入的内容进行切分。

其中:

  • 1)“去”没有前缀,因此只有一种切分方式;
  • 2)对于“北”,则有“北”、“北京”、“北京大学”三种切分方式;
  • 3)对于“京”,也只有一种切分方式;
  • 4)对于“大”,有“大”、“大学”两种切分方式;
  • 5)对于“学”和“玩”,依然只有一种切分方式。

如此,可以得到每个字作为前缀词的切分方式。

其 DAG 图如下图所示:

6.5 最大概率路径计算

以上 DAG 图的所有路径如下:

去/北/京/大/学/玩

去/北京/大/学/玩

去/北京/大学/玩

去/北京大学/玩

因为每个节点都是有权重(Weight)的,对于在前缀词典里的词语,它的权重就是它的词频。因此我们的问题就是想要求得一条最大路径,使得整个句子的权重最高。

这是一个典型的动态规划问题,首先我们确认下动态规划的两个条件。

1)重复子问题:

对于节点 i 和其可能存在的多个后继节点 j 和 k:

  • 1)任意通过i到达j的路径的权重 = 该路径通过i的路径权重 + j的权重,即 R(i -> j) = R(i) + W(j);
  • 2)任意通过i到达k的路径的权重 = 该路径通过i的路径权重 + k的权重,即 R(i -> k) = R(i) + W(k)。

即对于拥有公共前驱节点 i 的 j 和 k,需要重复计算到达 i 路径的权重。

2)最优子结构:

设整个句子的最优路径为 Rmax,末端节点为 x,多个可能存在的前驱节点为 i、j、k。

得到公式如下:

Rmax = max(Rmaxi, Rmaxj, Rmaxk) + W(x)

于是问题变成了求解 Rmaxi、Rmaxj 以及 Rmaxk,子结构里的最优解即是全局最优解的一部分。

如上,最后计算得出最优路径为“去/北京大学/玩”。

6.6 HMM 隐式马尔科夫模型

对于未登陆词,jieba 分词采用 HMM(Hidden Markov Model 的缩写)模型进行分词。

它将分词问题视为一个序列标注问题,句子为观测序列,分词结果为状态序列。

jieba 分词作者在 issue 中提到,HMM 模型的参数基于网上能下载到的 1998 人民日报的切分语料,一个 MSR 语料以及自己收集的 TXT 小说、用 ICTCLAS 切分,最后用 Python 脚本统计词频而成。

该模型由一个五元组组成,并有两个基本假设。

五元组:

  • 1)状态值集合;
  • 2)观察值集合;
  • 3)状态初始概率;
  • 4)状态转移概率;
  • 5)状态发射概率。

基本假设:

  • 1)齐次性假设:即假设隐藏的马尔科夫链在任意时刻 t 的状态只依赖于其前一时刻 t-1 的状态,与其它时刻的状态及观测无关,也与时刻 t 无关;
  • 2)观察值独立性假设:即假设任意时刻的观察值只与该时刻的马尔科夫链的状态有关,与其它观测和状态无关。

状态值集合即{ B: begin, E: end, M: middle, S: single },表示每个字所处在句子中的位置,B 为开始位置,E 为结束位置,M 为中间位置,S 是单字成词。

观察值集合就是我们输入句子中每个字组成的集合。

状态初始概率表明句子中的第一个字属于 B、M、E、S 四种状态的概率,其中 E 和 M 的概率都是0,因为第一个字只可能 B 或者 S,这与实际相符。

状态转移概率表明从状态 1 转移到状态 2 的概率,满足齐次性假设,结构可以用一个嵌套的对象表示:

P = {

    B: {E: -0.510825623765990, M: -0.916290731874155},

    E: {B: -0.5897149736854513, S: -0.8085250474669937},

    M: {E: -0.33344856811948514, M: -1.2603623820268226},

    S: {B: -0.7211965654669841, S: -0.6658631448798212},

}

P['B']['E'] 表示从状态 B 转移到状态 E 的概率(结构中为概率的对数,方便计算)为 0.6,同理,P['B']['M'] 表示下一个状态是 M 的概率为 0.4,说明当一个字处于开头时,下一个字处于结尾的概率高于下一个字处于中间的概率,符合直觉,因为二个字的词比多个字的词要更常见。

状态发射概率表明当前状态,满足观察值独立性假设,结构同上,也可以用一个嵌套的对象表示:

P = {

    B: {'突': -2.70366861046, '肃': -10.2782270947, '适': -5.57547658034},

    M: {'要': -4.26625051239, '合': -2.1517176509, '成': -5.11354837278},

    S: {……},

    E: {……},

}

P['B']['突'] 的含义就是状态处于 B,观测的字是“突”的概率的对数值等于 -2.70366861046。

最后,通过 Viterbi 算法,输入观察值集合,将状态初始概率、状态转移概率、状态发射概率作为参数,输出状态值集合(即最大概率的分词结果)。关于 Viterbi 算法,本文不再详细展开,有兴趣的读者可以自行查阅。

7、技术实现

上节中介绍的全文检索这两块技术,是我们架构的技术核心。基于此,我们对IM 的 Electron 端技术架构做了改进。以下将详细介绍之。

7.1 架构图详解

考虑到全文检索只是 IM 中的一个功能,为了不影响其他 IM 的功能,并且能更快的迭代需求,所以采用了如下的架构方案。

架构图如下:

如上图所示,右边是之前的技术架构,底层存储库使用了 indexDB,上层有读写两个模块。

读写模块的具体作用是:

  • 1)当用户主动发送消息、主动同步消息、主动删除消息以及收到消息的时候,会将消息对象同步到 indexDB;
  • 2)当用户需要查询关键字的时候,会去 indexDB 中遍历所有的消息对象,再使用 indexOf 判断每一条消息对象是否包含所查询的关键字(类似 LIKE)。

那么,当数据量大的时候,查询的速度是非常缓慢的。

左边是加入了分词以及倒排索引数据库的新的架构方案,这个方案不会对之前的方案有任何影响,只是在之前的方案之前加了一层。

现在,读写模块的工作逻辑:

  • 1)当用户主动发送消息、主动同步消息、主动删除消息以及收到消息的时候,会将每一条消息对象中的消息经过分词后同步到倒排索引数据库;
  • 2)当用户需要查询关键字的时候,会先去倒排索引数据库中找出对应消息的 idClient,再根据 idClient 去 indexDB 中找出对应的消息对象返回给用户。

7.2 架构优点

该方案有以下4个优点:

  • 1)速度快:通过 search-index 实现倒排索引,从而提升了搜索速度。
  • 2)跨平台:因为 search-index 与 indexDB 都是基于 levelDB,因此 search-index 也支持浏览器环境,这样就为 Web 端实现全文检索提供了可能性;
  • 3)独立性:倒排索引库与 IM 主业务库 indexDB 分离;
  • 4)灵活性:全文检索以插件的形式接入。

针对上述第“3)”点:当 indexDB 写入数据时,会自动通知到倒排索引库的写模块,将消息内容分词后,插入到存储队列当中,最后依次插入到倒排索引数据库中。当需要全文检索时,通过倒排索引库的读模块,能快速找到对应关键字的消息对象的 idClient,根据 idClient 再去 indexDB 中找到消息对象并返回。

针对上述第“4)”点:它暴露出一个高阶函数,包裹 IM 并返回新的经过继承扩展的 IM,因为 JS 面向原型的机制,在新的 IM 中不存在的方法,会自动去原型链(即老的 IM)当中查找,因此,使得插件可以聚焦于自身方法的实现上,并且不需要关心 IM 的具体版本,并且插件支持自定义分词函数,满足不同用户不同分词需求的场景

7.3 使用效果

使用了如上架构后,经过我们的测试,在数据量 20W 的级别上,搜索时间从最开始的十几秒降到一秒内,搜索速度快了 20 倍左右。

8、本文小结

本文中,我们便基于 Nodejieba 和 search-index 在 Electron 上实现了IM聊天消息的全文检索,加快了聊天记录的搜索速度。

当然,后续我们还会针对以下方面做更多的优化,比如以下两点:

1)写入性能 :在实际的使用中,发现当数据量大了以后,search-index 依赖的底层数据库 levelDB 会存在写入性能瓶颈,并且 CPU 和内存的消耗较大。经过调研,SQLite 的写入性能相对要好很多,从观测来看,写入速度只与数据量成正比,CPU 和内存也相对稳定,因此,后续可能会考虑用将 SQLite 编译成 Node 原生模块来替换 search-index。

2)可扩展性 :目前对于业务逻辑的解耦还不够彻底。倒排索引库当中存储了某些业务字段。后续可以考虑倒排索引库只根据关键字查找消息对象的 idClient,将带业务属性的搜索放到 indexDB 中,将倒排索引库与主业务库彻底解耦。

以上,就是本文的全部分享,希望我的分享能对大家有所帮助。

附录:更多IM干货技术文章

新手入门一篇就够:从零开发移动端IM

从客户端的角度来谈谈移动端IM的消息可靠性和送达机制

移动端IM中大规模群消息的推送如何保证效率、实时性?

移动端IM开发需要面对的技术问题

IM消息送达保证机制实现(一):保证在线实时消息的可靠投递

IM消息送达保证机制实现(二):保证离线消息的可靠投递

如何保证IM实时消息的“时序性”与“一致性”?

一个低成本确保IM消息时序的方法探讨

IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?

IM群聊消息如此复杂,如何保证不丢不重?

谈谈移动端 IM 开发中登录请求的优化

移动端IM登录时拉取数据如何作到省流量?

浅谈移动端IM的多点登录和消息漫游原理

完全自已开发的IM该如何设计“失败重试”机制?

通俗易懂:基于集群的移动端IM接入层负载均衡方案分享

微信对网络影响的技术试验及分析(论文全文)

微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)

融云技术分享:解密融云IM产品的聊天消息ID生成策略

适合新手:从零开发一个IM服务端(基于Netty,有完整源码)

拿起键盘就是干:跟我一起徒手开发一套分布式IM系统

适合新手:手把手教你用Go快速搭建高性能、可扩展的IM系统(有源码)

IM里“附近的人”功能实现原理是什么?如何高效率地实现它?

IM消息ID技术专题(一):微信的海量IM聊天消息序列号生成实践(算法原理篇)

IM开发宝典:史上最全,微信各种功能参数和逻辑规则资料汇总

IM开发干货分享:我是如何解决大量离线消息导致客户端卡顿的

零基础IM开发入门(一):什么是IM系统?

零基础IM开发入门(二):什么是IM系统的实时性?

零基础IM开发入门(三):什么是IM系统的可靠性?

零基础IM开发入门(四):什么是IM系统的消息时序一致性?

一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等

IM扫码登录技术专题(三):通俗易懂,IM扫码登录功能详细原理一篇就够

理解IM消息“可靠性”和“一致性”问题,以及解决方案探讨

阿里技术分享:闲鱼IM基于Flutter的移动端跨端改造实践

融云技术分享:全面揭秘亿级IM消息的可靠投递机制

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

IM开发干货分享:有赞移动端IM的组件化SDK架构设计实践

IM开发干货分享:网易云信IM客户端的聊天消息全文检索技术实践

>> 更多同类文章 ……

(本文同步发布于:http://www.52im.net/thread-3651-1-1.html

posted @ 2021-08-03 12:42 Jack Jiang 阅读(286) | 评论 (0)编辑 收藏

本文由融云技术团队原创分享,原题“IM 消息同步机制全面解析”,为使文章更好理解,对内容进行了重新归纳和细节修订。

1、内容概述

即时通讯(IM)系统最基础、最重要的是消息的及时性与准确性,及时体现在延迟,准确则具体表现为不丢、不重、不乱序。

综合考虑业务场景、系统复杂度、网络流量、终端能耗等,我们的亿级分布式IM消息系统精心设计了消息收发机制,并不断打磨优化,形成了现在的消息可靠投递机制。

整体思路就是:

  • 1)客户端、服务端共同配合,互相补充;
  • 2)采用多重机制,从不同层面保障;
  • 3)拆分上下行,分别处理。

本文根据融云亿级IM消息系统的技术实践,总结了分布式IM消息的可靠投递机制,希望能为你的IM开发和知识学习起到抛砖引玉的作用。

学习交流:

- 即时通讯/推送技术开发交流5群:215477170 [推荐]

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK

(本文已同步发布于:http://www.52im.net/thread-3638-1-1.html

2、推荐阅读

以下是相关文章汇总,有兴趣可以一并阅读:

零基础IM开发入门(二):什么是IM系统的实时性?

零基础IM开发入门(三):什么是IM系统的可靠性?

零基础IM开发入门(四):什么是IM系统的消息时序一致性?

IM消息送达保证机制实现(一):保证在线实时消息的可靠投递

IM消息送达保证机制实现(二):保证离线消息的可靠投递

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

理解IM消息“可靠性”和“一致性”问题,以及解决方案探讨

如何保证IM实时消息的“时序性”与“一致性”?

IM群聊消息如此复杂,如何保证不丢不重?

从客户端的角度来谈谈移动端IM的消息可靠性和送达机制

一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等

从新手到专家:如何设计一套亿级消息量的分布式IM系统

浅谈移动端IM的多点登录和消息漫游原理

完全自已开发的IM该如何设计“失败重试”机制?

IM开发干货分享:我是如何解决大量离线消息导致客户端卡顿的

以下是融云技术团队分享的其它文章:

IM消息ID技术专题(三):解密融云IM产品的聊天消息ID生成策略

融云技术分享:基于WebRTC的实时音视频首帧显示时间优化实践

融云技术分享:融云安卓端IM产品的网络链路保活技术实践

即时通讯云融云CTO的创业经验分享:技术创业,你真的准备好了?

3、客户端与服务端消息交互整体原理

3.1 概述

一个完整的IM消息交互逻辑,通常会为两段:

  • 1)消息上行段:即由消息发送者通过IM实时通道发送给服务端;
  • 2)消息下行段:由服务端按照一定的策略送达给最终的消息接收人。

3.2 消息上行段

消息上行段主要就是依赖IM的实时通道将消息传递给服务端。

这个阶段的消息可靠投递,需要从协议层进行保证,协议层需要提供可靠、有序的双向字节流传输,我们是通过自研的通信协议 RMTP(即 RongCloud Message Transfer Protocol)实现的。

客户端与服务端之间使用长连接,基于 RMTP 协议传输数据。

RMTP协议交互示意图:

如上图所示,协议层通过 QoS、 ACK 等机制,保证IM消息上行段数据传输的可靠性。

3.3 消息下行段

经过总结,消息下行段主要有三种行为。

1)客户端主动拉取消息,主动拉取有两个触发方式:

  • ① 拉取离线消息:与 IM 服务新建立连接成功,用于获取不在线的这段时间未收到的消息;
  • ② 定时拉取消息:在客户端最后收到消息后启动定时器,比如 3-5 分钟执行一次。主要有两个目的,一个是用于防止因网络、中间设备等不确定因素引起的通知送达失败,服务端客户端状态不一致,一个是可通过本次请求,对业务层做状态机保活。

2)服务端主动-发送消息(直发消息):

这是在线消息发送机制之一,简单理解为服务端将消息内容直接发送给客户端,适用于消息频率较低,并且持续交互,比如二人或者群内的正常交流讨论。

3)服务端主动-发送通知(通知拉取):

这是在线消息发送机制之一,简单理解为服务端给客户端发送一个通知,通知包含时间戳等可作为排序索引的内容,客户端收到通知后,依据自身数据,对比通知内时间戳,发起拉取消息的流程。

这种场景适用于较多消息传递:比如某人有很多大规模的群,每个群内都有很多成员正在激烈讨论。通过通知拉取机制,可以有效的减少客户端服务端网络交互次数,并且对多条消息进行打包,提升有效数据载荷。既能保证时效,又能保证性能。

客户端服务端下行段消息交互示意图:

4、客户端与服务端消息交互具体实现

正如上节所言,我们将消息的交互流程进行了拆分:即拆分出上下行。

4.1 上行

在上行过程保证发送消息顺序,为了保证消息有序, 最好的方式是按照 userId 区分,然后使用时间戳排序。那么分布式部署情况下,将用户归属到固定的业务服务器上(PS:指的是同一账号的不同端固定连接到相同的业务服务器上),会使得上行排序变得更容易。同时归属到同一个服务器,在多端维护时也更容易。

客户端连接过程:

  • 1)客户端通过 APP server ,获取到连接使用的 token;
  • 2)客户端使用 token 通过导航服务,获取具体连接的 IM 接入服务器(CMP),导航服务通过 userId 计算接入服务器,然后下发,使得某一客户端可以连接在同一台接入服务器(CMP)。

示意图如下:

小结一下就是:客户端发出消息后,通过接入服务,按照 userId 投递到指定消息服务器,生成消息 Id, 依据最后一条消息时间,确认更新当前消息的时间戳(如果存在相同时间戳则后延)。然后将时间戳,以及消息 Id,通过 Ack 返回给客户端 ; 然后对上行消息使用 userId + 时间戳进行缓存以及持久化存储,后续业务操作均使用此时间戳。

以上业务流程我们称为上行流程,上行过程存储的消息为发件箱消息。

PS:关于消息ID,需要补充说明一下:

我们采用全局唯一的消息 ID 生成策略。保证消息可通过 ID 进行识别,排重。消息ID的结构如下图所示。

如何实现分布式场景下唯一 ID 生成,具体请阅读:《IM消息ID技术专题(三):解密融云IM产品的聊天消息ID生成策略》。

4.2 下行

消息节点在处理完上行流程后,消息按照目标用户投递到所在消息节点,进入下行流程。

下行过程,按照目标 userId 以及本消息在上行过程中生成的时间戳,计算是否需要更新时间戳(正向)。

如果需要更新则对时间戳进行加法操作,直到当前用户时间戳不重复。

如此处理后,目标用户的存储以及客户端接收到消息后的排重可以做到一致,并且可以做到同一个会话内的时间戳是有序的。从而保证同一个接收用户的消息不会出现乱序。

至此:我们已经介绍完了消息的下行交互过程,消息下行过程中的具体实现方式并不简单,以下将详细展开。

1)直发消息:

即服务端主动发送(给目标客户端)的消息:

  • 1)客户端 SDK 依据本地存储的最新消息时间戳判断,用来做排序等逻辑;
  • 2)对同一个用户直发消息1条,其他转通知。通知拉取时候客户端选择本地最新一条消息时间戳作为开始拉取时间;
  • 3)在消息发送过程中,如果上一条消息发送流程未结束,下一条消息则不用直发(s_msg),而是用通知(s_ntf)。

直发逻辑示意图:

2)通知拉取:

即服务端主动发送通知(给目标客户端):

  • 1)服务端在通知体中携带当前消息时间戳。投递给客户端;
  • 2)客户端收到通知后,比对本地消息时间戳,选择是否发拉取消息信令;
  • 3)服务端收到拉取消息信令后,以信令携带的时间戳为开始,查询出消息列表(200 条或者 5M),并给客户端应答;
  • 4)客户端收到后,给服务端 ack,服务端维护状态;
  • 5)客户端拉取消息时使用的时间戳,是客户端本地最新一条消息的时间戳。

示意图:

在上图中,3-7 步可能需要循环多次,有以下考虑:

  • a、客户端一次收到的消息过多,应答体积过于庞大,传输过程对网络质量要求更高, 因此按照数量以及消息体积分批次进行;
  • b、一次拉取到的消息过多,客户端处理会占用大量资源,可能会有卡顿等,体验较差。

3)服务端直发消息与通知拉取切换逻辑:

主要涉及到的是状态机的更新。

下面示意图集成直发消息与通知拉取过程针对状态机的更新:

至此,消息收发的整个核心流程介绍完毕,余下的内容将介绍多端在线的消息同步处理。

5、多端在线的消息同步

多端按照消息的上下行两个阶段,同样区分为发送方多端同步以及接收方多端同步。

5.1 发送方多端同步

在前面客户端连接 IM 服务过程中(见本文 4.1节),我们已经将同一个用户的多个客户端汇聚在了同一台服务,那么维护一个 userId 的多端就会变得很简单。

具体逻辑是:

  • 1)用户多个终端链接成功后,发送一条消息,这个消息到达 CMP(IM 接入服务) 后,CMP 做基础检查,然后获此用户的其他终端连接;
  • 2)服务把客户端上行的消息,封装为服务端下行消息,直接投递给用户的其他客户端。这样完成了发送方的多端抄送,然后将这条消息投递到 IM 服务。进入正常发送投递流程。

针对上面的第2)点,发送方的多端同步没有经过 IM Server,这么做的好处是:

  • 1)比较快速;
  • 2)经过越少的服务节点,出问题的几率越小。

5.2 接收方多端同步

具体逻辑是:

  • 1)IM 服务收到消息后,先判断接收方的投递范围,这个范围指的是接收方用户的哪些的终端要接收消息;
  • 2)IM 服务将范围以及当前消息,发送到 CMP,CMP 依据范围,匹配接收方的终端,然后投递消息。

接收方多端消息同步范围的应用场景,一般都是针对所有终端。

但有一些特殊业务:比如我在 A 客户端上,控制另外某个端的状态,可能需要一些命令消息, 这时候需要这个作用范围,针对性的投递消息。

到此,我们分享完了有关 IM 消息核心处理流程,通过层层拆解逻辑,提供了可靠的消息投递机制。

附录:更多IM架构设计的文章

浅谈IM系统的架构设计

简述移动端IM开发的那些坑:架构设计、通信协议和客户端

一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

一套原创分布式即时通讯(IM)系统理论架构方案

从零到卓越:京东客服即时通讯系统的技术架构演进历程

蘑菇街即时通讯/IM服务器开发之架构选择

腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT

微信后台基于时间序的海量数据冷热分级架构设计实践

微信技术总监谈架构:微信之道——大道至简(演讲全文)

如何解读《微信技术总监谈架构:微信之道——大道至简》

快速裂变:见证微信强大后台架构从0到1的演进历程(一)

17年的实践:腾讯海量产品的技术方法论

移动端IM中大规模群消息的推送如何保证效率、实时性?

现代IM系统中聊天消息的同步和存储方案探讨

IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?

IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议

IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token

WhatsApp技术实践分享:32人工程团队创造的技术神话

微信朋友圈千亿访问量背后的技术挑战和实践总结

王者荣耀2亿用户量的背后:产品定位、技术架构、网络方案等

IM系统的MQ消息中间件选型:Kafka还是RabbitMQ?

腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面

以微博类应用场景为例,总结海量社交系统的架构设计步骤

快速理解高性能HTTP服务端的负载均衡技术原理

子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践

知乎技术分享:从单机到2000万QPS并发的Redis高性能缓存实践之路

IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列

微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)

微信技术分享:微信的海量IM聊天消息序列号生成实践(容灾方案篇)

新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践

一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史

阿里技术分享:阿里自研金融级数据库OceanBase的艰辛成长之路

社交软件红包技术解密(一):全面解密QQ红包技术方案——架构、技术实现等

社交软件红包技术解密(二):解密微信摇一摇红包从0到1的技术演进

社交软件红包技术解密(三):微信摇一摇红包雨背后的技术细节

社交软件红包技术解密(四):微信红包系统是如何应对高并发的

社交软件红包技术解密(五):微信红包系统是如何实现高可用性的

社交软件红包技术解密(六):微信红包系统的存储层架构演进实践

社交软件红包技术解密(七):支付宝红包的海量高并发技术实践

社交软件红包技术解密(八):全面解密微博红包技术方案

社交软件红包技术解密(九):谈谈手Q红包的功能逻辑、容灾、运维、架构等

社交软件红包技术解密(十):手Q客户端针对2020年春节红包的技术实践

社交软件红包技术解密(十一):解密微信红包随机算法(含代码实现)

即时通讯新手入门:一文读懂什么是Nginx?它能否实现IM的负载均衡?

即时通讯新手入门:快速理解RPC技术——基本概念、原理和用途

多维度对比5款主流分布式MQ消息队列,妈妈再也不担心我的技术选型了

从游击队到正规军(一):马蜂窝旅游网的IM系统架构演进之路

从游击队到正规军(二):马蜂窝旅游网的IM客户端架构演进和实践总结

从游击队到正规军(三):基于Go的马蜂窝旅游网分布式IM系统技术实践

IM开发基础知识补课(六):数据库用NoSQL还是SQL?读这篇就够了!

瓜子IM智能客服系统的数据架构设计(整理自现场演讲,有配套PPT)

阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处

微信后台基于时间序的新一代海量数据存储架构的设计实践

IM开发基础知识补课(九):想开发IM集群?先搞懂什么是RPC!

阿里技术分享:电商IM消息平台,在群聊、直播场景下的技术实践

一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等

一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等

从新手到专家:如何设计一套亿级消息量的分布式IM系统

企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等

融云技术分享:全面揭秘亿级IM消息的可靠投递机制

>> 更多同类文章 ……

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入。同步发布链接是:http://www.52im.net/thread-3638-1-1.html

posted @ 2021-07-26 15:20 Jack Jiang 阅读(148) | 评论 (0)编辑 收藏

一、更新内容简介

本次为主要版本更新(本次更新内容见文末“MobileIMSDK v6.0更新内容 ”一节),强势升级,将同时支持TCP、UDP、WebSocket三种协议,精心封装之下,实现同一套API、三种协议同时并存。

可能是市面上唯一同时支持UDP+TCP+WebSocket三种协议的同类开源IM框架。

二、MobileIMSDK简介

MobileIMSDK 是一套专为移动端开发的原创IM通信层框架:

  • 历经8年、久经考验;
  • 超轻量级、高度提炼,lib包50KB以内;
  • 精心封装,一套API同时支持UDP、TCP、WebSocket三种协议(可能是全网唯一开源的);
  • 客户端支持 iOS、Android、标准Java、H5、小程序(开发中..)、Uniapp(开发中..);
  • 服务端基于Netty,性能卓越、易于扩展;👈
  • 可与姊妹工程 MobileIMSDK-Web 无缝互通实现网页端聊天或推送等;👈
  • 可应用于跨设备、跨网络的聊天APP、企业OA、消息推送等各种场景。

MobileIMSDK工程始于2013年10月,起初用作某产品的即时通讯底层实现,完全从零开发,技术自主可控!

您可能需要:查看关于MobileIMSDK的详细介绍

三、代码托管同步更新

OsChina.net

GitHub.com

四、MobileIMSDK设计目标

让开发者专注于应用逻辑的开发,底层复杂的即时通讯算法交由SDK开发人员,从而解偶即时通讯应用开发的复杂性。

五、MobileIMSDK框架组成

整套MobileIMSDK框架由以下5部分组成:

  1. Android客户端SDK:用于Android版即时通讯客户端,支持Android 2.3及以上,查看API文档
  2. iOS客户端SDK:用于开发iOS版即时通讯客户端,支持iOS 8.0及以上,查看API文档
  3. Java客户端SDK:用于开发跨平台的PC端即时通讯客户端,支持Java 1.6及以上,查看API文档
  4. H5客户端SDK:资料整理中,不日正式发布;
  5. 服务端SDK:用于开发即时通讯服务端,支持Java 1.7及以上版本,查看API文档

六、MobileIMSDK v6.0更新内容 

【重要说明】:

MobileIMSDK v6 为全新版本,新增了对WebSocket协议的优雅支持、多端互踢支持等! 查看详情

【新增重要特性】:

  1. 服务端新增WebSocket协议支持,一套API优雅支持TCP、UDP、WebSocket 3种协议;
  2. 支持多端互踢功能(可应对复杂的移动端网络变动逻辑对多端互踢算法的影响);

【解决的Bug】:

  1. [Andriod]解决了断线后,fireDisconnectedToServer()方法中的一处空指针隐患;
  2. [iOS] 修复了TCP版代码中,调用[ClientCoreSDK releaseCore]方法会触发自动登陆逻辑的bug;
  3. [服务端] 解决了UDP协议下,重连情况下的被踢者已被服务端注销会话后,客户端才发回登陆响应ACK应答,导致服务端错误地向未被踢者发出已登陆者重复登陆响应的问题;

【其它优化和提升】:

  1. [Andriod]废弃了SDK、Demo代码中的所有AsyncTask的使用;
  2. [Andriod]将所有可使用Lambda表达式的代码全部用Lambda进行了简化。
  3. [iOS] 解决了XCode12上编译SDK的.a包,打包成胖子.a时报“have the same architectures (arm64) and can't be in the same fat output file”的问题;
  4. [iOS] Demo中所有使用过时的UIAlertView改为UIAlertController实现;
  5. [iOS] 解决了iOS端SDK工程中两处因类名重构导致的在XCode12.5.1上编译出错。
  6. [服务端] 将服务端Demo中的Log4j日志框架升级为最新的Log4j2;
  7. [服务端] 服务端可控制是否为每条消息生成发送时间戳(可辅助用于客户端的消息排序逻辑等)。

七、相关链接

posted @ 2021-07-22 16:02 Jack Jiang 阅读(156) | 评论 (0)编辑 收藏

     摘要: 本文作者潘唐磊,腾讯WXG(微信事业群)开发工程师,毕业于中山大学。内容有修订。1、内容概述本文总结了企业微信的IM消息系统架构设计,阐述了企业业务给IM架构设计带来的技术难点和挑战,以及技术方案的对比与分析。同时总结了IM后台开发的一些常用手段,适用于IM消息系统。* 推荐阅读:企业微信团队分享的另一篇《企业微信客户端中组织架构数据的同步更新方案优化实战》也值得一读。学习交流:- 即时...  阅读全文

posted @ 2021-07-19 16:24 Jack Jiang 阅读(205) | 评论 (0)编辑 收藏

本文由喜马拉雅技术团队李乾坤原创,原题《推送系统实践》,感谢作者的无私分享。

1、引言

1.1 什么是离线消息推送

对于IM的开发者来说,离线消息推送是再熟悉不过的需求了,比如下图就是典型的IM离线消息通知效果。

1.2 Andriod端离线推送真心不易

移动端离线消息推送涉及的端无非就是两个——iOS端和Andriod端,iOS端没什么好说的,APNs是唯一选项。

Andriod端比较奇葩(主要指国内的手机),为了实现离线推送,各种保活黑科技层出不穷,随着保活难度的不断升级,可以使用的保活手段也是越来越少,有兴趣可以读一读我整理的下面这些文章,感受一下(文章是按时间顺序,随着Andriod系统保活难度的提升,不断进阶的)。

上面这几篇只是我整理的这方面的文章中的一部分,特别注意这最后一篇《Android保活从入门到放弃:乖乖引导用户加白名单吧(附7大机型加白示例)》。是的,当前Andriod系统对APP自已保活的容忍度几乎为0,所以那些曾今的保活手段在新版本系统里,几乎统统都失效了。

自已做保活已经没戏了,保离线消息推送总归是还得做。怎么办?按照现时的最佳实践,那就是对接种手机厂商的ROOM级推送通道。具体我就不在这里展开,有兴趣的地可以详读《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦》。

自已做保活、自建推送通道的时代(这里当然指的是Andriod端啦),离线消息推送这种系统的架构设计相对简单,无非就是每台终端计算出一个deviceID,服务端通过自建通道进行消息透传,就这么点事。

而在自建通道死翘翘,只能依赖厂商推送通道的如今,小米华为魅族OPPOvivo(这只是主流的几家)等等,手机型号太多,各家的推送API、设计规范各不相同(别跟我提什么统一推送联盟,那玩意儿我等他3年了——详见《万众瞩目的“统一推送联盟”上场了),这也直接导致先前的离线消息推送系统架构设计必须重新设计,以适应新时代的推送技术要求。

1.3 怎么设计合理呢

那么,针对不同厂商的ROOM级推送通道,我们的后台推送架构到底该怎么设计合理呢?

本文分享的离线消息推送系统设计并非专门针对IM产品,但无论业务层的差别有多少,大致的技术思路上都是相通的,希望借喜马拉雅的这篇分享能给正在设计大用户量的离线消息推送的你带来些许启发。

* 推荐阅读:喜马拉雅技术团队分享的另一篇《长连接网关技术专题(五):喜马拉雅自研亿级API网关技术实践》,有兴趣也可以一并阅读。

学习交流:

- 即时通讯/推送技术开发交流5群:215477170 [推荐]

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK

本文同步发布于:http://www.52im.net/thread-3621-1-1.html

2、技术背景

首先介绍下在喜马拉雅APP中推送系统的作用,如下图就是一个新闻业务的推送/通知。

离线推送主要就是在用户不打开APP的时候有一个手段触达用户,保持APP的存在感,提高APP的日活。

我们目前主要用推送的业务包括:

  • 1)主播开播:公司有直播业务,主播在开直播的时候会给这个主播的所有粉丝发一个推送开播提醒
  • 2)专辑更新:平台上有非常多的专辑,专辑下面是一系列具体的声音,比如一本儿小说是一个专辑,小说有很多章节,那么当小说更新章节的时候给所有订阅这个专辑的用户发一个更新的提醒:
  • 3)个性化、新闻业务等。

既然想给一个用户发离线推送,系统就要跟这个用户设备之间有一个联系的通道。

做过这个的都知道:自建推送通道需要App常驻后台(就是引言里提到的应用“保活”),而手机厂商因为省电等原因普遍采取“激进”的后台进程管理策略,导致自建通道质量较差。目前通道一般是由“推送服务商”去维护,也就是说公司内的推送系统并不直接给用户发推送(就是上节内容的这篇里提到的情况:《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦)。

这种情况下的离线推送流转流程如下:

国内的几大厂商(小米华为魅族OPPOvivo等)都有自己官方的推送通道,但是每一家接口都不一样,所以一些厂商比如小米、个推提供集成接口。发送时推送系统发给集成商,然后集成商根据具体的设备,发给具体的厂商推送通道,最终发给用户。

给设备发推送的时候,必须说清楚你要发的是什么内容:即title、message/body,还要指定给哪个设备发推送。

我们以token来标识一个设备, 在不同的场景下token的含义是不一样的,公司内部一般用uid或者deviceId标识一个设备,对于集成商、不同的厂商也有自己对设备的唯一“编号”,所以公司内部的推送服务,要负责进行uid、deviceId到集成商token 的转换。

3、整体架构设计

如上图所示,推送系统整体上是一个基于队列的流式处理系统。

上图右侧:是主链路,各个业务方通过推送接口给推送系统发推送,推送接口会把数据发到一个队列,由转换和过滤服务消费。转换就是上文说的uid/deviceId到token的转换,过滤下文专门讲,转换过滤处理后发给发送模块,最终给到集成商接口。

App 启动时:会向服务端发送绑定请求,上报uid/deviceId与token的绑定关系。当卸载/重装App等导致token失效时,集成商通过http回调告知推送系统。各个组件都会通过kafka 发送流水到公司的xstream 实时流处理集群,聚合数据并落盘到mysql,最终由grafana提供各种报表展示。

4、业务过滤机制设计

各个业务方可以无脑给用户发推送,但推送系统要有节制,因此要对业务消息有选择的过滤。

过滤机制的设计包括以下几点(按支持的先后顺序):

  • 1)用户开关:App支持配置用户开关,若用户关闭了推送,则不向用户设备发推送;
  • 2)文案排重:一个用户不能收到重复的文案,用于防止上游业务方发送逻辑出错;
  • 3)频率控制:每一个业务对应一个msg_type,设定xx时间内最多发xx条推送;
  • 4)静默时间:每天xx点到xx点不给用户发推送,以免打扰用户休息。
  • 5)分级管理:从用户和消息两维度进行分级控制。

针对第5点,具体来说就是:

  • 1)每一个msg/msg_type有一个level,给重要/高level业务更多发送机会;
  • 2)当用户一天收到xx条推送时,不是重要的消息就不再发给这些用户。

5、分库分表下的多维查询问题

很多时候,设计都是基于理论和经验,但实操时,总会遇到各种具体的问题。

喜马拉雅现在已经有6亿+用户,对应的推送系统的设备表(记录uid/deviceId到token的映射)也有类似的量级,所以对设备表进行了分库分表,以 deviceId 为分表列。

但实际上:经常有根据 uid/token 的查询需求,因此还需要建立以 uid/token 到 deviceId 的映射关系。因为uid 查询的场景也很频繁,因此uid副表也拥有和主表同样的字段。

因为每天会进行一两次全局推,且针对沉默用户(即不常使用APP的用户)也有专门的推送,存储方面实际上不存在“热点”,虽然使用了缓存,但作用很有限,且占用空间巨大。

多分表以及缓存导致数据存在三四个副本,不同逻辑使用不同副本,经常出现不一致问题(追求一致则影响性能), 查询代码非常复杂且性能较低。

最终我们选择了将设备数据存储在tidb上,在性能够用的前提下,大大简化了代码。

6、特殊业务的时效性问题

6.1 基本概念

推送系统是基于队列的,“先到先推”。大部分业务不要求很高的实时性,但直播业务要求半个小时送达,新闻业务更是“欲求不满”,越快越好。

若进行新闻推送时:队列中有巨量的“专辑更新”推送等待处理,则专辑更新业务会严重干扰新闻业务的送达。

6.2 这是隔离问题?

一开始我们认为这是一个隔离问题:比如10个消费节点,3个专门负责高时效性业务、7个节点负责一般业务。当时队列用的是rabbitmq,为此改造了 spring-rabbit 支持根据msytype将消息路由到特定节点。

该方案有以下缺点:

  • 1)总有一些机器很忙的时候,另一些机器在“袖手旁观”;
  • 2)新增业务时,需要额外配置msgType到消费节点的映射关系,维护成本较高;
  • 3)rabbitmq基于内存实现,推送瞬时高峰时占用内存较大,进而引发rabbitmq 不稳定。

6.3 其实是个优先级问题

后来我们觉察到这是一个优先级问题:高优先级业务/消息可以插队,于是封装kafka支持优先级,比较好的解决了隔离性方案带来的问题。具体实现是建立多个topic,一个topic代表一个优先级,封装kafka主要是封装消费端的逻辑(即构造一个PriorityConsumer)。

备注:为描述简单,本文使用 consumer.poll(num) 来描述使用 consumer 拉取 num 个消息,与真实 kafka api 不一致,请知悉。

PriorityConsumer实现有三种方案,以下分别阐述。

1)poll到内存后重新排序:java 有现成的基于内存的优先级队列PriorityQueue 或PriorityBlockingQueue,kafka consumer 正常消费,并将poll 到的数据重新push到优先级队列。

  • 1.1)如果使用有界队列,队列打满后,后面的消息优先级再高也put 不进去,失去“插队”效果;
  • 1.2)如果使用无界队列,本来应堆在kafka上的消息都会堆到内存里,OOM的风险很大。

2)先拉取高优先级topic的数据:只要有就一直消费,直到没有数据再消费低一级topic。消费低一级topic的过程中,如果发现有高一级topic消息到来,则转向消费高优先级消息。

该方案实现较为复杂,且在晚高峰等推送密集的时间段,可能会导致低优先级业务完全失去推送机会。

3)优先级从高到低,循环拉取数据:

一次循环的逻辑为:

consumer-1.poll(topic1-num);

cosumer-i.poll(topic-i-num);

consumer-max.priority.poll(topic-max.priority-num)

如果topic1-num=topic-i-num=topic-max.priority-num,则该方案是没有优先级效果的。topic1-num 可以视为权重,我们约定:topic-高-num=2 * topic-低-num,同一时刻所有topic 都会被消费,通过一次消费数量的多少来变相实现“插队效果”。具体细节上还借鉴了“滑动窗口”策略来优化某个优先级的topic 长期没有消息时总的消费性能。

从中我们可以看到,时效问题先是被理解为一个隔离问题,后被视为优先级问题,最终转化为了一个权重问题。

7、过滤机制的存储和性能问题

在我们的架构中,影响推送发送速度的主要就是tidb查询和过滤逻辑,过滤机制又分为存储和性能两个问题。

这里我们以xx业务频控限制“一个小时最多发送一条”为例来进行分析。

第一版实现时:redis kv 结构为 <deviceId_msgtype,已发送推送数量>

频控实现逻辑为:

  • 1)发送时,incr key,发送次数加1;
  • 2)如果超限(incr命令返回值>发送次数上限),则不推送;
  • 3)若未超限且返回值为1,说明在msgtype频控周期内第一次向该deviceId发消息,需expire key设置过期时间(等于频控周期)。

上述方案有以下缺点:

  • 1)目前公司有60+推送业务, 6亿+ deviceId,一共6亿*60个key ,占用空间巨大;
  • 2)很多时候,处理一个deviceId需要2条指令:incr+expire。

为此,我们的解决方法是:

  • 1)使用pika(基于磁盘的redis)替换redis,磁盘空间可以满足存储需求;
  • 2)委托系统架构组扩充了redis协议,支持新结构ehash。

ehash基于redis hash修改,是一个两级map <key,field,value>,除了key 可以设置有效期外,field也可以支持有效期,且支持有条件的设置有效期。

频控数据的存储结构由<deviceId_msgtype,value>变为 <deviceId,msgtype,value>,这样对于多个msgtype,deviceId只存一次,节省了占用空间。

incr 和 expire 合并为1条指令:incr(key,filed,expire),减少了一次网络通信:

  • 1)当field未设置有效期时,则为其设置有效期;
  • 2)当field还未过期时,则忽略有效期参数。

因为推送系统重度使用 incr 指令,可以视为一条写指令,大部分场景还用了pipeline 来实现批量写的效果,我们委托系统架构组小伙伴专门优化了pika 的写入性能,支持“写模式”(优化了写场景下的相关参数),qps达到10w以上。

ehash结构在流水记录时也发挥了重要作用,比如<deviceId,msgId,100001002>,其中 100001002 是我们约定的一个数据格式示例值,前中后三个部分(每个部分占3位)分别表示了某个消息(msgId)针对deviceId的发送、接收和点击详情,比如头3位“100”表示因发送时处于静默时间段所以发送失败。

附录:更多消息推送技术文章

iOS的推送服务APNs详解:设计思路、技术原理及缺陷等

信鸽团队原创:一起走过 iOS10 上消息推送(APNS)的坑

Android端消息推送总结:实现原理、心跳保活、遇到的问题等

扫盲贴:认识MQTT通信协议

一个基于MQTT通信协议的完整Android推送Demo

IBM技术经理访谈:MQTT协议的制定历程、发展现状等

求教android消息推送:GCM、XMPP、MQTT三种方案的优劣

移动端实时消息推送技术浅析

扫盲贴:浅谈iOS和Android后台实时消息推送的原理和区别

绝对干货:基于Netty实现海量接入的推送服务技术要点

移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)

为何微信、QQ这样的IM工具不使用GCM服务推送消息?

极光推送系统大规模高并发架构的技术实践分享

从HTTP到MQTT:一个基于位置服务的APP数据通信实践概述

魅族2500万长连接的实时消息推送架构的技术实践分享

专访魅族架构师:海量长连接的实时消息推送系统的心得体会

深入的聊聊Android消息推送这件小事

基于WebSocket实现Hybrid移动应用的消息推送实践(含代码示例)

一个基于长连接的安全可扩展的订阅/推送服务实现思路

实践分享:如何构建一套高可用的移动端消息推送系统?

Go语言构建千万级在线的高并发消息推送系统实践(来自360公司)

腾讯信鸽技术分享:百亿级实时消息推送的实战经验

百万在线的美拍直播弹幕系统的实时推送技术实践之路

京东京麦商家开放平台的消息推送架构演进之路

了解iOS消息推送一文就够:史上最全iOS Push技术详解

基于APNs最新HTTP/2接口实现iOS的高性能消息推送(服务端篇)

解密“达达-京东到家”的订单即时派发技术原理和实践

技术干货:从零开始,教你设计一个百万级的消息推送系统

长连接网关技术专题(四):爱奇艺WebSocket实时推送网关技术实践

喜马拉雅亿级用户量的离线消息推送系统架构设计实践

>> 更多同类文章 ……

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入。同步发布链接是:http://www.52im.net/thread-3621-1-1.html

posted @ 2021-07-12 15:31 Jack Jiang 阅读(454) | 评论 (0)编辑 收藏

本文由阿里闲鱼技术团队祈晴分享,本次有修订和改动,感谢作者的技术分享。

1、内容概述

本文总结了阿里闲鱼技术团队使用Flutter在对闲鱼IM进行移动端跨端改造过程中的技术实践等,文中对比了传统Native与现在大热的Flutter跨端方案在一些主要技术实现上的差异,以及针对Flutter技术特点的具体技术实现,值得同样准备使用Flutter开发IM的技术同行们借鉴和参考。

学习交流:

- 即时通讯/推送技术开发交流5群:215477170 [推荐]

- 移动端IM开发入门文章:新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK

本文同步发布于:http://www.52im.net/thread-3615-1-1.html

2、闲鱼IM现状

闲鱼IM的移动端框架构建于2016至2017年间,期间经过多次迭代升级导致历史包袱累积多,后面又经历IM界面的Flutter化,从而造成了客户端架构愈加复杂。

从开发层面总结闲鱼IM移动端当前架构主要存在如下几个问题:

  • 1)研发效率较低:当前架构涉及到Android/iOS双端的逻辑代码以及Flutter的UI代码,定位问题往往只能从Flutter UI表相倒查到Native逻辑层;
  • 2)架构层次较差:架构设计上分层不清晰,业务逻辑夹杂在核心的逻辑层致使代码变更风险大;
  • 3)性能测试略差:核心数据源存储Native内存,需经Flutter Plugin将数据源序列化上抛Flutter侧,在大批量数据源情况下性能表现较差。

从产品层面总结闲鱼IM移动端当前架构的主要问题如下:

  • 1)定位问题困难:线上舆情反馈千奇百怪,测试始终无法复现相关场景,因此很多时候只能靠现象猜测本质;
  • 2)疑难杂症较多:架构的不稳定性造成出现的问题反复出现,当前疑难杂症主要包括未读红点计数、iPhone5C低端机以及多媒体发送等多个问题;
  • 3)问题差异性大:Android和iOS两端逻辑代码差异大,包括埋点逻辑都不尽相同,排查问题根源时双端都会有不同根因,解决方案也不相同。

3、业界的移动端跨端方案

为解决当前IM的技术痛点,闲鱼今年特起关于IM架构升级项目,重在解决客户端中Andriod和iOS双端一致性的痛点,初步设想方案就是实现跨端统一的Android/iOS逻辑架构。

在当前行业内跨端方案可初步归类如下图架构:

在GUI层面的跨端方案有WeexReactNative、H5、Uni-APP等,其内存模型大多需要通过桥接到Native模式存储。

在逻辑层面的跨端方案大致有C/C++等与虚拟机无关语言实现跨端,当然汇编语言也可行。

此外有两个独立于上述体系之外的架构就是Flutter和KMM(谷歌基于Kotlin实现类似Flutter架构),其中Flutter运行特定DartVM,将内存数据挂载其自身的isolate中。

考虑闲鱼是Flutter的前沿探索者,方案上优先使用Flutter。然而Flutter的isolate更像一个进程的概念(底层实现非使用进程模式),相比Android,同一进程场景中,Android的Dalvik虚拟机多个线程运行共享一个内存Heap,而DartVM的Isolate运行隔离各自的Heap,因而isolate之间通讯方式比较繁琐(需经过序列化反序列化过程)。

整个模型如下图所示:

若按官方混合架构实现Flutter应用,开启多个FlutterAcitivty/FlutterController,底层会生成多个Engine,对应会存在多个isolate,而isolate通讯类似于进程通讯(类似socket或AIDL),这里借鉴闲鱼FlutterBoost的设计理念,FlutterIM架构将多个页面的Engine共享,则内存模型就天然支持共享读取。

原理图如下:

4、闲鱼IM基于Flutter的架构设计

4.1 新老架构对比

如下图所示:是一个老架构方案,其核心问题主要集中于Native逻辑抽象差,其中逻辑层面还设计到多线程并发使得问题倍增,Android/iOS/Flutter交互繁杂,开发维护成本高,核心层耦合较为严重,无插拔式概念.

考虑到历史架构的问题,演进如下新架构设计:

如上图所示,架构从上至下依次为:

  • 1)业务层;
  • 2)分发层;
  • 3)逻辑层;
  • 4)数据源层。

数据源层来源于推送或网络请求,其封装于Native层,通过Flutter插件将消息协议数据上抛到Flutter侧的核心逻辑层,处理完成后变成Flutter DB的Enitity实体,实体中挂载一些消息协议实体。

核心逻辑层将繁杂数据扁平化打包挂载到分发层中的会话内存模型数据或消息内存模型数据,最后通过观察者模式的订阅分发到业务逻辑中。

Flutter IM重点集中改造逻辑层和分发层,将IM核心逻辑和业务层面数据模型进行封装隔离,核心逻辑层和数据库交互后将数据封装到分发层的moduleData中,通过订阅方式分发到业务层数据模型中。

此外在IM模型中DB也是重点依赖的,个人对DB数据库管理进行全面封装解,实现一种轻量级,性能佳的Flutter DB管理框架。

4.2 DB存储模型

Flutter IM架构的DB存储依赖数据库插件,目前主流插件是Sqflite

其存储模型如下:

依据上图Sqflite插件的DB存储模型会有2个等待队列:

  • 一个是Flutter层同步执行队列;
  • 一个是Native层的线程执行队列。

其Android实现机制是HandlerThread,因此Query/Save读写在会同一线程队列中,导致响应速度慢,容易造成DB SQL堆积,此外缺失缓存模型。

于是个人定制如下改进方案:

Flutter侧通过表的主键设计查询时候会优先从Entity Cache层去获取,若缓存不存在,则通过Sqflite插件查询。

同时改造Sqflite插件成支持sync/Async同步异步两种方式操作,对应到Native侧也会有同步线程队列和异步线程队列,保证数据吞吐率。但是这里建议查询使用异步,存储使用同步更稳妥,主要怕出现多个相同的数据元model同一时间进入异步线程池中,存储先后顺序无法有效的保证。

4.3 ORM数据库方案

IM架构重度依赖DB数据库,而当前业界还没有一个完备的数据库ORM管理方案,参考了Android的OrmLite/GreenDao,个人自行设计一套Flutter ORM数据库管理方案。

其核心思想如下:

由于Flutter不支持反射,因此无法直接像Android的开源数据库方式操作,但可通过APT方式,将Entity和Orm Entity绑定于一身,操作OrmEntity即操作Entity,整个代码风格设计也和OrmLite极其相似。

参考代码如下:

4.4 IM内存数据模型

基于Flutter的IM移动端架构在内存数据模型主要划分为会话和消息两个颗粒度:

  • 1)会话内存数据模型交托于SessionModuleData:会话内存数据有一个根节点RootNotice,然后其挂载PSessionMessageNotice(这里PSessionMessageNotice是ORM映射的会话DB表模型)子节点集合。
  • 2)消息内存数据模型交托于MessageModuleData:消息内存数据会有一个MessageConatiner容器管理,其内部挂载此会话中的PMessage(PMessage是ORM映射的消息DB表模型)消息集合。

依据上一章节,PSessionMessageNotice设计了一个OrmEnitity Cache,考虑到IM中会话数是有限的,因此PSessionMessageNotice都是直接缓存到Cache中。

这种做法的好处是各地去拿会话数据元时候都是缓存中同一个对象,容易保证多次重复读写的数据一致性。而PSessionMessageNotice考虑到其数量可以无限多的特殊性,因此这里将其挂载到MessageContainer的内存管理中,在退出会话的时机会校验容器中PMessage集合的数量,适当缩容可以减少内存开销。

模型如下图所示:

4.5 状态管理方案

基于Flutter的IM移动端架构状态管理方案比较简单,对数据源Session/Message维度使用观察者模式的订阅分发方式实现,架构类似于EventBus模式,页面级的状态管理无论使用fish-redux、scopeModel或者provider几乎影响面不大,核心还是需保留一种插拔式抽象更重要。

架构如下图:

4.6 IM同步模型方案

当前现状的消息同步模型:

如上图所示是,模型中存在ACCS Thread/Main Thread/Region Thread等多线程并发场景,导致易出现多线程高并发的问题。

native的推送和网络请求同步的隔离方案通过Lock的锁机制,并且通过队列降频等方式处理,流程繁琐且易出错。整体通过Region Version Gap去判断是否有域空洞,进而执行域同步补充数据。

改进的同步模型如下:

如上图所示,在Flutter侧天然没多线程场景,通过一种标记位的转化同步异步实现类似Handler消息队列,架构清晰简约了很多,避免锁带来的开销以及同步问题。

5、本次改造进展以及性能对比

1)针对架构层面:

在基于Flutter的IM架构中,重点将双端逻辑差异性统一成同一份Dart代码,完全磨平Android/iOS的代码差异性带来的问题。

带来的好处很明显:

  • 1)降低开发维护、测试回归、视觉验收的一半成本,极大提高研发效率;
  • 2)架构上进行重构分层,实现一种解耦合,插拔式的IM架构;
  • 3)同时Native到Flutter侧的大量数据上抛序列化过程改造程Flutter引用传递,解决极限测试场景下的私聊卡顿问题。

2)针对线上舆情:

  • 1)补齐UT和TLog的集团日志方式做到可追踪,可排查;
  • 2)针对于很多现存的疑难杂症重点集中专项解决,比如iphone5C的架构在Flutter侧统一规划;
  • 3)未读红点计数等问题也在架构模型升级中修复;
  • 4)此外多媒体音视频发送模块进行改造升级。

3)性能数据对比:

当IM架构的逻辑层和UI层都切换成Flutter后,和原先架构模式初步对比,整体内存水位持平。

其中:

  • 1)私聊场景下小米9测试结构内存下降40M,功耗降低4mah,CPU降低1%;
  • 2)极限测试场景下新架构内存数据相比于旧架构有一个较为明显的改观(主要由于两个界面都使用Flutter场景下,页面切换的开销降低很多)。

6、未来展望

JS跨端不安全,C++跨端成本有点高,Flutter会是一个较好选择。彼时闲鱼FlutterIM架构升级根本目的从来不是因Flutter而Flutter,是由于历史包袱的繁重,代码层面的维护成本高,新业务的扩展性差,人力配比不协调以及疑难杂症的舆情持续反馈等等因素造成我们不得不去探索新方案。

经过闲鱼IM超复杂业务场景验证Flutter模式的逻辑跨端可行性,闲鱼在Flutter路上会一直保持前沿探索,最后能反馈到生态圈。

总结一句话,探索过程在于你勇于迈出第一步,后面才会不断惊喜发现。

(原文链接:点此进入,本次有修订和改动)

附录:更多文章汇总

[1] 更多阿里团队的文章分享:

阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处

现代IM系统中聊天消息的同步和存储方案探讨

阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史

阿里技术分享:阿里自研金融级数据库OceanBase的艰辛成长之路

来自阿里OpenIM:打造安全可靠即时通讯服务的技术实践分享

钉钉——基于IM技术的新一代企业OA平台的技术挑战(视频+PPT) [附件下载]

阿里技术结晶:《阿里巴巴Java开发手册(规约)-华山版》[附件下载]

重磅发布:《阿里巴巴Android开发手册(规约)》[附件下载]

作者谈《阿里巴巴Java开发手册(规约)》背后的故事

《阿里巴巴Android开发手册(规约)》背后的故事

干了这碗鸡汤:从理发店小弟到阿里P10技术大牛

揭秘阿里、腾讯、华为、百度的职级和薪酬体系

淘宝技术分享:手淘亿级移动端接入层网关的技术演进之路

难得干货,揭秘支付宝的2维码扫码技术优化实践之路

淘宝直播技术干货:高清、低延时的实时视频直播技术解密

阿里技术分享:电商IM消息平台,在群聊、直播场景下的技术实践

阿里技术分享:闲鱼IM基于Flutter的移动端跨端改造实践

 

[2] 更多IM开发综合文章:

新手入门一篇就够:从零开发移动端IM

移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”

移动端IM开发者必读(二):史上最全移动弱网络优化方法总结

从客户端的角度来谈谈移动端IM的消息可靠性和送达机制

现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障

移动端IM中大规模群消息的推送如何保证效率、实时性?

移动端IM开发需要面对的技术问题

开发IM是自己设计协议用字节流好还是字符流好?

IM消息送达保证机制实现(一):保证在线实时消息的可靠投递

IM消息送达保证机制实现(二):保证离线消息的可靠投递

如何保证IM实时消息的“时序性”与“一致性”?

一个低成本确保IM消息时序的方法探讨

IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?

IM群聊消息如此复杂,如何保证不丢不重?

谈谈移动端 IM 开发中登录请求的优化

移动端IM登录时拉取数据如何作到省流量?

浅谈移动端IM的多点登录和消息漫游原理

完全自已开发的IM该如何设计“失败重试”机制?

通俗易懂:基于集群的移动端IM接入层负载均衡方案分享

微信对网络影响的技术试验及分析(论文全文)

开源IM工程“蘑菇街TeamTalk”的现状:一场有始无终的开源秀

如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源

子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践

微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)

融云技术分享:解密融云IM产品的聊天消息ID生成策略

IM开发基础知识补课(六):数据库用NoSQL还是SQL?读这篇就够了!

适合新手:从零开发一个IM服务端(基于Netty,有完整源码)

拿起键盘就是干:跟我一起徒手开发一套分布式IM系统

适合新手:手把手教你用Go快速搭建高性能、可扩展的IM系统(有源码)

IM里“附近的人”功能实现原理是什么?如何高效率地实现它?

IM“扫一扫”功能很好做?看看微信“扫一扫识物”的完整技术实现

IM消息ID技术专题(一):微信的海量IM聊天消息序列号生成实践(算法原理篇)

IM消息ID技术专题(二):微信的海量IM聊天消息序列号生成实践(容灾方案篇)

IM消息ID技术专题(三):解密融云IM产品的聊天消息ID生成策略

IM消息ID技术专题(四):深度解密美团的分布式ID生成算法

IM消息ID技术专题(五):开源分布式ID生成器UidGenerator的技术实现

IM消息ID技术专题(六):深度解密滴滴的高性能ID生成器(Tinyid)

IM开发宝典:史上最全,微信各种功能参数和逻辑规则资料汇总

IM开发干货分享:我是如何解决大量离线消息导致客户端卡顿的

零基础IM开发入门(一):什么是IM系统?

零基础IM开发入门(二):什么是IM系统的实时性?

零基础IM开发入门(三):什么是IM系统的可靠性?

零基础IM开发入门(四):什么是IM系统的消息时序一致性?

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

IM开发干货分享:有赞移动端IM的组件化SDK架构设计实践

一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等

IM扫码登录技术专题(一):微信的扫码登录功能技术原理调试分析

IM扫码登录技术专题(二):市面主流的扫码登录技术原理调试分析

IM扫码登录技术专题(三):通俗易懂,IM扫码登录功能详细原理一篇就够

理解IM消息“可靠性”和“一致性”问题,以及解决方案探讨

阿里技术分享:闲鱼IM基于Flutter的移动端跨端改造实践

>> 更多同类文章 ……

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入。同步发布链接是:http://www.52im.net/thread-3615-1-1.html

posted @ 2021-07-05 14:55 Jack Jiang 阅读(193) | 评论 (0)编辑 收藏

仅列出标题
共47页: First 上一页 19 20 21 22 23 24 25 26 27 下一页 Last 
Jack Jiang的 Mail: jb2011@163.com, 联系QQ: 413980957, 微信: hellojackjiang