即时消息技术剖析与实战

1.架构与特性:一个完整的IM系统是怎样的?

当服务端有消息需要推送给客户端时,也是将经过业务层处理的消息先递交给接入层,再由接入层通过网络发送到客户端。

此外,在很多基于私有通信协议的IM系统实现中,接入服务还提供协议的编解码工作,编解码实际主要是为了节省网络流量,系统会针对传输的内容进行紧凑的编码(比如Protobuf),为了让业务处理时不需要关心这些业务无关的编解码工作,一般由接入层来处理。

另外,还有session维护的工作很多时候也由接入服务来实现,session的作用是标识“哪个用户在哪个TCP连接”,用于后续的消息推送能够知道,如何找到接收人对应的连接来发送。

2.消息收发架构:为你的App,加上实时通信功能?

 

3.轮询与长连接:如何解决消息的实时到达问题?

短轮询和长轮询之所以没法做到基于事件的完全的“边缘触发(当状态变化时,发生一个IO事件)”,这是因为服务端在有新消息产生时,没有办法直接向客户端进行推送。

这里的根本原因在于短轮询和长轮询是基于HTTP协议实现的,由于HTTP是一个无状态协议,同一客户端的多次请求对于服务端来说并没有关系,也不会去记录客户端相关的连接信息。

因此,所有的请求只能由客户端发起,服务端由于并不记录客户端状态,当服务端接收到新消息时,没法找到对应的客户端来进行推送。

4.ACK机制:如何保证消息的可靠投递?

针对第一部分123,我们通过客户端A的超时重发和IM服务器的去重机制,基本就可以解决问题;针对第二部分4,业界一般参考TCP协议的ACK机制,实现一套业务层的ACK协议。

IM服务器在推送消息时,携带一个标识SID(安全标识符,类似TCP的sequenceId),推送出消息后会将当前消息添加到“待ACK消息列表”,客户端B成功接收完消息后,会给IM服务器回一个业务层的ACK包,包中携带有本条接收消息的SID,IM服务器接收后,会从“待ACK消息列表”记录中删除此条消息,本次推送才算真正结束。

超时重传带来的问题是重复推送消息,需要业务方去重。

消息完整性检查

5.消息序号生成器:如何保证你的消息不会乱序?

单机本地化的时钟或者序号都存在问题,那么如果有一个全局的时钟或者序号是不是就能解决这个问题了呢?所有的消息的排序都依托于这个全局的序号,这样就不存在时钟不同步的问题了。

而且这种“全局序号生成器”可以通过多种方式来实现,常见的比如Redis的原子自增命令incr,DB自带的自增id,或者类似Twitter的snowflake算法、“时间相关”的分布式序号生成服务等。

有了“时序基准”,是不是就能确保消息能按照“既定顺序”到达接收方呢?答案是并不一定能做到。原因在于下面两点。

  1. IM服务器都是集群化部署,每台服务器的机器性能存在差异,因此处理效率有差别,并不能保证先到的消息一定可以先推送到接收方,比如有的服务器处理得慢,或者刚好碰到一次GC,导致它接收的更早消息,反而比其他处理更快的机器更晚推送出去。

  2. IM服务端接收到发送方的消息后,之后相应的处理一般都是多线程进行处理的,比如“取序号”“暂存消息”“查询接收方连接信息”等,由于多线程处理流程,并不能保证先取到序号的消息能先到达接收方,这样的话对于多个接收方看到的消息顺序可能是不一致的。

所以一般还需要端上能支持对消息的“本地整流”。

根据包的ID,对一定时间内的消息做一个整流和排序,这样即使服务端处理多条消息时出现乱序,仍然可以在最终推送给客户端时整流为有序的。

 

在即时消息收发场景中,用于保证消息接收时序的序号生成器为什么可以不是全局递增的?

答: 这是由业务场景决定的,这个群的消息和另一个群的消息在逻辑上是完全隔离的,只要保证消息的序号在群这样的一个局部范围内是递增的即可; 当然如果可以做到全局递增最好,但是会浪费很多的资源,却没有带来更多的收益。

