likes
comments
collection
share

【计算机网络实战】简易IM(五)关于消息发送、存储与同步问题的深入探讨

作者站长头像
站长
· 阅读数 14

前言

在上一篇文章中,我对KIM这个开源项目聊天部分的业务代码进行了简要的分析。实际上,对于通信系统而言,最重要的不是界面是否好看,也不是系统的代码写得多么复杂或者用了多么复杂的组件,也不是 能 完成功能,而是要保证功能绝对可靠。

比如,绝对不能出现发送方这边发送了一条信息,并且系统显示发送成功了,但是接收方根本没收到这样的情况(具体场景不同,引发的原因不同,通常是由消息实际发送失败但又无提醒导致,极端情况下可能是因为数据同步异常);再比如,若用户很长一段时间没在线,在此期间,所有消息都应该被服务器妥善存储好,而不能某处丢了几条(断章取义)、或者顺序错乱,让人看得不明所以。

然而上述提到的问题,并不是像上一篇文章那样简单列几个类定义、展示一下handler思路流程可以解答的,因此需要结合业界已有的共识解决方案,更加深入地分析原作者的底层代码思路。

发送

我们可以从如下角度去思考这个问题:

传输链路

为什么传输链路是很重要的一环呢?

实际上,distributed systemcomputer 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
评论
请登录