前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《基于实践,设计一个百万级别的高可用 & 高可靠的 IM 消息系统》

《基于实践,设计一个百万级别的高可用 & 高可靠的 IM 消息系统》

作者头像
后台技术汇
发布2022-05-28 15:14:11
1.4K0
发布2022-05-28 15:14:11
举报
文章被收录于专栏:后台技术汇后台技术汇

原文链接:

https://xie.infoq.cn/article/4061081a5ce66137a8c021994

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

下面我将从开发者的视角出发,一步一步的与大家一起剖析:如何去设计一个能支撑起百万级别的高可用高可用的 IM 消息系统架构;

下面我主要围绕着七个主题进行说明:项目背景、背景需求、实现原理、开发方案、对比方案、成果展示和参考文献。

项目背景

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

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

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

系统需求

我们将 IM 系统的需求需要满足四点:高可靠性、高可用性、实时性和有序性。

1.架构设计

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

2.架构图

3.分层架构

实现原理

实现原理我们通过六个单元模块来剖析:

  • 消息存储模型:关注数据模型与存储工具选型
  • 消息消费模式:关注高可用性与性能
  • 消息实时通信:关注连接管理与框架选型
  • 微服务设计:关注业务代码划分
  • 离线消息方案:关注推送方式与实现方案
  • 总结:总结方案的各项指标

P1 消息存储模型

读扩散和写扩散

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

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

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

优化前的群聊消息发送的流程如下:

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

数据模型如下:

难点在于:如果第四步发生异常,群友会丢失消息,那么会导致有家人不知道“你脱单了”,造成催婚的严重后果。所以优化的方案是:不管群员是否在线,都要先存储消息。

一次优化后的发送群消息的流程优化如下:(写扩散)

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

数据模型如下:

难点在于:每个人都存一份相同的“你脱单了”的消息,对磁盘和带宽造成了很大的浪费(这就是写扩散)。所以优化的方案是:群消息实体存储一份,用户只存消息 ID 索引。

二次优化后的发送群消息的流程优化如下:(读扩散)

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

数据模型如下:

特点

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

从公开的技术资料来看,微信的群聊消息应该使用的是存多份(即扩散写方式),详细的方案可以在微信团队分享的这篇文章里找到答案:《微信后台团队:微信后台异步消息队列的优化升级实践分享》

消息模型

我们将消息业务需求抽象出六个消息模型点用户/联系人关系/用户设备/用户连接状态/消息/消息队列;

  • 用户
  • 用户->用户终端设备:每个用户能够多端登录并收发消息;
  • 用户->消息:考虑到读扩散,每个用户与消息的关系都是 1:n;
  • 用户->消息队列:考虑到读扩散,每个用户都会维护自己的一份“消息列表”(1:1),如果考虑到扩容,甚至可以开辟一份消息溢出列表接收超出“消息列表”容量的消息数据(此时是 1:n);
  • 用户->用户连接状态:考虑到用户能够多端登录,那么 app/web 都会有对应的在线状态信息(1:n);
  • 用户->联系人关系:考虑到用户最终以某种业务联系到一起,组成多份联系人关系,最终形成私聊或者群聊(1:n);
  • 联系人关系
  • 业务决定用户与用户之间的关系:比如说,某个家庭下有多少人,这个家庭群聊就有多少人;在 ToB 场景,在钉钉企业版里,我们往往有企业群聊这个存在;
  • 消息
  • 消息->消息队列:考虑到读扩散,消息最终归属于一个或多个消息队列里,因此群聊场景它会分布在不同的消息队列里;
  • 消息队列
  • 消息队列:确切说是消息引用队列,它里面的索引元素最终指向具体的消息实体对象
  • 用户连接状态
  • 用户连接状态:

- 对于 app 端:网络原因导致断线,或者用户手动 kill 掉应用进程,都属于离线

- 对于 web 端:网络原因导致浏览器断网,或者用户手动关闭标签页,都属于离线

- 对于公众号:无法分别离线在线

- 对于小程序:无法分别离线在线

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

消息存储

我们对于消息存储方案其实有两种方案,下面分别解析这两个方案的优点与弊端:

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