6.HttpDNS和TLS:你的消息聊天真的安全吗?

  1. 消息传输安全性。“访问入口安全”和“传输链路安全”是基于互联网的即时消息场景下的重要防范点。针对“访问入口安全”可以通过HttpDNS来解决路由器被恶意篡改和运营商的LocalDNS问题;而TLS传输层加密协议是保证消息传输过程中不被截获、篡改、伪造的常用手段。

  2. 消息存储安全性。针对账号密码的存储安全可以通过“高强度单向散列算法”和“加盐”机制来提升加密密码可逆性;对于追求极致安全性的即时消息场景并且政策允许的情况下,服务端应该尽量不存储消息内容,并且采用“端到端加密”方式来提供更加安全的消息传输保护。

  3. 消息内容安全性。针对消息内容的安全识别可以依托“敏感词库”“图片识别”“OCR和语音转文字”“外链爬虫抓取分析”等多种手段,并且配合“联动惩罚处置”来进行风险识别的后置闭环。

7.分布式锁和原子性:你看到的未读消息提醒是真的吗?

如果说会话未读数和总未读数拆开维护的话,原因是:对于高频使用的“总未读”,如果每次都通过聚合所有会话未读来获取,用户的互动会话不多的话,性能还可以保证;一旦会话数比较多,由于需要多次从存储获取,容易出现某些会话未读由于超时等原因没取到,导致总未读数计算少了。

但是如果拆开又会导致一致性丢失了,所以引入分布式锁、事务保证保证未读更新的原子性

8.智能心跳机制:解决网络的不确定性

心跳检测的几种实现方式

TCP Keepalive

默认的三个配置项:心跳周期是2小时,失败后再重试9次,超时时间75s。三个配置项均可以调整。

这样来看,TCP的Keepalive作为系统层TCP/IP协议栈的已有实现,不需要其他开发工作量,用来作为连接存活与否的探测机制是非常方便的;上层应用只需要处理探测后的连接异常情况即可,而且心跳包不携带数据,带宽资源的浪费也是最少的。

应用层心跳

为了解决TCP Keepalive存在的一些不足的问题,很多IM服务使用应用层心跳来提升探测的灵活性和准确性。应用层心跳实际上就是客户端每隔一定时间间隔,向IM服务端发送一个业务层的数据包告知自身存活。

智能心跳

就是让心跳间隔能够根据网络环境来自动调整,通过不断自动调整心跳间隔的方式,逐步逼近NAT超时临界点,在保证NAT不超时的情况下尽量节约设备资源。(很多运营商为了节省资源和降低自身网关的压力,对于一段时间没有数据收发的连接,运营商会将它们从NAT映射表中清除掉,而且这个清除动作也不会被手机端和IM服务端感知到。)

9.分布式一致性:让你的消息支持多终端漫游

要支持消息多终端漫游一般来说需要两个前置条件:一种是通过设备维度的在线状态来实现,一种是通过离线消息存储来实现。

 怎么做到离线消息按需拉取?

 

离线消息存储超过限额了怎么办?

如果离线消息存储容量超过限制,部分增量消息被淘汰掉了,会导致根据客户端最新版本号获取增量消息失败。

这种情况的处理方式可以是:直接下推所有离线消息或者从消息的联系人列表和索引表中获取最近联系人的部分最新的消息,后续让客户端在浏览时再根据“时间相关”的消息ID来按页获取剩余消息,对于重复的消息让客户端根据消息ID去重。

因为消息索引表里只存储消息,并不存储操作信令,这种处理方式可能会导致部分操作信令丢失,但不会出现丢消息的情况。因此,对于资源充足且对一致性要求高的业务场景,可以尽量提升离线消息存储的容量来提升离线存储的命中率。

离线存储写入失败了会怎么样?

