IM——四个重要特性

实时性

实时性主要解决的问题是:当一条消息发出去后,我们的系统如何确保这条消息最快被接收人感知并获取到,并且尽量让耗费的资源较少。这里关键的几个点是:最快触达,且耗费资源少。

下面我们来看一看,IM在追求 消息实时性的架构上,所经历过的几个代表性阶段。

短轮询场景

作为一问一答的请求响应模式孵化出来的短轮询模式,具有较低的迁移成本,比较容易落地。但劣势也很明显:

1.为了提高实时性,短轮询的频率一般比较高,但大部分轮询请求实际上是无用的,客户端既费电也费流量;

2.高频请求对服务端资源的压力也比较大,一是大量服务器用于扛高频轮询的QPS,二是对后端存储资源也有较大压力。

因此,短轮询这种方式,一般多用在用户规模较小,且不愿意花费太多服务改造成本的小型应用上。

长轮询场景

长轮询与短轮询相比,一个最大的改进之处在于:在短轮询模式下,服务端不管本轮有没有新消息产生,都会马上响应并返回。而长轮询模式当本次请求没有获取到新消息时,并不会马上结束返回,而是会在服务端悬挂(hang),等待一段时间;如果在这段时间内有新消息产生,就能马上响应并返回。

比较之下,我们发现,长轮询能大幅降低短轮询模式中客户端高频无用的轮询导致的网络开销和功耗开销,也降低了服务端处理请求的QPS,相比短轮询模式而言,显得更加先进

长轮询的使用场景多见于:对实时性要求比较高,但是整体用户量不太大。它在不支持websocket的浏览器端的场景下还有比较多的使用。

但是仍存在以下问题:

1.服务端悬挂请求,只是降低了入口请求的QPS,并没有减少对后端资源轮询的压力。假如有1000个请求在等待消息,可能意味着有1000个线程在不断轮询消息存储资源。

2.长轮询在超时时间内没有获取到消息时,会结束返回,因此仍然没有完全解决客户端无效请求。

服务端推送:真正的边缘触发

随着HTML5的出现,全双工的websocket彻底解决了服务端推送的问题

和短轮询,长轮询相比,基于websocket实现的IM服务,客户端和服务端只需要完成一次握手,就可以创建持久的长连接,并进行随时的双向数据传输。当服务端接收到新消息时,可以通过建立的websocket连接,直接进行推送,真正做到边缘触发,也保证了消息到达的实时性。

websocket的优点是:

1.支持服务端推送的双向通信,大幅降低服务端轮询压力

2.数据交互的控制开销低,降低双方通信的网络开销。

3.web原生支持,实现相对简单。

tcp长连接衍生的IM协议

XMPP,MQTT,或者基于TCP,UDP来实现自己的私有协议。

可靠性

消息丢失有哪几种情况?

参考上面时序图,发消息大概整体上分为两部分:

1.用户A发送消息到IM服务器,服务器将消息暂存,然后返回成功的结果给发送方A(步骤1,2,3)

2.IM服务器接着再将短暂的用户A发出的消息,推送给接收方用户B(步骤4)

其中可能丢失消息的场景有下面这些:

在第一部分中,步骤1,2,3都可能存在失败的情况。

由于用户A发消息时一个请求和响应的过程,如果用户A在把消息发送到IM服务器的过程中,由于网络不通等原因失败了;或者IM服务器接收到消息进行服务端存储时失败了;或者用户A等待IM服务器一定的超时时间,但IM服务器一直没有返回结果,那么这些情况用户A都会被提示发送失败。

接下来,他可以通过重试等方式来弥补,注意这里可能会导致发送重复消息的问题。

比如:客户端在超时时间内没有收到响应然后重试,但实际上,请求可能已经在服务端成功处理了,只是响应慢了,因此这种情况需要服务端有去重逻辑,一般发送端针对同一条重试消息有一个唯一的ID,便于服务端去重。

第二部分中。消息在IM服务器存储完后,响应用户A告知消息发送成功了,然后IM服务器把消息推送给用户B的在线设备。

在推送的准备阶段或者把消息写入到内核缓冲区后,如果服务端出现掉电,也会导致消息不能成功推送给用户B。这种情况实际上由于连接的IM服务器可能已经无法正常运转,需要通过后期的补救措施来解决丢消息的问题,后续详细介绍。

即使我们的消息成功通过TCP连接给到用户B的设备,但如果用户B的设备在接收后的处理过程出现问题,也会导致消息丢失。比如:用户B的设备在把消息写入本地DB时,出现异常导致没能成功入库,这种情况下,由于网络层面实际上已经成功投递了,但用户B却看不到消息。所以比较难处理。

解决方案:

1.针对第一部分,我们通过客户端A的超时重传和IM服务器的去重机制,基本就可以解决问题。

2.针对第二部分,业界一般参考TCP协议的ACK机制,实现一套业务层的ACK协议。

解决丢失的方案:业务层的ACK机制

具体实现如下图:

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

ACK机制中的消息重传

如果消息推给用户B的过程中丢失了怎么办?比如:

1.B网络实际已经不可达,但IM服务器还没有感知到

2.用户B的设备还没从内核缓冲区取完数据就崩溃了

3.消息在中间网络途中被某些中间设备丢掉了,TCP层还一直重传不成功等。

解决这个问题的常用策略其实也是参考了TCP协议的重传机制,类似的,IM服务器的等待ACK队列,一般都会维护一个超时计时器,一定时间内如果没有收到用户B回的ACK包,会从ACK队列中重新取回那条消息进行重推。

消息重复推送的问题

ACK包丢失导致的服务端重传,可能会让接收方收到重复推送的消息。

一般的解决方案是:服务端推送消息时携带一个sequence ID,Sequence ID在本次连接会话中需要唯一,针对同一条重推的消息Sequence Id不变。接收方根据这个唯一的sequence ID来进行业务层的去重,这样经过去重后,对于用户B来说,看到的还是接收到一条消息,不影响使用体验。

补救措施:消息完整性检查

假设一台IM服务器在推送出消息后,由于硬件原因宕机了,这种情况下,如果这条消息真的丢了,由于负责的IM服务器宕机了无法触发重传,导致接收方B收不到这条消息。

问题在于:服务器机器宕机,重传这条路走不通了

那如果在用户B在重新上线时,让服务端有能力进行完整性检查,发现用户B有消息丢失的情况,就可以重新同步或者修复丢失的数据

比较常见的消息完整性检查的实现机制有时间戳比对

1、IM服务器给接收方B推送msg1,顺便带上一个最新的时间戳timestamp1,接收方B收到msg1后,更新本地最新消息的时间戳为timestamp1

2.IM服务器推送第二条消息msg2,带上一个当前最新的时间戳timestamp2,msg2在推送过程中由于某种原因接收B和IM服务器连接断开,导致msg2没有成功送达到接收方B。

3、用户B重新连上线,携带本地最新的时间戳timestamp1,IM服务器将用户B暂存的消息中时间戳大于timestamp1的所有消息返回给用户B,其中就包括之前没有成功的msg2.

4.用户b收到msg2后,更新本地最新消息的时间戳为timestamp2

需要说明的是,由于时间戳可能存在多机器时钟不同步的问题,所以可能存在一定的偏差,导致数据获取不够精确。所以在实际的实现上,也可以使用全局的自增序列作为版本号来代替。

发布了43 篇原创文章 · 获赞 37 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_28119741/article/details/103847683