方案一:redis

  • 前提
  • 用户 &联系人关系,由于是业务数据,因此统一默认使用关系型数据库存储
  • 流程图
  • (1)用户发消息
  • (2)redis 创建一条实体数据 &一个实体数据计时器
  • (3)redis 在 B 用户的用户队列 添加实体数据引用
  • (4)B 用户拉取消息(后续 5.2 会提及拉模式)
  • 解决方案
  • 用户队列,zset(score 确保有序性)
  • 消息实体列表,hash(msg_id 确保唯一性)
  • 消息实体计数器,hash(支持群聊消息的引用次数,倒计时到零时则删除实体列表的对应消息,以节省资源)
  • 优点
  • 1、内存操作,响应性能好
  • 弊端
  • 1、内存消耗巨大,eg,阿里云 20G 内存,百万业务量下,每 2~3 个月就消耗了 50%资源,需要手动清理数据
  • 2、受 redis 容灾性策略影响较大,如果 redis 宕机,直接导致数据丢失(可以使用 redis 的集群部署/哨兵机制/主从复制等手段解决)

方案二:redis+mysql

  • 前提
  • 用户 &联系人关系,由于是业务数据,因此统一默认使用关系型数据库存储
  • 流程图
  • (1)用户发消息
  • (2)mysql 创建一条实体数据
  • (3)redis 在 B 用户的用户队列 添加实体数据引用
  • (4)B 用户拉取消息(5.2 会提及拉模式)
  • 解决方案
  • 用户队列,zset(score 确保有序性)
  • 消息实体列表,转移到 mysql(表主键 id 确保唯一性)
  • 消息实体计数器,hash(删除这个概念,因为磁盘可用总资源远远高于内存总资源,哪怕一直存放 mysql 数据库,在业务量百万级别时也不会有大问题,如果是巨大体量业务就需要考虑分表分库处理检索数据的性能了)
  • 优点
  • 1、抽离了数据量最大的消息实体,大大节省了内存资源
  • 2、磁盘资源易于拓展 ,便宜实用
  • 弊端
  • 1、磁盘读取操作,响应性能较差(从产品设计的角度出发,你维护的这套 IM 系统究竟是强 IM 还是弱 IM)

P2 消息消费模式

拉模式

选用消息拉模式的原因

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

ack 机制

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

实现:客户端在接收到消息之后,发送 ACK 消息编号给服务端,告知已经收到该消息。服务端在收到 ACK 消息编号的时候,标记该消息已经发送成功;

弊端:这种方案,因为客户端逐条 ACK 消息编号,所以会导致客户端和服务端交互次数过多。当然,客户端可以异步批量 ACK 多条消息,从而减少次数。

  • 基于滑动窗口 ACK

实现:

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

  • 如果比本地的小,说明该消息已经收到,忽略不处理;
  • 如果比本地的大,使用本地的消息编号,向服务端拉取大于本地的消息编号的消息列表,即增量消息列表。
  • 拉取完成后,更新消息列表中最大的消息编号为新的本地的消息编号;

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

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

ack机制的必要性

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

(1)服务器删除已读消息数据

(2)服务端把数据包响应给客户端

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

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

流程图

P3 消息实时通信

spring-messaging 模块

Spring 框架 4.0 引入了一个新模块 —— spring-messaging 模块,它包含了很多来自于 Spring Integration 项目中的概念抽象,比如:Message 消息、消息频道 MessageChannel、消息句柄 MessageHandler 等。

此模块还包括了一套注释,可以把消息映射到方法上,与 Spring MVC 基于注释的编程模型相似。

Spring 框架提供了对使用 STOMP 子协议的支持。STOMP,Streaming Text Orientated Message Protocol,流文本定向消息协议。

STOMP 是一个简单的消息传递协议, 是一种为 MOM(Message Oriented Middleware,面向消息的中间件)设计的简单文本协议。

  • maven 依赖
代码语言:javascript
复制
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
  • 数据通信协议 STOMP

STOMP 协议与 HTTP 协议很相似,它基于 TCP 协议,使用了以下命令:

  • CONNECT
  • SEND
  • SUBSCRIBE
  • UNSUBSCRIBE
  • BEGIN
  • COMMIT
  • ABORT
  • ACK
  • NACK
  • DISCONNECT

STOMP 的客户端和服务器之间的通信是通过“帧”(Frame)实现的,每个帧由多“行”(Line)组成:通过 MESSAGE 帧、RECEIPT 帧或 ERROR 帧实现,它们的格式相似。

代码语言:javascript
复制
第一行包含了命令,然后紧跟键值对形式的 Header 内容。
第二行必须是空行。
第三行开始就是 Body 内容,末尾都以空字符结尾。

长连接机制

