8

基于实践:一套百万消息量小规模IM系统技术要点总结

 2 years ago
source link: http://www.blogjava.net/jb2011/archive/2021/11/27/436064.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文由公众号“后台技术汇”分享,原题“基于实践,设计一个百万级别的高可用 & 高可靠的 IM 消息系统”,原文链接在文末。由于原文存在较多错误和不准确内容,有大量修订和改动。

大家好,我是公众号“后台技术汇”的博主“一枚少年”。

本人从事后台开发工作 3 年有余了,其中让我感触最深刻的一个项目,就是在两年前从架构师手上接过来的 IM 消息系统。

本文内容将从开发者的视角出发(主要是我自已的开发体会),围绕项目背景、业务需求、技术原理、开发方案等主题,一步一步的与大家一起剖析:设计一套百万消息量的小规模IM系统架构设计上需要注意的技术要点。

(本文同步发布于:http://www.52im.net/thread-3752-1-1.html

2、项目背景

我们仔细观察就能发现,生活中的任何类型互联网服务都有 IM 系统的存在。

比如:

  • 1)基础性服务类-腾讯新闻(评论消息);
  • 2)商务应用类-钉钉(审批工作流通知);
  • 3)交流娱乐类-QQ/微信(私聊群聊 &讨论组 &朋友圈);
  • 4)互联网自媒体-抖音快手(点赞打赏通知)。

在这些林林总总的互联网生态产品里,即时消息系统作为底层能力,在确保业务正常与用户体验优化上,始终扮演了至关重要的角色。

所以,现如今的互联网产品中,即时通讯技术已经不仅限于传统IM聊天工具本身,它早已通过有形或无形的方式嵌入到了各种形式的互联网应用当中。IM技术(或者说即时通讯技术)对于很多开发者来说,确实是必不好可少的领域知识,不可或缺。

3、系统能力

典型的IM系统通常需要满足四点能力:高可靠性、高可用性、实时性和有序性。

这几个概念我就不详细展开,如果你是IM开发入门者,可以详读下面这几篇:

4、架构设计

以我的这个项目来说,架构设设计要点主要是:

  • 1)微服务:拆分为用户微服务 &消息连接服务 &消息业务服务;
  • 2)存储架构:兼容性能与资源开销,选择 reids&mysql;
  • 3)高可用:可以支撑起高并发场景,选择 Spring 提供的 websocket;
  • 4)支持多端消息同步:app 端、web 端、微信公众号、小程序消息;
  • 5)支持在线与离线消息场景。

业务架构图主要是这样:

技术模块分层架构大概是这样:

5、消息存储技术要点

5.1 理解读扩散和写扩散

5.1.1)基本概念:

我们举个例子说明什么是读扩散,什么是写扩散:

一个群聊“相亲相爱一家人”,成员:爸爸、妈妈、哥哥、姐姐和我(共 5 人)。

因为你最近交到女朋友了,所以发了一条消息“我脱单了”到群里面,那么自然希望爸爸妈妈哥哥姐姐四个亲人都能收到了。

正常逻辑下,群聊消息发送的流程应该是这样:

  • 1)遍历群聊的成员并发送消息;
  • 2)查询每个成员的在线状态;
  • 3)成员不在线的存储离线;
  • 4)成员在线的实时推送。

数据分发模型如下:

问题在于:如果第4步发生异常,群友会丢失消息,那么会导致有家人不知道“你脱单了”,造成催婚的严重后果。

所以优化的方案是:不管群员是否在线,都要先存储消息。

按照上面的思路,优化后的群消息流程如下:

  • 1)遍历群聊的成员并发送消息;
  • 2)群聊所有人都存一份;
  • 3)查询每个成员的在线状态;
  • 4)在线的实时推送。

以上优化后的方案,便是所谓的“写扩散”了。

问题在于:每个人都存一份相同的“你脱单了”的消息,对磁盘和带宽造成了很大的浪费(这就是写扩散的最大弊端)。

所以优化的方案是:群消息实体存储一份,用户只存消息 ID 索引。

于是再次优化后的发送群消息流程如下:

  • 1)遍历群聊的成员并发送消息;
  • 2)先存一份消息实体;
  • 3)然后群聊所有人都存一份消息实体的 ID 引用;
  • 4)查询每个成员的在线状态;
  • 5)在线的实时推送。