可以在存储离线消息时不仅存储当前版本号,还存储上一条消息或信令的版本号,获取消息时不仅要求客户端最新版本号在离线消息存储中存在,同时还要求离线存储的消息通过每条消息携带的上一版本号和当前版本号能够整体串联上,否则如果离线存储写入失败,所有消息的这两个版本号是没法串联上的。

这样,当用户上线拉取离线消息时,IM服务端发现该用户的离线消息版本号不连续的情况后,就可以用和离线消息存储超限一样的处理方式,从消息的联系人列表和索引表来获取最近联系人的部分最新的消息。

消息打包下推和压缩

对于较长时间不上线的用户,上线后需要拉取的离线消息比较多,采用整体打包的方式来把多条消息合并成一个大包推下去,并且再压缩包

发送方设备的同步问题

给消息的发送方设备仍然下推一条只携带版本号的单独的消息,发送方设备接收到该消息只需要更新本地的最新版本号就能做到和服务端的版本号同步了。

如果用户的离线消息比较多,有没有办法来减少用户上线时离线消息的数据传输量?

1 将用户的所有离线消息,按联系人进行分开; 2 用户登录后进入与联系人的聊天窗口时,首先加载与该联系人的最近的10条离线消息; 3 当用户用手滑动手机屏幕的时候,再分页拉取10条。

10.自动智能扩缩容:直播互动场景中峰值流量的应对

以10w人的房间来说,假设有50台网关机,那么平均每台网关机上这个直播间的用户应该有2000人,我们完全没有必要去“精准”确认这个直播间的用户都在哪台网关机上,只需要把这个直播间的消息都全量“投递”给所有网关机即可,每台网关机也只需要在本地维护一个“某个房间的哪些用户的连接在本机”,最终由网关机把消息下推给本机上当前直播间的在线用户。优化后的直播消息下推架构大概是这样:

 在核心服务里,消息的发送行为和处理一般不容易出现瓶颈,一个10w人的直播间里每秒的互动行为一般超不过1000,在这一步,我们不希望和容易出现瓶颈的消息下推业务混在一起。因此,我们可以把消息的发和收从接入层到业务处理层都进行隔离拆分。整个系统进行微服务化改造后大概就是下面这样:

 

通过微服务拆分后,你就需要考虑如何对拆分出来的服务进行扩容了,因为在平时没有高热度的直播时,考虑到成本的因素,一般不会让整个服务的集群规模太大。当有大型直播活动时,我们可以通过监控服务或者机器的一些关键指标,在热度快要到达瓶颈点时来进行扩容,整个过程实际不需要人工参与,完全可以做成自动化。

智能负载均衡

11.服务高可用:保证核心链路稳定性的流控和熔断机制

流量控制

“流控”这个词你应该不陌生,当我们坐飞机航班延误或者被取消时,航空公司给出的原因经常就是“因为目的机场流量控制”。对于机场来说,当承载的航班量超过极限负荷时,就会限制后续出港和到港的航班来进行排队等候,从而保护整个机场的正常运转。

漏桶算法 

令牌桶算法

流控依赖资源瓶颈

有时入口流量太大,导致实现流控的资源出现访问瓶颈,反而影响了正常业务的可用性。在微博消息箱业务中,就发生过流控使用的Redis资源由于访问量太大导致出现不可用的情况。

针对这种情况,我们可以通过“本地批量预取”的方式来降低对资源的压力。

所谓的“本地批量预取”,是指让使用限流服务的业务进程,每次从远程资源预取多个令牌在本地缓存,处理限流逻辑时先从本地缓存消耗令牌,本地消费完再触发从远程资源获取到本地缓存,如果远程获取资源时配额已经不够了,本次请求就会被抛弃。

通过“本地批量预取”的方式,能大幅降低对资源的压力,比如每次预取10个令牌,那么相应地对资源的压力能降低到1/10。

自动熔断机制