连接建立

  • nginx 配置:设置 http 可以升级为 websocket 协议;
  • http 三次握手:客户端 &服务端双方确保发送和接受能力正常;
  • 升级 websocket:客户端以登录令牌“token”标识用户连接;
  • 服务端内存将“token”与长连接会话“Session”缓存到一个 ConcurrentHashMap,这样便能以 O(n)的效率检索到指定用户的长连接并发送通知包;

双工通信协议

  • 客户端保活机制:客户端发送“ping”包,服务端接受到,返回“pong”包,这是最基础的保活手段;(保活机制放在客户端,减轻服务端压力,同时节省服务端资源)
  • 新消息通知协议:前后端约定使用固定的通知协议做为通知信号(eg,“msg.route.new”),确保数据量小,宽带消耗低;

服务端剔除无效连接

  • 使用定时调度任务:轮训缓存好的 ConcurrentHashMap,检索每个长连接会话是否超时,超时则关闭以节省资源;

P4 微服务设计

微服务划分

微服务主要拆分为三个:用户 &消息业务 &消息连接管理。

  • 参考架构图
  • IM 消息系统包括了三个微服务:用户微服务、消息连接管理微服务和消息业务微服务,他们分工合作如下:
  • 用户微服务
  • (1)用户设备的登录 &登出:设备号存库,连接状态更新,其他登录端用户踢出等;
  • 消息连接管理微服务
  • (1)状态保存:保存用户设备长连接对象
  • (2)剔除无效连接:轮训已有长连接对象状态,超时删除对象
  • (3)接受客户端的心跳包:刷新长连接对象的状态
  • 消息业务微服务
  • (1)消息存储:参考 5.1-消息存储模型,进行私聊/群聊的消息存储策略
  • (2)消息消费:参考 5.2-消息消费模式,进行消息获取响应与 ack 确认删除
  • (3)消息路由:用户在线时,路由消息通知包到“消息连接管理微服务”,以通知用户客户端来取消息;

消息路由

相信看完“ 5.4.1 微服务划分”,了解到微服务之间也有通信手段:消息业务服 -> 消息连接管理服,两者之间可以通过 websocket 实现主动或被动的双工通信,以支持实时消息的路由通知。

P5 离线消息方案

离线推送方案上,我们考虑了两种方案:

1)自研后台离线 PUSH 系统

2)对接第三方手机厂商 PUSH 系统

自研后台离线 PUSH 系统

原理

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

弊端

  • 随着安卓和苹果系统的限制越来越严格,一般客户端的活动周期被限制的死死的,一旦客户端进程被挪到后台就立马被 kill 掉了,导致客户端保活特别难做好。

第三方厂商 PUSH 系统

原理

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

弊端

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

推送厂商分类

  • ios 推送
  • android 推送(华为/小米/OPPO/魅族/个推等)

P6 总结

1. 安全性

  • 传输安全性使用 https 访问;使用私有协议,不容易解析;
  • 内容安全性端到端加密,中间任何环节都不能解密;即发送和接收端交换互相的密钥来解密,服务器端解密不了;服务器端不存储消息;

2. 一致性

  • 消息一致性:保证消息不乱序;
  • 消息唯一 id:有多种方式,如由统一的 MySQL/Redis 统一生成、或由 snowflake 算法生成等,此时若要支持高并发,则要考虑该生成器对高并发的支持情况;

3. 可靠性

  • 上述方案用到了 ack 机制,同时消息创建过程尽量确保操作原子性,并且封装为一个事务(虽然分开 mysql&redis 存储让分布式事务变得较高难度)。

4. 实时性

  • 通用方案都是采用 websocket,但是某些低版本的浏览器可能不支持 websocket,所以实际开发时,要兼容前端所能提供的能力进行方案设计。

实现方案

工作日常

在前公司的工作中,有两年多的时间都在维护迭代公司的 IM 消息系统:

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

可优化点

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

对比方案

跟大厂的IM系统设计方案的比较,在“分层架构”、“群聊技术方案”、“离线方案”这三个方面,我们选取了:网易云IM架构、网易云信云视频系统和微信聊天系统进行比较。

由于篇幅所限,这部分内容可以参考原文的第七节:https://xie.infoq.cn/article/4061081a5ce66137a8c021994。

成果展示

由于篇幅所限,这部分内容可以参考原文的第八节:https://xie.infoq.cn/article/4061081a5ce66137a8c021994。

参考文献

总结

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

多说一句,在日常开发里,我们同学们也要学会参考业界的解决方案,思考如何维护整套系统的高可用,思考如何解决大流量背景下的存储优化等关键问题。

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-04-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后台技术汇 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档