第13章IM服务——13.3 消息投递

13.3 消息投递

IM服务最重要的议题是消息投递,合格的消息投递应该具有如下特点。

  • 准确性:比如用户1向用户2发送消息,消息只可能被用户2收到,不可能发生用户3收到这条消息的“串线”问题。
  • 消息不可丢失:消息不可被漏发,更不可丢失,即使遇到故障(如机房断电、网络断开等),用户也应该能够在故障排除后接收到错过的消息。所有IM的用户都坚信消息是一定会被送达的,如果用户发送了重要的消息,而我们的IM服务却因为各种原因把它丢失了,那么用户会直接流失。
  • 实时性:应该尽量保证一条发出的消息被实时地投递到接收方,保证用户通信的连贯性,这样才能达到即时通信所强调的“即时”。
  • 有序性:消息需要按照发送时间严格排序,不能发生时空错乱。即时通信毕竟与日常交流一样有提问才有回答,也就是消息之间存在因果关系。比如用户1在群聊中问用户2“吃烤肉还是吃火锅”,用户2回答“吃火锅”,那么群聊中的其他用户看到的消息列表不应该是用户2先回答“吃火锅”,然后用户1再提问;否则,他们会疑惑用户2是怎么做到抢答的。

本节我们先围绕准确性、消息不可丢失和实时性来介绍消息投递的几个重点话题,而关于维护消息的有序性内容将在接下来的章节中详细介绍。

13.3.1 存储消息:读扩散与写扩散

消息不可丢失意味着必须要把消息保存起来,而为了保证消息的准确性,又需要把消息保存到其所属会话的信箱中。信箱主要有两种实现模式,分别是读扩散模式和写扩散模式。

在读扩散模式下,每个会话占用一个信箱,与会话相关的用户收发消息就是对这个信箱进行读/写。如图13-1所示,用户u1与用户u2、用户u3以及g1群组的会话都对应一个信箱,会话信箱被会话成员共享。

  • 对于单聊场景来说,当用户u1向用户u2发送消息时,消息被投递到u1-u2单聊会话的信箱中,用户u2从这个信箱中接收用户u1发来的消息;当用户u2向用户u1发送消息时,消息同样被投递到这个信箱中,用户u1从这个信箱中接收用户u2发来的消息。
  • 对于群聊场景来说,g1群组中的每个成员都向g1群聊会话的信箱中投递自己发出的消息和从这个信箱中接收别人发来的消息。

image-20250503220033521

用户在自己的设备上拉取新消息时,IM服务需要从此用户的所有相关会话中拉取新消息,即IM服务需要将拉取新消息的请求扩散为N个拉取会话消息的请求,所以这种消息存储模式被称为“读扩散”。读扩散的优势是发送消息的逻辑非常轻量,只需要把消息写入信箱即可,而不管会话是属于单聊场景还是属于群聊场景。此外,每条消息仅被存储 一次,较为节省存储空间。不过,从此模式的名字上就可以看出其核心缺点,那就是用户读取消息有严重的读放大问题,同时在线人数越多或者会话越多,读放大问题带来的流量越大。

与读扩散模式相对应的一种模式是写扩散模式。在写扩散模式下,会话信箱不再被与会话相关的用户全局共享,而是每个用户都拥有一个自己关联会话的信箱列表。如图13-2所示,用户u1、用户u2、用户u3之间单聊会话的信箱在每个用户的会话信箱列表中都会被冗余保存一份,g1群组的会话信息同样会被保存在群组内每个成员的信箱列表中。

image-20250503220501175

在写扩散模式下,用户发送消息,不仅需要把消息投递到自己的会话信箱中,而且需要把消息投递到消息接收者的会话信箱中。

  • 在单聊场景下,用户u1向用户u2发送消息,既要将消息写入自己的“with u2”信箱中,又要将消息写入用户u2的“with u1”信箱中,即发送消息的写请求被扩散为原来的2倍;

  • 在群聊场景下,用户u1向有N个成员的g1群组发送消息,需要把消息依次写入群组内每个成员的“in g1”信箱中,即发送消息的写请求被扩散为原来的N倍。

写扩散模式的缺点也显而易见,即发送消息的请求存在写放大问题,对于单聊场景来说,只放大2倍尚可接受,但是对于有更多成员的群聊场景来说,写放大问题尤为严重。写放大带来的另一个问题是消息被冗余存储多份,比较浪费存储空间。

