即时消息:如何保证消息的可靠投递

什么是消息的可靠投递

站在使用者的角度来看,消息的可靠投递主要是指:消息在发送接收过程中,能够做到不丢消息、消息不重复

消息丢失有哪几种情况

我们以最常见的“服务端路由中转”类型的 IM 系统为例

这里解释一下,所谓的“服务端路由中转”是指:一条消息从用户 A 发出后,需要先经过 IM 服务器来进行中转,然后再由 IM 服务器推送给用户 B,这个也是目前最常见的 IM 系统的消息分发类型。

那么,我们来假设一个场景:用户 A 给用户 B 发送一条消息。接下来我们看看哪些环节可能存在丢消息的风险?

在这里插入图片描述
参考上面时序图,发消息整体上大概可以分为两部分:
(1)用户A发送消息到IM服务器,服务器将消息暂存,然后返回成功的结果给发送方A(步骤1、2、3)

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

可能丢失消息的场景:

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

  • 用户 A 在把消息发送到 IM服务器的过程中,由于网络不通等原因失败了
  • IM服务器在接收到消息进行服务器存储时失败了
  • 用户 A 等待 IM 服务器一定的超时时间,但 IM 服务器一直没有返回结果

接下来,可以通过重试的方法来弥补,但是这可能会导致发送消息重复的问题。比如:

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

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

  • 在推送的准备阶段或者把消息写入到内核缓冲区后,如果服务端出现掉电,也会导致消息不能成功推送给用户B。这种情况实际上由于连接的IM服务器可能已经无法正常运转,需要通过后期的补救措施来解决丢消息的问题
  • 即使我们的消息成功通过TCP连接给用户B的设备,但如果用户B的设备在接收后的处理过程出现问题,也会导致消息丢失。比如,用户B的设备在把消息写入本地DB时,出现异常导致没能成功入库,这种情况下,由于网络层面实际上已经成功投递了,但用户B却看不到消息,所以比较难以处理

那怎么避免在这些异常情况下丢消息呢?一般有如下解决方案:

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

解决方案:业务层ACK机制

ACK 全称 Acknowledge,是确认的意思。

  • 在TCP协议中,默认提供了ACK机制,通过一个协议自带的标准的ACK数据包,来对通信方接收的数据进行确认,告知通信发送方已经确认成功接收了数据。
  • 业务层的ACK机制也是如此,解决的是: IM服务端推送后如何确认消息是否成功送到接收方

ACK机制的实现

业务层ACK机制实现如下图:
在这里插入图片描述

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

ACK机制中的消息重传

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

  • B网络实际已经不可达,但IM服务器还没有感知到
  • 用户B的设备还没有从内核缓冲区取完数据就崩溃了
  • 消息在中间网络途中被某些中间设备丢掉了,TCP层还一直重传不成功等

以上的问题都会导致用户B接收不到消息。

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

问题:服务端发消息给客户端,没收到ack重试有最大次数吗?是像tcp那样一直重试直到收到ack吗?

  • 会有重试次数,毕竟即使收不到还有离线消息来补充。
  • 重试多次仍然失败服务端可以主动断连来避免资源消耗。

问题:在线群消息客户端插入失败然后怎么处理呢

  • 比如群里有用户A、B、C,这三个用户分别连接到IM服务器
  • A往群里发送一条消息,B和C的连接的网关机分布下推这一条消息,并且将这条消息分布加入B和C的“待ACK列表”
  • 如果B接收后处理成功然后回了ACK,B的网关机收到ACK就会从“待ACK列表”删除这条消息
  • 如果C接收后客户端本地db写入失败,这时C的客户端就不会回ACK,C的网关机一段时间后没有接收到这条消息的ACK,就会触发超时重传,重新发送这条消息给C的客户端。

ACK不是和某一条消息绑定的,而是和某一个人要接收的消息绑定的,是某一个连接维度的

消息重复推送的问题

ACK消息重传机制,对于推送的消息,如果在一定时间内没有收到ACK包,就会触发服务器的重传。收不到ACK的情况有两种,除了推送的消息真正丢失导致用户B不回ACK外,还可能是用户B回的ACK包本身丢了

对于第二种情况,ACK包丢失导致的服务器重传,可能会让接收方收到重复推送的问题。

解决方法:

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

消息完整性检查

通过"ACK+超时重传+去重"的组合机制,能解决大部分用户在线消息推送丢失的问题,那是不是能完全覆盖所有丢消息的场景呢?

不能

  • 假设一台IM服务器在推送出消息后,由于硬件原因宕机了,这种情况下,如果这条消息真丢了,由于负责的IM服务器宕机了无法触发重传,导致接收方B收不到这条消息
  • 这样就存在一个问题,当用户B再次重连上线后,可能并不知道有一条消息丢失的情况。

