IM即时通讯-3-如何设计消息协议层方案

时间:2024-10-28 08:20:16

1. 前言

上篇即时通讯系列—如何下手做技术方案设计 最后总结出IM系统的端侧基本结构
后续文章将从下到上以此做架构设计.

本文Agenda

  1. 什么是消息同步
  2. 同步协议的常见设计方案包含哪些
  3. 应该采用哪种方案
  4. 方案细节

2. 名词定义:

消息漫游 : 用户如何从消息服务器获取会话和消息,指即时通讯服务将用户的历史消息保存在消息服务器上,用户即使切换终端设备,也能从服务器获取到单聊、群聊的历史消息,保持一致的会话场景。来源

在线同步 : 即用户的设备处于登录态, 并且终端设备与服务端的连接状态是online的, 此时有消息或者会话的变更被及时同步下来, 便被称之为在线同步.

离线同步: 即用户的设备未处于登录态, 或者终端设备与服务端的连接状态是offline的, 此时有消息或者会话的变更被及时同步下来, 便被称之为离线同步.

3. 什么是消息同步

消息同步, 指的是用户的会话和消息, 可以在线同步和离线同步.

存在会话的增删改查, 也存在会话消息的增删改查的同步. 对于一个完整的IM的系统而言, 还存在用户的Profile的同步等等.
因而整体需要服务端跟客户端的一个消息的同步.

4. 消息同步有哪几种方案

消息同步不整体可以分为推模式, 拉模式, 以及推拉结合的模式.

case 场景分析

用户在端设备上, 消息同步可以归纳为四个场景

  1. 新设备用户
  2. 用户短暂离线再次在线(app进程被杀死, 用户短时间退出登录, 再次登录)
  3. 用户长期离线再次在线(用户1个月未登录此设备,再次登录)
  4. 用户在线收消息

假设用户当前有4个会话, 2个单聊和2个群聊.
具体每个会话的消息有多少条消息, 参见下方图示.
接下来根据四种场景进行展示
请添加图片描述

case1 新设备用户

请添加图片描述

正常逻辑而言, 需要同步的内容

  1. 同步所有的会话
  2. 同步所有会话的消息

case2: 用户短暂离线再次在线请添加图片描述

正常逻辑而言, 需要同步的内容

  1. 同步缺失的会话
  2. 同步所有会话的消息

case3 用户长期离线再次在线

请添加图片描述)

正常逻辑而言, 需要同步的内容

  1. 同步缺失的会话
  2. 同步所有会话的消息

case4 用户在线收消息

在这里插入图片描述
正常逻辑而言, 需要同步的内容

  1. 同步缺失的会话
  2. 同步所有会话的消息

"用户短暂离线后再次在线"的场景, 用户的消息是批次下发下来的.
"用户在线收消息"的场景, 用户的消息是逐条(消息间隔的时间超过同步时机)或者是小批次下来的(如果几条消息间隔很近, 小于同步时机)

结论

  1. 需要同步的内容有两类, 会话+会话的消息
  2. 需要同步的内容, 可能有很多, 全量同步可能会很耗时

方案1 推模式

什么是推模式

服务端和客户端之间, 消息同步采用的是***服务端将变更推送给端侧***
如: 在需要同步的消息量少时, 采取推模式, 而在消息量大时, 仅拉取最新会话, 仅拉取会话, 不拉取

方案2 拉模式

服务端和客户端之间, 消息同步采用的是***端侧主动向服务端拉取消息***
即以端侧为主导, 所有的同步时机, 为端侧主动发起, 向服务端拉取数据. 如最新的会话变更, 以及最新的消息. 再如新设备用户, 仅拉取会话列表, 用户再进入到会话详情时, 再拉取消息.

方案3 推拉结合的模式

服务端和客户端之间, 消息同步采用的是***部分场景下采用推模式, 部分场景下采用拉取模式***
根据不同的场景, 采取不同的策略.
如新设备用户, 采取拉模式, 将会话列表拉取下来, 但是不拉取消息. 用户进入到会话详情时, 再拉取消息. 用户在线收消息, 采取推模式. sever将推送会话和消息.

方案之间对比

在这里插入图片描述