虽然服务间的调用能够通过超时控制来降低被影响的程度,但在很多情况下,单纯依赖超时控制很难避免依赖服务性能恶化的问题。这种情况下,需要能快速“熔断”对这些性能出现问题的依赖调用。

一种常见的方式是手动通过开关来进行依赖的降级,微博的很多场景和业务都有用到开关来实现业务或者资源依赖的降级。

另一种更智能的方式是自动熔断机制。自动熔断机制主要是通过持续收集被依赖服务或者资源的访问数据和性能指标,当性能出现一定程度的恶化或者失败量达到某个阈值时,会自动触发熔断,让当前依赖快速失败(Fail-fast),并降级到其他备用依赖,或者暂存到其他地方便于后续重试恢复。在熔断过程中,再通过不停探测被依赖服务或者资源是否恢复,来判断是否自动关闭熔断,恢复业务。

自动熔断这一机制目前业界已经有很多比较成熟的框架可以直接使用,比如,Netflix公司出品的Hystrix,以及目前社区很火热的Resilience4j等。

12.HTTP Tunnel:复杂网络下消息通道高可用设计的思考

HTTP Tunnel

所谓HTTP Tunnel,其实就是通过HTTP协议,来封装其他由于网络原因不兼容的协议(比如TCP私有协议)。

这样不仅解决了网络协议连通性问题,而且因为HTTP Tunnel也只是在原来的私有协议内容最外层做了最轻量的HTTP封装(HTTP Body内容就是二进制的私有协议),所以协议解析时也基本没有额外的代价。

多接入点IP列表

预埋一个域名和几个常用的接入点IP,用这个作为请求接入最后的兜底策略。当然,这些预埋的域名和接入点IP一般需要尽量保证稳定性,如果有变动,需要及时预埋到新版App中。

解决跨网延迟

因此,要让用户连得快,首先要求我们需要有多运营商机房的接入点;其次,要避免运营商DNS解析转发和NAT导致接入IP被解析到其他运营商的问题。

第一个多运营商机房的要求比较好实现,基本只是成本方面的投入,目前很多IDC机房都支持多线运营商接入。

第二个问题,我们可以通过之前讲到的HttpDNS来解决。HttpDNS能直接获取到用户的出口网关IP,调度更精准,而且绕过了运营商的LocalDNS,不会出现DNS解析转发导致错误调度的问题。

通道和业务解耦

但是在即时消息系统中,消息的收发是严重依赖长连接通道的,如果我们的通道层需要跟随业务的变化而不断调整,那么就会导致通道服务也需要频繁地上线、重启。这些操作会让已经连到通道机器的用户连接断开,虽然客户端一般都会有断线重连的机制,但是频繁地断连也会降低消息收发的成功率和用户体验。

因此,要提高消息通道的稳定性,我们要从架构上对通道层进行业务解耦,通道层只负责网络连接管理和通用的逻辑处理。

比如,用户和连接的映射维护、通信协议的编解码、建连和断连逻辑处理、ACK包和心跳包处理等,将变化较大的业务逻辑下沉到后端的业务处理层。这样不管业务怎么变动,我们的通道网关服务都不需要跟着变更,稳定性也会更好。

上下行通道隔离

用户A和用户B分别都通过接入查询服务来获取最优接入点,用户A通过上行通道的短连接网关来发送消息,发送的消息在上行业务处理服务进行存储、加未读等业务操作;然后通过消息队列把这条消息给到下行通道,下行分发逻辑服务查询用户B的在线状态等信息,并对消息进行必要的推送准备处理(比如版本兼容处理);接着把消息给到用户B的长连接所在的长连网关机器,长连网关机器再将消息推送到用户B的设备中。

13.分片上传:如何让你的图片、音视频消息发送得更快?

多上传接入点

对于拥有多机房的公司,也可以只把上传存储服务部署在单线机房,然后再通过专线解决多个单线机房之间的访问。比如,目前微博消息箱图片的上传,就是采用这种网络访问架构。大概IDC网络架构如下图:

 

上传链路优化