二次优化后的方案,便是所谓的“读扩散”了。

5.1.2)小结一下:

  • 1)读扩散:读取操作很重,写入操作很轻,资源消耗相对小一些;
  • 2)写扩散:读取操作很轻,写入操作很重,资源消耗相对大一些。

从公开的技术资料来看,微信和钉钉的群聊消息应该使用的是写扩散方式,具体可以参看这两篇:《微信后台团队:微信后台异步消息队列的优化升级实践分享》、《阿里IM技术分享(四):闲鱼亿级IM消息系统的可靠投递优化实践》(注意“5.5 服务端存储模型优化”这一节)。

5.2 “消息”所关联的对象

5.2.1)消息实体模型:

常见的消息业务,可以抽象为几个实体模型概念:用户/用户关系/用户设备/用户连接状态/消息/消息队列。

在IM系统中的实体模型关系大致如下:

5.2.2)实体模型概念解释:

用户实体:

  • 1)用户->用户终端设备:每个用户能够多端登录并收发消息;
  • 2)用户->消息:考虑到读扩散,每个用户与消息的关系都是 1:n;
  • 3)用户->消息队列:考虑到读扩散,每个用户都会维护自己的一份“消息列表”(1:1),如果考虑到扩容,甚至可以开辟一份消息溢出列表接收超出“消息列表”容量的消息数据(此时是 1:n);
  • 4)用户->用户连接状态:考虑到用户能够多端登录,那么 app/web 都会有对应的在线状态信息(1:n);
  • 5)用户->联系人关系:考虑到用户最终以某种业务联系到一起,组成多份联系人关系,最终形成私聊或者群聊(1:n);

联系人关系(主要由业务决定用户与用户之间的关系),比如说:

  • 1)某个家庭下有多少人,这个家庭群聊就有多少人;
  • 2)在 ToB 场景,在钉钉企业版里,我们往往有企业群聊这个存在。

消息实体:

消息->消息队列:考虑到读扩散,消息最终归属于一个或多个消息队列里,因此群聊场景它会分布在不同的消息队列里。

消息队列实体:

消息队列:确切说是消息引用队列,它里面的索引元素最终指向具体的消息实体对象。

用户连接状态:

  • 1)对于 app 端:网络原因导致断线,或者用户手动 kill 掉应用进程,都属于离线;
  • 2)对于 web 端:网络原因导致浏览器断网,或者用户手动关闭标签页,都属于离线;
  • 3)对于公众号:无法分别离线在线;
  • 4)对于小程序:无法分别离线在线。

用户终端设备:

客户端一般是 Android&IOS,web 端一般是浏览器,还有其他灵活的 WebView(公众号/小程序)。

5.3 消息的存储方案

对于消息存储方案,本质上只有三种方案:要么放在内存、要么放在磁盘、要么两者结合存储(据说大公司为了优化性能,活跃的消息数据都是放在内存里面的,毕竟有钱~)。

下面分别解析主要方案的优点与弊端:

  • 1)方案一:考虑性能,数据全部放到 redis 进行存储;
  • 2)方案二:考虑资源,数据用 redis + mysql 进行存储。

5.3.1)对于方案一:redis

前提:用户 & 联系人关系,由于是业务数据,因此统一默认使用关系型数据库存储。

流程图:

解释如下:

  • 1)用户发消息;
  • 2)redis 创建一条实体数据 &一个实体数据计时器;
  • 3)redis 在 B 用户的用户队列 添加实体数据引用;
  • 4)B 用户拉取消息(后续 5.2 会提及拉模式)。

实现方案:

  • 1)用户队列,zset(score 确保有序性);
  • 2)消息实体列表,hash(msg_id 确保唯一性);
  • 3)消息实体计数器,hash(支持群聊消息的引用次数,倒计时到零时则删除实体列表的对应消息,以节省资源)。

优点是:内存操作,响应性能好

弊端是:

  • 1)内存消耗巨大,eg:除非大厂,小公司的服务器的宝贵内存资源是耗不起业务的,随着业务增长,不想拓展资源,就需要手动清理数据了;
  • 2)受 redis 容灾性策略影响较大,如果 redis 宕机,直接导致数据丢失(可以使用 redis 的集群部署/哨兵机制/主从复制等手段解决)。

