You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

132 lines
10 KiB
Markdown

2 years ago
# 04 | ACK机制如何保证消息的可靠投递
你好,我是袁武林。
在第一节的课程中,我们说到了即时消息系统中的四个重要特性,实时性、可靠性、一致性、安全性。
上一节课我们从如何保证消息实时性方面,了解了业界常用的一些方式以及背后具体的原理。那么今天我们接着来讲一讲,在即时消息的系统架构设计里,如何来保证消息的可靠投递。
首先,我们来了解一下,什么是消息的可靠投递?
站在使用者的角度来看,消息的可靠投递主要是指:消息在发送接收过程中,能够做到不丢消息、消息不重复两点。
这两个特性对于用户来讲都是非常影响体验的。我们先说一下不丢消息。
试想一下,你把辛辛苦苦攒到的零花钱打赏给了中意的“主播小姐姐”,但由于系统或者网络的问题,这条对你来说至关重要的打赏消息并没有成功投递给“主播小姐姐”,自然也就没有后续小姐姐和你一对一的互动环节了,想想是不是很悲剧?
消息重复也不用多说,谁也不愿意浪费时间在查看一遍又一遍的重复内容上。
那么在一般的IM系统的设计中究竟是如何解决这两大难题的呢下面我们结合一些简单的案例来看一看“不丢消息”“消息不重复”这些能力在技术上到底是怎么实现的。
## 消息丢失有哪几种情况?
我们以最常见的“服务端路由中转”类型的IM系统为例非P2P这里解释一下所谓的“服务端路由中转”是指一条消息从用户A发出后需要先经过IM服务器来进行中转然后再由IM服务器推送给用户B这个也是目前最常见的IM系统的消息分发类型。
我们可以把它和少数P2P类型区别一下P2P类型的消息投递是直接由用户A的网络发送到用户B的网络不经过服务端路由。
那么我们来假设一个场景用户A给用户B发送一条消息。接下来我们看看哪些环节可能存在丢消息的风险
![](https://static001.geekbang.org/resource/image/5b/28/5b2ee576e22ac109121714aaaaec3528.png)
参考上面时序图,发消息大概整体上分为两部分:
* 用户A发送消息到IM服务器服务器将消息暂存然后返回成功的结果给发送方A步骤1、2、3
* 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机制
我们先解释一下ACKACK全称 Acknowledge是确认的意思。在TCP协议中默认提供了ACK机制通过一个协议自带的标准的ACK数据包来对通信方接收的数据进行确认告知通信发送方已经确认成功接收了数据。
那么业务层ACK机制也是类似解决的是IM服务推送后如何确认消息是否成功送达接收方。具体实现如下图
![](https://static001.geekbang.org/resource/image/a4/7e/a4e3c1cfb27aa32e1c42891f3c14eb7e.png)
IM服务器在推送消息时携带一个标识SID安全标识符类似TCP的sequenceId推送出消息后会将当前消息添加到“待ACK消息列表”客户端B成功接收完消息后会给IM服务器回一个业务层的ACK包包中携带有本条接收消息的SIDIM服务器接收后会从“待ACK消息列表”记录中删除此条消息本次推送才算真正结束。
### ACK机制中的消息重传
如果消息推给用户B的过程中丢失了怎么办比如
* B网络实际已经不可达但IM服务器还没有感知到
* 用户B的设备还没从内核缓冲区取完数据就崩溃了
* 消息在中间网络途中被某些中间设备丢掉了TCP层还一直重传不成功等。
以上的问题都会导致用户B接收不到消息。
解决这个问题的常用策略其实也是参考了TCP协议的重传机制。类似的IM服务器的“等待ACK队列”一般都会维护一个超时计时器一定时间内如果没有收到用户B回的ACK包会从“等待ACK队列”中重新取出那条消息进行重推。
### 消息重复推送的问题
刚才提到对于推送的消息如果在一定时间内没有收到ACK包就会触发服务端的重传。收不到ACK的情况有两种除了推送的消息真正丢失导致用户B不回ACK外还可能是用户B回的ACK包本身丢了。
对于第二种情况ACK包丢失导致的服务端重传可能会让接收方收到重复推送的消息。
针对这种情况一般的解决方案是服务端推送消息时携带一个Sequence IDSequence ID在本次连接会话中需要唯一针对同一条重推的消息Sequence ID不变接收方根据这个唯一的Sequence ID来进行业务层的去重这样经过去重后对于用户B来说看到的还是接收到一条消息不影响使用体验。
## 这样真的就不会丢消息了吗?
细心的你可能发现通过“ACK+超时重传+去重”的组合机制,能解决大部分用户在线时消息推送丢失的问题,那是不是就能完全覆盖所有丢消息的场景呢?
设想一下假设一台IM服务器在推送出消息后由于硬件原因宕机了这种情况下如果这条消息真的丢了由于负责的IM服务器宕机了无法触发重传导致接收方B收不到这条消息。
这就存在一个问题当用户B再次重连上线后可能并不知道之前有一条消息丢失的情况。对于这种重传失效的情况该如何处理
### 补救措施:消息完整性检查
针对服务器宕机可能导致的重传失效的问题我们来分析一下,这里的问题在于:服务器机器宕机,重传这条路走不通了。
那如果在用户B在重新上线时让服务端有能力进行完整性检查发现用户B“有消息丢失”的情况就可以重新同步或者修复丢失的数据。
比较常见的消息完整性检查的实现机制有“时间戳比对”,具体的实现如下图:
![](https://static001.geekbang.org/resource/image/14/c6/149af9b46ff04769d8957efaac84e1c6.png)
下面我们来看一下“时间戳机制”是如何对消息进行完整性检查的,我用这个例子来解释一下这个过程。
* IM服务器给接收方B推送msg1顺便带上一个最新的时间戳timestamp1接收方B收到msg1后更新本地最新消息的时间戳为timestamp1。
* IM服务器推送第二条消息msg2带上一个当前最新的时间戳timestamp2msg2在推送过程中由于某种原因接收方B和IM服务器连接断开导致msg2没有成功送达到接收方B。
* 用户B重新连上线携带本地最新的时间戳timestamp1IM服务器将用户B暂存的消息中时间戳大于timestamp1的所有消息返回给用户B其中就包括之前没有成功的msg2。
* 用户B收到msg2后更新本地最新消息的时间戳为timestamp2。
通过上面的时间戳机制用户B可以成功地让丢失的msg2进行补偿发送。
需要说明的是,由于时间戳可能存在多机器时钟不同步的问题,所以可能存在一定的偏差,导致数据获取上不够精确。所以在实际的实现上,也可以使用全局的自增序列作为版本号来代替。
## 小结
保证消息的可靠投递是IM系统设计中至关重要的一个环节“不丢消息”“消息不重复”对用户体验的影响较大我们可以通过以下手段来确保消息下推的可靠性。
* 大部分场景和实际实现中通过业务层的ACK确认和重传机制能解决大部分推送过程中消息丢失的情况。
* 通过客户端的去重机制,屏蔽掉重传过程中可能导致消息重复的问题,从而不影响用户体验。
* 针对重传消息不可达的特殊场景,我们还可以通过“兜底”的完整性检查机制来及时发现消息丢失的情况并进行补推修复,消息完整性检查可以通过时间戳比对,或者全局自增序列等方式来实现。
最后,给你留一个思考题:**有了TCP协议本身的ACK机制为什么还需要业务层的ACK机制**
你可以给我留言,我们一起讨论,感谢你的收听,我们下期再见。