避免数据的来回拷贝,我们一般会把这些多媒体消息上传通道和普通消息收发通道独立开。发送多媒体消息时,先通过独立通道上传文件流,上传完成后会返回文件的唯一标识ID,然后再把这个唯一标识ID作为消息的引用,通过普通消息收发通道进行发送

 

语音的“分片先行下推”

因此,如果有两人正在通过语音实时聊天,我们更希望通过长连下推的方式将语音流推到对端,这样用户在播放语音时就不需要再从远程临时下载文件,使用流畅度也会更好。

另外,IM服务端在接收到分片后,可以同步先行把分片的二进制流下推给接收方但暂不显示,不需要等所有分片都在后端存储完成再进行下推。这样的好处是:当语音的最后一片到达后端并存储完成后,IM服务端只需要给接收方推一条“所有分片存储完成”的轻量信令,即可让接收方马上看到这条语音消息。这个“分片先行下推”机制在实时性上比远程临时下载的方式更好,能有效降低语音聊天的延时。

分片上传

  • 分片太大,片数少,上传的并发度不够,可能会降低上传效率,每个大的分片在失败后重传的成本会比较高。
  • 分片太小,片数多,并发需要的TCP连接太多,多条TCP连接的“窗口慢启动”会降低整体吞吐,两端拆分与合并分片的开销也相应增加,而且传输时的额外流量(HTTP报头)也会更多。

对于分片大小的设置,简单一点的处理可以按照网络状态来粗略划分。比如,WiFi下2M,4G下1M,3G/2G下256K。

断点续传

只保留一段时间

秒传机制

已经存在视频或者图片,直接返回存储ID

14.CDN加速:如何让你的图片、视频、语音消息浏览播放不卡?

CDN预热

大部分CDN都支持这个功能,通过CDN服务提供的API接口,把需要预热的资源地址和需要预热的区域等信息提交上去,CDN收到后,就会触发这些区域的边缘节点进行回源来实现预热。此外,利用CDN预热功能,还可以在业务高峰期预热热门的视频和图片等资源,提高资源的访问效率。

使用CDN如何保障消息私密性?

而对于用户量较大的超级大群、直播间、聊天室等场景来说,如果通过CDN确实能提升用户浏览图片和播放视频的流畅度,我们可以选择通过“流加密”的方式来提供私密性的保障。

比如,在视频消息中,如果针对视频文件使用HLS协议来进行分片,那么就可以采用HLS协议自带的加解密功能,来实现视频的流加密。

 

边下边播和拖动播放

支持边下边播需要有两个前提条件。

  • 格式信息和关键帧信息在文件流的头部。如果这些信息在文件尾部,就没法做到边下边播了。对于格式信息和关键帧信息不在头部的视频,可以在转码完成时改成写入到头部位置。
  • 服务端支持Range分片获取。有两种支持方式。- a.一种是文件的存储服务本身支持按Range获取,比如阿里的对象存储服务OSS和腾讯的对象存储服务COS,都支持分片获取,能够利用存储本身的分片获取机制真正做到“按需下载”。- b.对于不支持分片获取的存储服务来说,还可以利用负载均衡层对Range的支持来进行优化。比如,Nginx的HTTP Slice模块就支持在接收到Range请求后从后端获取整个文件,然后暂存到Nginx本地的Cache中,这样取下一片时能够直接从Nginx的Cache中获取,不需要再次向后端请求。这种方式虽然仍存在首次获取速度慢和Cache命中率的问题,但也可以作为分片下载的一种优化策略。

图片压缩和视频转码

分辨率自适应

用户点击再加载

WebP和渐进式JPEG

使用WebP和渐进式JPEG来对图片进行压缩,以降低体积,提升加载性能

H.265转码

针对热门的小视频采用H.265转码,在保证画质的同时,降低带宽成本并加快视频加载;

15.APNs:聊一聊第三方系统级消息通道的事