对于这种重传失效的方法该怎么处理?

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

  • 解决方法:在用户B重新上线时,让服务端有能力进行完整性检测,发现用户B“有消息丢失”的情况,就可以重新同步或者修复丢失的数据

比较常见的消息完整性检测的实现机制有“时间戳比对”。如下图:

在这里插入图片描述

  • IM 服务器给接收方 B 推送 msg1,顺便带上一个最新的时间戳 timestamp1,接收方 B 收到 msg1 后,更新本地最新消息的时间戳为 timestamp1。
  • IM 服务器推送第二条消息 msg2,带上一个当前最新的时间戳 timestamp2,msg2 在推送过程中由于某种原因接收方 B 和 IM 服务器连接断开,导致 msg2 没有成功送达到接收方 B。
  • 用户 B 重新连上线,携带本地最新的时间戳 timestamp1,IM 服务器将用户 B 暂存的消息中时间戳大于 timestamp1 的所有消息返回给用户 B,其中就包括之前没有成功的 msg2。
  • 用户 B 收到 msg2 后,更新本地最新消息的时间戳为 timestamp2

通过上面的时间戳机制,用户 B 可以成功地让丢失的 msg2 进行补偿发送。

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

问题:如果客户端一直连接不上,IM服务器会怎么处理?
回答:一般至少会缓存到离线消息的buffer中

(服务端IM先后推送两条消息msg0、msg1、msg2到客户端B,如果msg0、msg2先到达,此时客户端B应该不会更新到msg2的发送时间戳吧,而是等待msg1到达。怎么优化?)

小结

保证消息的可靠投递是IM系统设计中非常重要的一个环节,“不丢消息”、“消息不重复”对用户体验的影响比较大,我们可以通过如下手段来确保消息下推的可靠性。

  • 大部分场景和实际实现中,通过业务层的 ACK 确认和重传机制,能解决大部分推送过程中消息丢失的情况。
  • 通过客户端的去重机制,屏蔽掉重传过程中可能导致消息重复的问题,从而不影响用户体验。
  • 针对重传消息不可达的特殊场景,我们还可以通过“兜底”的完整性检查机制来及时发现消息丢失的情况并进行补推修复,消息完整性检查可以通过时间戳比对,或者全局自增序列等方式来实现。

问题:有了 TCP 协议本身的 ACK 机制为什么还需要业务层的 ACK 机制?

回答:

  • 即使数据成功发送到接收方设备了,TCP层再把数据交给应用层时也可能会出现异常,比如存储客户端的本地DB失败,导致消息在业务层实际是没成功收到的。这种情况下,可以通过业务层的ACK来提供保证,客户端只有都执行成功才会返回ACK给客户端
  • TCP属于传输层,而IM服务属于应用层,TCP的ACK只能保证传输层的可靠性,即A端到B端的可靠性,但是不能保证数据能够被应用层正确可靠处理,比如应用层里面的业务逻辑导致消息处理失败了,TCP层是不知道的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RabbitMQ 是一个开源的消息队列,它采用了 AMQP 协议来实现消息可靠传输。AMQP 协议提供了消息确认机制,保证消息在发送到队列之后得到确认,以避免消息的丢失。 RabbitMQ 实现消息可靠投递主要有以下几个方面: 1. 消息确认机制:RabbitMQ 支持消息的确认机制,当消息被成功接收并处理后,消费者可以给 RabbitMQ 发送一个确认消息,告诉 RabbitMQ 这个消息已经被成功处理了。如果 RabbitMQ 没有收到消费者的确认消息,那么它会将消息重新发送给另一个消费者进行处理。 2. 持久化机制:RabbitMQ 支持将消息持久化到磁盘上,以防止消息在 RabbitMQ 重启时丢失。如果消息需要被持久化,生产者需要将消息的 delivery mode 设置为 2。 3. 生产者确认机制:RabbitMQ 支持生产者确认机制,当生产者发送消息给 RabbitMQ 后,可以等待 RabbitMQ 发送确认消息给生产者,告诉生产者消息已经被成功接收并保存。如果 RabbitMQ 无法接收消息,则会返回一个 Nack 消息给生产者,告诉它消息发送失败。 4. 重试机制:RabbitMQ 支持消息的重试机制,如果消息没有被成功处理,RabbitMQ 会将消息重新发送给另一个消费者进行处理,直到消息被成功处理为止。 综上所述,RabbitMQ 提供了多种机制来保证消息可靠投递,可以根据实际情况选择合适的机制来保证消息可靠性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值