5.3.2)方案二:redis+mysql

前提:用户 & 联系人关系,由于是业务数据,因此统一默认使用关系型数据库存储。

流程图:

解释如下:

  • 1)用户发消息;
  • 2)mysql 创建一条实体数据;
  • 3)redis 在 B 用户的用户队列 添加实体数据引用;
  • 4)B 用户拉取消息(下文会提及拉模式)。

实现方案:

  • 1)用户队列,zset(score 确保有序性);
  • 2)消息实体列表,转移到 mysql(表主键 id 确保唯一性);
  • 3)消息实体计数器,hash(删除这个概念,因为磁盘可用总资源远远高于内存总资源,哪怕一直存放 mysql 数据库,在业务量百万级别时也不会有大问题,如果是巨大体量业务就需要考虑分表分库处理检索数据的性能了)。

优点是:

  • 1)抽离了数据量最大的消息实体,大大节省了内存资源;
  • 2)磁盘资源易于拓展 ,便宜实用。

弊端是:磁盘读取操作,响应性能较差(从产品设计的角度出发,你维护的这套 IM 系统究竟是强 IM 还是弱 IM)。

6、消息的消费模式

6.1 拉模式

选用消息拉模式的原因:

  • 1)由于用户数量太多(观察者),服务器无法一一监控客户端的状态,因此消息模块的数据交互使用拉模式,可以节约服务器资源;
  • 2)当用户有未读消息时,由客户器主动发起请求的方式,可以及时刷新客户端状态。

6.2 ack 机制

技术原理:

  • 1)基于拉模式实现的数据拉取请求(第一次 fetch 接口)与数据拉取确认请求(第二次 fetch 接口)是成对出现的;
  • 2)客户端二次调用 fetch 接口,需要将上次消息消费的锚点告诉服务端,服务器进而删除已读消息。

请求模型原理图如下:

实现方案1:基于每一条消息编号 ACK:

  • 1)实现:客户端在接收到消息之后,发送 ACK 消息编号给服务端,告知已经收到该消息。服务端在收到 ACK 消息编号的时候,标记该消息已经发送成功;
  • 2)弊端:这种方案,因为客户端逐条 ACK 消息编号,所以会导致客户端和服务端交互次数过多。当然,客户端可以异步批量 ACK 多条消息,从而减少次数。

实现方案2:基于滑动窗口 ACK:

1)客户端在接收到消息编号之后,和本地的消息编号进行比对:

 - 如果比本地的小,说明该消息已经收到,忽略不处理;

 - 如果比本地的大,使用本地的消息编号,向服务端拉取大于本地的消息编号的消息列表,即增量消息列表。

 - 拉取完成后,更新消息列表中最大的消息编号为新的本地的消息编号;

2)服务端在收到 ack 消息时,进行批量标记已读或者删除。

这种方式,在业务被称为推拉结合的方案,在分布式消息队列、配置中心、注册中心实现实时的数据同步,经常被采用。

6.3 基于ack 机制的好处

第一次获取消息完成之后,如果没有 ack 机制,流程是:

  • 1)服务器删除已读消息数据;
  • 2)服务端把数据包响应给客户端。

如果由于网络延迟,导致客户端长时间取不到数据,这时客户端会断开该次 HTTP 请求,进而忽略这次响应数据的处理,最终导致消息数据被删除而后续无法恢复。

有了 ack 机制,哪怕第一次获取消息失败,客户端还是可以继续请求消息数据,因为在 ack 确认之前,消息数据都不会删除掉。

7、微服务设计

一般来说 IM 微服务,能拆分为基础的三个微服务:

  • 1)用户服务;
  • 2)业务服务;
  • 3)连接管理服务。

参考架构图:

他们分工合作如下。

用户微服务(用户设备的登录 & 登出):

  • 1)设备号存库;
  • 2)连接状态更新;
  • 3)其他登录端用户踢出等。

连接管理微服务:

  • 1)状态保存:保存用户设备长连接对象;
  • 2)剔除无效连接:轮训已有长连接对象状态,超时删除对象;
  • 3)接受客户端的心跳包:刷新长连接对象的状态。