在iOS端,是由苹果提供的APNs服务来提供系统推送的能力。IM服务器把待推送的消息连同唯一标识某台设备的DeviceToken,一起给到APNs服务器,再由APNs服务器通过系统级的与任何App无关的长连接,来推送给用户设备并展示。新版本的APNs服务支持文本、音频、图片等多媒体消息的推送,以及无任何弹窗通知的静默推送。

但是APNs并不保证消息推送不发生延迟,也不保证消息能真正到达设备,对消息大小也有限制。因此,在可靠性上,APNs比App自建的长连接会差一些,所以一般也只是作为自建长连接不可用时的备选通道。

 

目前工信部主导的“统一推送联盟”,它的目的就在于通过提供统一的接入方式,以此解决这种混乱状况。其推出的产品“推必达”支持在移动网络不可用的情况下,通过电信信令通道来触达用户,能进一步提升消息的到达率,是我们值得期待的解决方案。

系统推送作为一种常用的触达用户的方式,对于即时消息场景来说是提升消息到达率的一条非常重要的途径。但这些系统推送通道目前还存在可靠性低、功能不完善、生态混乱等问题,因此,对消息可靠性要求较高的场景来说,系统推送通道基本上只能作为对自建长连接推送通道的一个补充。

16.Cache:多级缓存架构在消息系统中的应用

首先,我介绍了缓存在高并发应用中的重要性,以及在IM系统中使用的部分场景。然后再带你了解了缓存分布式的两种算法:取模求余和一致性哈希。

  • 取模求余算法在实现上非常简单,但存在的问题是,取模求余算法在节点扩容和宕机后会出现震荡,缓存命中率会严重降低。
  • 一致性哈希算法解决了节点增删时震荡的问题,并通过虚拟节点的引入,缓解了“数据倾斜”的情况。

最后,我着重介绍了业界通用的三种分布式缓存的常见架构。

  • 一种是主从模式。简单的主从模式最常见,但是在面对峰值热点流量时,容易出现带宽问题,也存在缓存节点宕机后穿透到存储层的问题。
  • 第二种是L1+主从模式。通过增加L1缓存层,以并行的多组小容量的L1缓存,解决了单一热点的带宽问题,也避免了单一节点宕机后容易穿透到DB存储层的情况。
  • 最后一种是本地缓存+L1+主从的多层模式。作为低成本的解决方案,我们在L1+主从模式的基础上,引入了本地缓存。本地缓存依托应用服务器的本机少量内存,既提升了资源的有效利用,也彻底解决了带宽的问题。同时在性能方面,也比远程缓存获取更加优秀。对于本地缓存的数据一致性问题,我们可以通过“短过期时间”来平衡缓存命中率和数据一致性。

17.Docker容器化:说一说IM系统中模块水平扩展的实现

由于“垂直扩展”的可扩展性依赖于单机自身的硬件能力,并不能彻底解决资源和服务器“无限扩容”的问题,因此需要链路各层能够做到“水平扩展”。

各层的水平扩展的实现,有以下几种参考方案。

  1. 针对接入层的水平扩展,我们需要解决好两个瓶颈问题:- 一个是接入层的入口VIP瓶颈问题,我们可以针对单域名支持多VIP映射,并通过DNS轮询来进行负载均衡;- 而针对业务自身的接入层服务,我们可以通过中央的“在线状态”资源,来解耦业务层的依赖,从而实现水平扩展。

  2. 针对业务层的水平扩展,我们可以进行“服务化”改造,依托“服务注册中心”和“服务自动发现”解决调用方寻址问题,实现业务层的水平扩展。

  3. 针对资源层的水平扩展,我们可以通过数据分片机制缓解主库和从库压力,还可以通过多从库提升读取能力,实现资源的水平扩展。

在链路各层的水平扩展的具体实施上,我们可以借助Docker等容器化技术,来屏蔽部署机器的差异。通过应用镜像的自定义部署环境,来提升链路各层水平扩展时的部署效率。

18.端到端Trace:消息收发链路的监控体系搭建

