【计算机网络实战】简易IM(五)关于消息发送、存储与同步问题的深入探讨
前言
在上一篇文章中,我对KIM这个开源项目聊天部分的业务代码进行了简要的分析。实际上,对于通信系统而言,最重要的不是界面是否好看,也不是系统的代码写得多么复杂或者用了多么复杂的组件,也不是 能 完成功能,而是要保证功能绝对可靠。
比如,绝对不能出现发送方这边发送了一条信息,并且系统显示发送成功了,但是接收方根本没收到这样的情况(具体场景不同,引发的原因不同,通常是由消息实际发送失败但又无提醒导致,极端情况下可能是因为数据同步异常);再比如,若用户很长一段时间没在线,在此期间,所有消息都应该被服务器妥善存储好,而不能某处丢了几条(断章取义)、或者顺序错乱,让人看得不明所以。
然而上述提到的问题,并不是像上一篇文章那样简单列几个类定义、展示一下handler思路流程可以解答的,因此需要结合业界已有的共识解决方案,更加深入地分析原作者的底层代码思路。
发送
我们可以从如下角度去思考这个问题:
传输链路
为什么传输链路是很重要的一环呢?
实际上,distributed system
和 computer network
是很相近的,本质上都是要高度依赖于网络。网络情况良好的时候,可以很自豪地宣称:每一台计算机都是一个节点,我们可以有各种管理策略:中心化、去中心化、选举制度巴拉巴拉;但是一旦网络部分异常,这种主机资源虚拟化、高度共享化的性质决定了问题究竟出现在哪里都很难定位;一旦网络瘫痪了,所有的主机都死翘翘。
因此,从我目前的观察来看,为什么分布式会有这样那样的概念呢?因为它们本质上是一种对网络故障的“快速止血”式解决方案(比如服务熔断,直白理解就是断开外界与这部分的服务连接;服务限流,就是减少或者转移掉发送请求,短期内减轻服务器的压力)。并不是因为相关概念看上去高深莫测所以要学习,而是因为如果完全不了解,在出现故障的时候连急救都不会。
说到这里,想必读者也明白了:虽然在宏观世界看来,发送方发送消息,接收方收到消息是一个瞬时的过程,但实际上,这个中间过程也是很复杂的,一旦信号被干扰、或者传输链路出现故障等等,有一定概率会导致信息发送失败;更有甚者,不同于单体架构下的由单台服务器完成所有任务,在分布式系统的背景下,是由不同的服务器来承担不同的任务,这过程中出现的变数就更多了,本质上是增加了消息传递的不确定性。
tcp协议本身就有相应的保证可靠传输的机制,我在之前写过的一篇文章中有简要提到过这个问题: tcp协议十八连问
不过,正如我之前所提到的,在计算机网络中,往往是上层依赖下层的服务,某些特性并不是某一层特有;同时,越往底层看,就发现相应功能越简陋、越对实际效果不作任何保证(当然也可以反过来理解为是上层封装得越来越好)。
因此,我们在使用现成的东西进行开发的时候也要秉承一个基本思想:了解所使用的东西的原理,但对于重要并关键的问题,不要把全部希望寄托在它身上,要意识到它也是有可能出错的,必须再加一层屏障。
SDK
在大型分布式系统中,往往会用到消息队列(MQ),其根据消息投递的次数分为如下情况:
- at-most once
- at-least once
- exactly once
不过,KIM的作者并没有在项目中使用消息队列,而是通过SDK实现了类似于exactly-once
的功能。
这是如何实现的呢?答案是利用计算机学科的幂等思想。
具体思路是利用数据库的主键唯一特性。SDK端把收到的数据写入数据库,如果有ID重复的数据,会导致在数据库中插入失败,后续也不会再被上报至业务层。
这里有两个问题值得思考:
-
用SDK来实现和直接用MQ有什么不同呢?
- 从具体实现方式看,MQ如kafka的幂等性是通过 两阶段提交协议 实现的;SDK则利用了数据库的主键唯一特性。
- 从性质看,在MQ中,消息的生产和消费是异步的;而IM的即时性要求消息应尽可能保证同步。
-
at-most once 和at-least once 有什么特点,这里为什么不考虑?
- at-most once 无法保证消息一定会被接收方收到,不符合可靠性的标准;
- 其实并没有不考虑at-least once,这种情况下业务层可能会收到重复信息,这种情况事实上也是存在和被允许的
存储
这里的存储指的是消息的暂存。 为什么要采用先存储后判断的方式呢?在上一篇文章中也简要地提过是想要最大程度保证消息不丢失。
考虑这样一种极端情况: 在a给b发送一连串消息的时候,b突然掉线了,之后又马上上线,如果不先暂存消息,由于状态刷新不及时,某条消息被认为是“在线发送”,而实际上b是离线的,在b重新上线后也不会再收到这条消息了。因此需要先存储,后判断,就算错了,起码也能有个备份。
同步
请求合并
这里和计算机网络提到的“捎带”技术有点类似:处理离线消息时,本轮待确认消息id跟随下一轮请求一起发送。
巧用索引
- 基于id的有序性,确认相对最大的id,则认为在它之前的消息都已经被确认
那么问题来了:
- 在分布式id中,能保证id的绝对有序性吗?
其实是不能的。有些问题就是必须在性能和可靠性之间做选择,没法既要又要,只是看哪个出现的概率更小以及造成的代价更小。
- 针对SDK端和服务端的索引不同步的情况怎么办呢?
以本地SDK端的为准,可以减少因网络问题带来的重复同步的消息条数。
- 索引与内容分离
顾名思义,就是将索引看成指针,把消息内容看做是它指向的内容。先快速加载出索引列表,之后再根据实际情况动态加载索引指向的内容。避免了因消息内容过大而导致进程在加载索引时就已经堵塞的情况。
转载自:https://juejin.cn/post/7250442153431711800