5. 应该采用哪种方案

采取那种方案, 是根据业务诉求决定的, 一般对于及时通讯而言, 实时性要求较高, 因而通常情况下, 采用***推拉结合***的策略较多. 我们因而接下来主要从推拉结合的方式展开.

消息存储库: 用于全量保存所有会话的消息,主要用于支持消息漫游
消息同步库: 主要用于接收方的消息同步(以及多端同步). 规定阈值内的变更可以在这里同步到. 可以快速的收取消息.

6. 推拉结合的技术方案细节

6.1 端到端的技术选型

推拉结合的模型对应技术的选型, 比较合适的是server侧拆分为 消息同步库和消息存储库. 参考链接

在这里插入图片描述

Timeline 模型

引用内容, 原文

Timeline模型是一个对消息抽象的逻辑模型,该模型会帮助我们简化对消息同步和存储模型的理解,
而消息同步库和存储库的设计和实现也是围绕Timeline的特性和需求来展开。
在这里插入图片描述

上图是Timeline模型的一个抽象表述,Timeline可以简单理解为是一个消息队列,但这个消息队列有如下特性。
每条消息对应一个顺序ID:每个消息拥有一个唯一的顺序ID(SequenceId),队列消息按SequenceId排序。
新消息写入能自动分配递增的顺序ID,保证永远插入队尾:Timeline中是根据同步位点也就是顺序ID来同步消息,所以需要保证新写入的消息数据的
顺序ID绝对不能比已同步的消息的顺序ID还小,否则会导致数据漏同步,所以需要支持对新写入的数据自动分配比当前已存储的所有消息的顺序ID更
大的顺序ID。
新消息写入也能自定义顺序ID,满足自定义排序需求:上面提到的自动分配顺序ID,主要是为了满足消息同步的需求,消息同步要求消息是根据『已
同步』或是『已写入』的顺序来排序。而消息的存储,通常要求消息能根据会话顺序来排序,会话顺序通常由端的会话来决定,而不是服务端的同步
顺序来定,这是两种顺序要求。
支持根据顺序ID的随机定位:可根据SequenceId随机定位到Timeline中的某个位置,从这个位置开始正序或逆序的读取消息,也可支持读取指定顺序
ID的某条消息。
支持对消息的自定义索引:消息体内数据根据业务不同会包含不同的字段,Timeline需要支持对不同字段的自定义索引,来支持对消息内容的全文索
引,或者是任意字段的灵活条件组合查询。

消息同步可以基于Timeline很简单的实现。图中的例子中,消息发送方是A,消息接收方是B,同时B存在多个接收端,分别是B1、B2和B3。A向B发
送消息,消息需要同步到B的多个端,待同步的消息通过一个Timeline来进行交换。A向B发送的所有消息,都会保存在这个Timeline中,B的每个接收
端都是独立的从这个Timeline中拉取消息。每个接收端同步完毕后,都会在本地记录下最新同步到的消息的SequenceId,即最新的一个位点,作为下
次消息同步的起始位点。服务端不会保存各个端的同步状态,各个端均可以在任意时间从任意点开始拉取消息。

消息存储也是基于Timeline实现,和消息同步唯一的区别是,消息存储要求服务端能够对Timeline内的所有数据进行持久化,并且消息采用会话顺序来
保存,需要自定义顺序ID。

消息检索基于Timeline提供的消息索引来实现,能支持比较灵活的多字段索引,根据业务的不同可有*度较高的定制。

消息存储模型

引用内容, 原文

在这里插入图片描述
上图是基于Timeline的消息存储模型,消息存储要求每个会话都对应一个独立的Timeline。如图例子所示,A与B/C/D/E/F均发生了会话,每个会话对
应一个独立的Timeline,每个Timeline内存有这个会话中的所有消息,消息根据会话顺序排序,服务端会对每个Timeline进行持久化存储,也就拥有了
消息漫游的能力。

消息同步模型

引用内容, 原文