被动监控主要依赖服务器或者应用服务的监控数据上报,通过第三方监控系统,来对监控数据进行展示。

被动监控又可以细分为系统层监控和应用层监控,这两种监控通过实时收集机器层面和应用服务层面的性能数据,协助我们实时掌握机器和应用服务的可用性。

另外,还有一种全链路Trace监控,也属于被动监控,实际上也属于应用层监控的范畴。

它是基于Google的Dapper论文衍生出的众多分布式链路追踪系统,进一步通过链路Trace,将消息收发行为进行整体的端到端的串联,极大地提升了问题排查的效率,而且为链路优化分析和用户访问数据分析,提供了强有力的监控数据支撑。

为了弥补被动监控依赖机器和应用服务的监控数据上报的问题,我们还可以通过第三方的主动探测程序,来实现主动监控。在消息收发场景中,通过模拟用户收发消息行为的回环探测方式,来监控通道的可用性。

我们在即时消息场景中,就可以通过以上这两种监控方式的协同,来更好地监控消息收发服务的可用性。

19.存储和并发:万人群聊系统设计中的几个难点

群聊消息怎么存储?

业界针对群聊消息的存储,一般采取“读扩散”的方式。也就是一条消息只针对群维度存储一次,群里用户需要查询消息时,都通过这个群维度的消息索引来获取。

 

怎么保证新加入群的用户只看到新消息?

单个用户删除消息怎么办?

一个可行的办法是:在用户删除消息的时候,把这条被删除消息加入到当前用户和群维度的一个删除索引中;当用户查询消息时,我们对群维度的所有消息,以及对这个“用户和群维度”的删除索引进行聚合剔除就可以了。

未读数合并变更

当群里有人发言时,我们需要对这个群里的每一个人都进行“加未读”操作。因此,对于服务端和未读数存储资源来说,整体并发的压力会随着群人数和发消息频率的增长而成倍上升。

解决这个问题的一个可行方案是:在应用层对未读数采取合并变更的方式,来降低对存储资源的压力。

 

离线Buffer只存消息ID

这里的离线Buffer是用户维度的,因此对于群聊中的每一条消息,服务端都会在扇出后进行暂存。

假设是一个5000人的群,一条消息可能会暂存5000次,这样一方面对离线Buffer的压力会比较大,另外针对同一条消息的多次重复暂存,对资源的浪费也是非常大的。

要解决多次暂存导致离线Buffer并发压力大的问题,一种方案是可以参考“未读数合并变更”的方式,对群聊离线消息的存储也采用“合并暂存”进行优化,所以这里我就不再细讲了。

另一种解决方案是:我们可以对群聊离线消息的暂存进行限速,必要时可以丢弃一些离线消息的暂存,来保护后端资源。

因为通过“版本号的链表机制”,我们可以在用户上线时发现“离线消息”不完整的问题,然后再从后端消息存储中重新分页获取离线消息,从而可以将一部分写入压力延迟转移到读取压力上来。

离线消息批量ACK

以微博场景中的超大规模的粉丝群为例:本来群内的用户就已经比较活跃了,如果该群隶属的明星突然空降进来,可能会导致大量离线用户被激活,同一时间会触发多个用户的离线消息下推和这些离线消息的ACK;针对离线消息接收端的ACK回包,服务端需要进行高并发的处理,因而对服务端压力会比较大。

针对离线消息接收端进行批量ACK。

参照TCP的Delay ACK(延迟确认)机制,我们可以在接收到离线推送的消息后,“等待”一定的时间,如果有其他ACK包需要返回,那么可以对这两个回包的ACK进行合并,从而降低服务端的处理压力。

不记录全局的在线状态

每一台网关机在启动后都会订阅这个全局的Topic,因此都能获取到这条消息;接着,各网关机查询各自本地维护的“在线用户”的信息,把归属本机的用户的消息,通过长连下推下去。

 

posted @   王鹏鑫  阅读(80)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示