但是写扩散模式对用户接收消息非常友好。无论是单聊场景还是群聊场景,用户只需要从自己的信箱列表中拉取消息即可,即只需要一次读消息请求就能满足要求,这是写扩散模式的优势所在。

写扩散模式无视当前的在线人数与会话数量,但是其带来的写放大问题在群聊场景中会明显地暴露出来,且群组成员人数越多,问题越严重。所以,写扩散模式更适合单聊模式或者小群模式(限定群组成员总人数)的场景。

那么,到底是采用读扩散模式还是写扩散模式,归根结底,还是要根据群聊场景在聊天功能中的比重,以及群聊场景是否支持大群(群组成员人数较多)来决定。

  • 聊天功能完全面向单聊场景,则适合采用写扩散模式,因为写请求只被固定放大2倍,没有不可控的高并发流量。
  • 聊天功能虽然支持群聊场景,但是极少有群聊场景的出现(比如求职类应用、电商类应用等的聊天功能只是为了增进交流),则适合采用写扩散模式,因为群聊场景的写放大问题的影响微乎其微。
  • 对于即时通信软件,单聊场景和群聊场景都广泛存在(比如交友软件、办公通信软件),那么单聊场景自然适合采用写扩散模式,群聊场景则需要根据群组成员人数进一步细化选型:当群组成员人数小于N时,采用写扩散模式;当群组成员人数大于N时 ,采用读扩散模式。N的取值往往根据IM服务的压测表现来定,当群组成员人数超过某个阈值,发送消息产生严重的性能劣化时,使用此阈值作为N。

13.3.2 接收消息:拉模式与推模式

将消息存储到会话信箱中可以保证消息的准确性和不丢失 ,但是消息尚未真正触达用户设备。下面我们就来讨论用户设备是如何获取消息的。

用户设备获取消息的一种方式是采用拉模式,即用户设备周期性地主动向IM服务后台发起获取消息的请求,由服务端将所有相关会话的未读消息返回。拉模式的实现方式较为简单,但是用户设备多久拉取一次消息并不好确定:为了实时投递消息,用户设备可以1s拉取一次消息,但是这会给IM服务后台带来巨大的访问压力,造成大量计算资源的浪费;如果30s拉取一次消息,那么消息的实时性表现会很差。

用户设备获取消息的另一种方式是采用推模式。推模式与拉模式相反,用户每发送一条消息,IM服务后台都要把消息存储到会话信箱中,同时顺便把消息直接推送到用户设备上,这里使用的是第6章介绍的海量推送系统。

如图13-3所示,用户u1向用户u2发送消息,一方面,将消息存储到u1-u2会话信箱中;另一方面,将消息交给推送系统实时推送到用户u2的设备上。

image-20250503220839552

借助推送系统与用户设备维持的长连接,推模式有效地保证了消息的实时触达。然而, 长连接毕竟是脆弱的网络连接,网络中断、网络抖动等情况都会导致消息无法触达用户设备。事实上,没有任何推送系统可以在只采用推模式的前提下做到100%的消息推送,所以说完全依赖推模式接收消息会有丢失消息的情况发生,这是推模式的主要缺点。

但是推模式与拉模式可以优势互补,我们自然想到将两者结合起来,即形成推拉结合模式。当用户u1发送消息时,先将消息存储到对应的会话信箱中,然后向目标用户设备推送此消息;同时,用户设备周期性地从IM服务后台拉取消息,作为推送消息触达失败的补偿手段,防止消息丢失。推拉结合模式的消息推送架构如图13-4所示。

image-20250503220955376

推拉结合模式还有另一种实现方式:服务端只向客户端推送通知,用于告知客户端“有一条新消息到来”,然后由客户端来拉取对应的消息内容,如图13-5所示。不同于直接推送消息本身,采用这种只推送通知的方式,可以在客户端真正要读取消息时才传输消息内容,在一定程度上节约了网络带宽。

image-20250503221054268

业界绝大多数应用在聊天场景中都采用了推拉结合模式来获取消息。无论是直接推送消息,还是只推送通知,都属于推拉结合模式的表现形式。为了简化问题,这里假设本章介绍的推拉结合模式是直接推送消息。