消息同步模型会比消息存储模型稍复杂一些,消息的同步一般有读扩散(也叫拉模式)和写扩散(也叫推模式)两种不同的方式,分别对应不同的
Timeline物理模型。
在这里插入图片描述
如图是读扩散和写扩散两种不同同步模式下对应的不同的Timeline模型。按图中的示例,A作为消息接收者,其与B/C/D/E/F发生了会话,每个会话中
的新的消息都需要同步到A的某个端。
读扩散:消息存储模型中,每个会话的Timeline中保存了这个会话的全量消息。读扩散的消息同步模式下,每个会话中产生的新的消息,只需要写一次到其用于存储的Timeline中,接收端从这个Timeline中拉取新的消息。优点是消息只需要写一次,相比写扩散的模式,能够大大降低消息写入次数,特别是在群消息这种场景下。但其缺点也比较明显,接收端去同步消息的逻辑会相对复杂和低效。接收端需要对每个会话都拉取一次才能获取全部消息,读被大大的放大,并且会产生很多无效的读,因为并不是每个会话都会有新消息产生。
写扩散:写扩散的消息同步模式,需要有一个额外的Timeline来专门用于消息同步,通常是每个接收端都会拥有一个独立的同步Timeline(或者叫收件
箱),用于存放需要向这个接收端同步的所有消息。每个会话中的消息,会产生多次写,除了写入用于消息存储的会话Timeline,还需要写入需要同步到的接收端的同步Timeline。在个人与个人的会话中,消息会被额外写两次,除了写入这个会话的存储Timeline,还需要写入参与这个会话的两个接收者的同步Timeline。而在群这个场景下,写入会被更加的放大,如果这个群拥有N个参与者,那每条消息都需要额外的写N次。写扩散同步模式的优点是,在接收端消息同步逻辑会非常简单,只需要从其同步Timeline中读取一次即可,大大降低了消息同步所需的读的压力。其缺点就是消息写入会被放大,特别是针对群这种场景。
Timeline模型不会对选择读扩散还是写扩散做约束,而是能同时支持两种模式,因为本质上两种模式的逻辑数据模型并无差别,只是消息数据是用一
个Timeline来支持多端读还是复制到多个Timeline来支持多端读的问题。
针对IM这种应用场景,消息系统通常会选择写扩散这种消息同步模式。IM场景下,一条消息只会产生一次,但是会被读取多次,是典型的读多写少的
场景,消息的读写比例大概是10:1。若使用读扩散同步模式,整个系统的读写比例会被放大到100:1。一个优化的好的系统,必须从设计上去平衡这种
读写压力,避免读或写任意一维触碰到天花板。所以IM系统这类场景下,通常会应用写扩散这种同步模式,来平衡读和写,将100:1的读写比例平衡
到30:30。当然写扩散这种同步模式,还需要处理一些极端场景,例如万人大群。针对这种极端写扩散的场景,会退化到使用读扩散。一个简单的IM
系统,通常会在产品层面限制这种大群的存在,而对于一个高级的IM系统,会采用读写扩散混合的同步模式,来满足这类产品的需求。采用混合模
式,会根据数据的不同类型和不同的读写负载,来决定用写扩散还是读扩散。

端到端的架构设计

引用内容

在这里插入图片描述
上图是一个典型的消息系统架构,架构中包含几个重要组件。
端:作为消息的发送和接收端,通过连接消息服务器来发送和接收消息。
消息服务器:一组无状态的服务器,可水平扩展,处理消息的发送和接收请求,连接后端消息系统。
消息队列:新写入消息的缓冲队列,消息系统的前置消息存储,用于削峰填谷以及异步消费。
消息处理:一组无状态的消费处理服务器,用于异步消费消息队列中的消息数据,处理消息的持久化和写扩散同步。
消息存储和索引库:持久化存储消息,每个会话对应一个Timeline进行消息存储,存储的消息建立索引来实现消息检索。
消息同步库:写扩散形式同步消息,每个用户的收件箱对应一个Timeline,同步库内消息不需要永久保存,通常对消息设定一个生命周期。
新消息会由端发出,通常消息体中会携带消息ID(用于去重)、逻辑时间戳(用于排序)、消息类型(控制消息、图片消息或者文本消息等)、消息
体等内容。消息会先写入消息队列,作为底层存储的一个临时缓冲区。消息队列中的消息会由消息处理服务器消费,可以允许乱序消费。消息处理服
务器对消息先存储后同步,先写入发件箱Timeline(存储库),后写扩散至各个接收端的收件箱(同步库)。消息数据写入存储库后,会被近实时的
构建索引,索引包括文本消息的全文索引以及多字段索引(发送方、消息类型等)。