消息业务微服务:

  • 1)消息存储:进行私聊/群聊的消息存储策略(请参看“消息存储模型”一节);
  • 2)消息消费:进行消息获取响应与 ack 确认删除(请参看“消息消费模式”一节);
  • 3)消息路由:用户在线时,路由消息通知包到“消息连接管理微服务”,以通知用户客户端来取消息。

最后提一下消息的路由:

微服务之间也有通信手段,比如业务服务到连接管理服务,两者之间可以通过 RPC 实现实时消息的路由通知。

8、离线消息推送

离线推送方案上,大家一般都会考虑采用两种方案:

  • 1)企业自研后台离线 PUSH 系统;
  • 2)企业自行对接第三方手机厂商 PUSH 系统。

8.1 企业自研后台离线 PUSH 系统

技术原理:

在应用级别,客户端与后台离线 PUSH 系统保持长连接,当用户状态被检测为离线时,通过这个长连接告知客户端“有新消息”,进而唤醒手机弹窗标题。

弊端就是:

随着安卓和苹果系统的限制越来越严格,一般客户端的活动周期被限制的死死的,一旦客户端进程被挪到后台就立马被 kill 掉了,导致客户端保活特别难做好(这也是很多中小企业头疼的地方,毕竟只有微信或者 QQ 这种体量的一级市场 APP,手机系统愿意给他们留后门来做保活)。具体可以读一下《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦》这篇。

8.2 企业自行对接第三方厂商 PUSH 系统

技术原理:

在系统级别,每个硬件系统都会与对应的手机厂商保持长连接,当用户状态被检测为离线时,后台将推送报文通过 HTTP 请求,告知第三方手机厂商服务器,进而通过系统唤醒 app 的弹窗标题。

弊端就是:

  • 1)作为应用端,消息是否确切送达给用户侧,是未知的;推送的稳定性也取决于第三方手机厂商的服务稳定性;
  • 2)额外进行 sdk 的对接工作,增加了工作量;
  • 3)第三方厂商随时可能升级 sdk 版本,导致没有升级 sdk 的服务器出现推送失败的情况,给 Sass 系统部署带来困难;
  • 4)推送证书配置也要考虑到维护成本。

总之,IM里离线消息推送是个很头疼的问题(当然这里主要说是Andriod了,iOS里苹果官方的APNs就舒服多了),有兴趣好一读一下下面这些文章:

9、其它需要考虑的技术要点

9.1 安全性

关于IM安全性,我个人的体会是这样:

  • 1)业务数据传输安全性使用 https 访问;
  • 2)实时消息使用SSL/TLS对长连接进行加密;
  • 3)使用私有协议,不容易解析;
  • 4)内容安全性端到端加密,中间任何环节都不能解密(即发送和接收端交换互相的密钥来解密,服务器端解密不了);
  • 5)服务器端不存储消息。

以上要点中:IM中的长连接安全性是比较重要且不容易处理的,因为它需要在安全性和性能上作平衡和取舍(不能光顾着安全而损失IM长连接的高吞吐性能),这方面可以参考微信团队分享的这篇《微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解》。

另外:更高安全性的场景可以考虑组合加密方案,详情可以参考《探讨组合加密算法在IM中的应用》。

9.2 一致性

IM消息一致性难题,主要是保证消息不乱序的问题。这个话题,初学者可以读读这篇《零基础IM开发入门(四):什么是IM系统的消息时序一致性?》,我就不再赘述了。

解决一致性问题的切入点有很多,最常见的是使用有序的消息唯一id,关于有序且唯一的ID生成问题,微信团队的思路就很好,可以借鉴一下《微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)》。

另外,以下几篇关于消息有序性问题的总结也非常好,可以进行参考:

9.3 可靠性

IM里所谓的可靠性,说直白一点就是保证消息不丢失,这看似理所当然、稀松平常的技术点,在IM系统中又是另一个很大的话题,鉴于本人水平有限,就不班门弄斧,IM初学者可以能过《零基础IM开发入门(三):什么是IM系统的可靠性?》这篇来理解可靠性这个概念。

然后再读读《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》、《IM消息送达保证机制实现(二):保证离线消息的可靠投递》这两篇,基本上就能对IM可靠性这个技术要点有了比较深刻的认识了。

下面这几篇实战性的总结,适合有一定IM经验的同行们学习,可以借鉴学习一下:

9.4 实时性