对于在线的设备,可以由消息服务器主动推送至在线设备端。对于离线设备,登录后会主动向服务端同步消息。每个设备会在本地保留有最新一条消
息的顺序ID,向服务端同步该顺序ID后的所有消息。

6.2 端到端的细节设计

章节6.1 我们只是知道了端侧与服务端整体的方案设计, 不过对于端侧具体什么时间点需要采用漫游的方式拉取, 以及什么时机采用同步的方式推送, 并没有很详细的描述. 本节内容详细展开.

结论先行: 近期数据(当做高频数据)做同步(推送模式), 远期数据(低频数据)做漫游(拉模式).

已有的同步协议

  1. 版本控制系统(git/svn) 基于拉取模式,主要用于同步文本文件
  2. CalDAV/CardDAV/WebDAV 主要用于同步日历,联系人等
  3. SyncML 公开的同步协议,基于XML
  4. Exchange ActiveSync(Mail, Calendar and Address Book)微软的协议
  5. WeiSync(微信) 公开分享说明是参考ActiveSync实现的,但没有详细的说明。
  6. Grouk多终端实时同步协议

基于业务场景主要选择推拉结合的方式以及用户的体验, 因而具有参考价值的是, Exchange ActivitySync以及WeiSync, 还有Grouk.

根据已有的文档分析

[微信技术总监谈架构:微信之道——大道至简(演讲全文)](http:///)
[什么是Exchange移动终端同步服务]( /help?doc_id=396&helpType=exmail)
[关于腾讯协议](/developer/news/122016)
[简单分析一下微信](/blog/2015/06/11/wx/)
[企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等](http:///)
[ActiveSync应用层程序协议分析-RAPI的握手过程 ](/bgfuufb/article/details/87264262)
[一套海量在线用户的移动端IM架构设计实践分享(含详细图文)](  http:///)
[移动时代的多终端同步协议设计与探索 | Grouk实践分享](/multi-device-sync-protocol-design/)

总结便可知, 大家是通过同步协议作为推模式, 用于解决近期数据(当做高频数据), 通过拉取模式, 用于解决消息漫游.

根据业务场景, 做细化

场景从简单到复杂

用户在线收消息

请添加图片描述

用户短暂离线后再次在线收消息

在这里插入图片描述

用户长期离线后再次在线收消息

在这里插入图片描述

用户首次登录

在这里插入图片描述

总结

根据上述的四个场景, 我们可以从另外一个角度分类看问题, 即在线同步 + 建连两类场景, 建联场景根据同步协议返回的是否gap过载区分场景.

场景1: 在线同步
  1. server直接下发同步协议的内容+最新同步位点
  2. 消息入库
  3. ack收到
场景2: 建连同步的gap消息过载
  1. server通过推模式给client, 同步数据过载+最新同步位点
  2. 处理client数据, 本地的gap处理.
  3. 端侧需要主动拉取数据
  4. ack同步收到
  5. 后续新消息, server按照用户在线时处理
场景3: 建连同步的gap消息不过载
  1. server通过推模式给client, 最新同步位点+gap的数据
  2. 端侧直接入库
  3. ack收到
  4. 后续新消息, server按照用户在线时处理

7. 引用

微信技术总监谈架构:微信之道——大道至简(演讲全文)
什么是Exchange移动终端同步服务
关于腾讯协议
简单分析一下微信
企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等
ActiveSync应用层程序协议分析-RAPI的握手过程
一套海量在线用户的移动端IM架构设计实践分享(含详细图文)
移动时代的多终端同步协议设计与探索 | Grouk实践分享
从新手到专家:如何设计一套亿级消息量的分布式IM系统
现代IM系统中消息推送和存储架构的实现
如何打造千万级Feed流系统
消息队列之推还是拉,RocketMQ 和 Kafka 是如何做的
现代IM系统中消息推送和存储架构的实现
现代IM系统中的消息系统—架构