IM实时性这个技术点,就回归到了“即时通讯”这个技术的立身之本了,可以说,没有实时性,也就不存在“即时通讯”这个技术范畴了,可以见它的重要性。关于实时性这个概念,初学者可以通过《零基础IM开发入门(二):什么是IM系统的实时性?》这篇去学习一下,我就不啰嗦了,人家比我说的好。

笔者公司的项目里实时通信用方案都是采用 WebSocket(如果你不了解WebSocket,可以读一下《WebSocket从入门到精通,半小时就够!》,以及《搞懂现代Web端即时通讯技术一文就够:WebSocket、socket.io、SSE》),但是某些低版本的浏览器可能不支持 WebSocket,所以实际开发时,要兼容前端所能提供的能力进行方案设计。

以下两篇关于实时性的同行实践性总结也不错:

10、我在项目实践中的体会

作为研发者,有两年多的时间都在维护迭代公司的 IM 消息系统,以下是我自已的小小体会。

我体会到的重点难点有以下几方面:

  • 1)业务闭环:消息是如何写入存储、消息是如何消费掉、在线消息是如何实现、离线消息是如何实现、群聊/私聊有何不一样、多端消息如何实现;
  • 2)解 Bug 填坑:在线消息收不到,第三方推送证书如何配置;
  • 3)代码优化:单体架构拆分微服务;
  • 4)存储优化:1.0 版本的 redis 存储到 2.0 版本的 redis+mysql;
  • 5)性能优化:未读提醒等接口性能优化。

项目还存在可优化的地方:

  • 1)高可用方案之一:是部署多部连接管理服务器,以支撑更多的用户连接;
  • 2)高可用方案之二:是对单部连接管理服务,使用 Netty 进行框架层优化,让一个服务器支撑更多的用户连接;
  • 3)消息量剧增时:可以考虑对消息存储作进一步优化;
  • 4)消息冷热部署:不同的地区会存在业务量差异,比如在某些经济发达的省份,IM 系统面临的压力会比较大,一些欠发达省份,服务压力会低一点,所以这块可以考虑数据的冷热部署。

11、写在最后

两年前从架构师手上接过来的 IM 消息系统模块,让我逐步培养了架构思维,见贤思齐,感谢恩师。

IM技术是个经久不衰的领域,但同时可直接使用的技术资产也非常匮乏,必竟传统的IM巨头们的产品通常都是私有化协议、私有化方案,很难有业界共同的方案可以直接使用(包括资料或开源代码),正是这种不通用、不准,间接导致IM技术门槛的提高。所以通常公司要搞IM的话,如果没有技术积累,就只能从零开始造轮子。

为了改变这种局面,也希望搞IM开发的同学不要闷头造车,应该多多借鉴同行的思路,同时也能积极分享自已的经验,让IM开发不再痛苦。

以上抛砖引玉,欢迎留言讨论,一起进步。

12、参考资料

[1] 新手入门一篇就够:从零开发移动端IM

[2] 为何基于TCP协议的移动端IM仍然需要心跳保活机制?

[3] Android P正式版即将到来:后台应用保活、消息推送的真正噩梦

[4] WebSocket从入门到精通,半小时就够!

[5] 搞懂现代Web端即时通讯技术一文就够:WebSocket、socket.io、SSE

[6] 一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

[7] 一套原创分布式即时通讯(IM)系统理论架构方案

[8] 一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

[9] 微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)

[10] 阿里IM技术分享(四):闲鱼亿级IM消息系统的可靠投递优化实践

[11] 阿里IM技术分享(五):闲鱼亿级IM消息系统的及时性优化实践

[12] 一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等

[13] 从新手到专家:如何设计一套亿级消息量的分布式IM系统

[14] 企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等

[15] 融云技术分享:全面揭秘亿级IM消息的可靠投递机制

[16] 即时通讯安全篇(六):非对称加密技术的原理与应用实践

[17] 通俗易懂:一篇掌握即时通讯的消息传输安全原理

[18] 微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解

[19] 零基础IM开发入门(二):什么是IM系统的实时性?

[20] 零基础IM开发入门(三):什么是IM系统的可靠性?

[21] 零基础IM开发入门(四):什么是IM系统的消息时序一致性?

本文已同步发布于“即时通讯技术圈”公众号。

同步发布链接是:http://www.52im.net/thread-3752-1-1.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK