基于Tars高并发IM系统的设计与实现--进阶篇2

news2025/1/13 15:52:44

基于Tars高并发IM系统的设计与实现–进阶篇2

消息时序

分为时间和序号

时间

分布式系统中,消息的时间一般都取服务端本地时间戳,一般IM系统服务主机不止一台,每台机器上时间可能会有差异,系统处理也会有延时,时间也许小范围(毫秒级)错乱;
比如两条消息序号seqId增长,时间戳大小可能正好相反;这种情况一般不影响用户体验,用户端显示消息时也不是每条消息都显示时间,一般显示到分钟;

序号

消息序号有两种使用方法:会话独立序号和全局统一序号;

  • 会话独立序号
    每个会话独立计算序号,任意两个会话中的消息序号可能会重复或者顺序颠倒;

    • 优点:
      不同会话可以选择不同序列号生成器,并发性好
    • 缺点:
      高优先级消息来自不同会话,用消息序列号会产生问题,需要单独再产生一个冗余序号来重新排序;消息同步需要区分会话;
  • 全局统一序号
    全局一个序号生成器,不同会话,统一排序;

    • 优点:
      任意会话中的消息序号不会重复或者顺序颠倒;高优先级,历史消息获取游标记录相对容易,只记录一个目前最新消息seqId即可,不需要记录每个会话的最新seqId;

    • 缺点:
      对并发要求比较高

如何产生全局唯一递增序号

对于IM系统来说,如何做到IM聊天消息离线差异拉取(差异拉取是为了节省流量)、消息多端同步、消息顺序保证等,是典型的IM技术难点;
主要满足以下几点:

  • 序列号递增
  • 保证唯一,不冲突
  • 服务重启不倒退
  • 快速生成

关于序号seqId我同事王志永写过一篇文章详细介绍,在此引用。

SeqId的前世今生,如何保证消息顺序到达?

对于聊天、直播互动等业务来说,消息的时序代表的是发送方的意见表述和接收方的语义逻辑理解,如果时序一致性不能保证,可能就会造成聊天语义不连贯、容易出现曲解和误会。

实际上保证消息绝对有序在IM中是一个非常困难的问题,可以说每个大厂都为了解决这个问题较劲脑汁。
但是为什么保证消息时序一致性如此困难呢?

从理论上来说,保持消息的时序一致性貌似并不难。
理论上,我们想象的消息收发场景中,只有单一的发送方、单一的接收方。
如果发送方和接收方的消息收发都是单线程操作,并且和 IM 服务端都只有唯一的一个 TCP 连接,来进行消息传输,IM 服务端也只有一个线程来处理消息接收和消息推送。这种场景下,消息的时序一致性是比较容易能得到保障的。但在实际的后端工程实现上,由于单发送方、单接收方、单处理线程的模型吞吐量和效率都太低,基本上不太可能存在。

更多的场景下,我们可能需要面对的是多发送方、多接收方、服务端多线程并发处理的情况。所以,知道了难点,我们再来看一看究竟在后端的工程实现上,保证消息的时序一致都存在哪些难点。

消息的时序一致性其实是要求我们的消息具备“时序可比较性”,也就是消息相对某一个共同的“时序基准”可以来进行比较,所以,要保证消息的时序一致性的一个关键问题是:
我们是否能找到这么一个时序基准,使得我们的消息具备“时序可比较性”,也就是说所有的消息都依据一个统一的基准时序,即可满足时序的比较。本文我们统称这个基准时序为SeqId。

如何找到一个合理的SeqId?

首先,我们来看看发送方的本地序号或者本地时钟是否可以作为SeqId?

所谓发送方的本地序号或者本地时钟是指发送方在发送消息时携带一个本地的时间戳或者本地维护的一个序号给到 IM 服务端,IM 服务端再把这个时间戳或者序号和消息一起发送给消息接收方,消息接收方根据这个时间戳或者序号来进行消息的排序。显然这种方式是行不通的,原因有下面几点。

发送方时钟存在较大不确定因素,用户可以随时调整时钟导致序号错乱。

发送方本地序号可能会由于APP卸载或者其他因素导致清0,从而导致序号错乱。

类似“群聊消息”和“单用户的多端登录”这种多发送方场景,都存在:同一时钟的某一时间点,都可能有多条消息发给同一接收对象。比如同一个群里,多个人同时发言;或者同一个用户登录多台设备,两台设备同时给某一接收方发消息。多设备间由于存在时钟不同步的问题,并不能保证设备带上来的时间是准确的。

因此以发送方的本地时钟或者本地序号作为SeqId是不可靠的。

其次, IM 服务器的本地时钟是否可以作为SeqId呢?

IM 服务器的本地时钟作为SeqId是指:发送方把消息提交给 IM 服务器后,IM 服务器依据自身服务器的时钟生成一个时间戳,再把消息推送给接收方时携带这个时间戳,接收方依据这个时间戳来进行消息的排序。但是似乎IM 服务器的本地时钟作为接收方消息排序的SeqId 也不太合适。因为,在实际应用中,IM 服务都是集群化部署,集群化部署也就是许多服务器同时部署任务。每一台Server的本地时钟都有可能出现不一致。虽然多台Server通过 NTP 时间同步服务,能降低服务集群机器间的时钟差异到毫秒级别,但仍然还是存在一定的时钟误差,而且 IM 服务器规模相对比较大,时钟的统一性维护上也比较有挑战,整体时钟很难保持极低误差。

最后, 是否可以有一个全局的序号生成器统一颁发SeqId 呢?

经过以上分析,显然单机本地化的时钟或者序号都存在问题,那么如果有一个全局的时钟或者序号是不是就能解决这个问题了呢?比如说如果有一个全局递增的序号生成器,应该就能避免多服务器时钟不同步的问题,IM 服务端就能通过这个序号生成器发出的序号,来作为消息排序的SeqId。而且这种“全局序号生成器”可以通过多种方式来实现,常见的比如 Redis 的原子自增命令 incr,DB 自带的自增 id,或者类似 Twitter 的 snowflake 算法、“时间相关”的分布式序号生成服务等。

但是这里显然还是有一个很明显的挑战,如果由全局的序号生成器来颁发一个全局的SeqId, 这个全局的序号生成器会不会成为一个服务瓶颈? 在一个及其复杂的高并发,高可用分布式环境中, 处理任何一条消息都需要请求一台全局的序号生成器,那么这个全局序号生成器的压力可想而知,就以微信的消息量为例,简直就是一个噩梦。

类似 Redis 的原子自增和 DB 的自增 id,都要求在主库上来执行“取号”操作,而主库基本都是单点部署,在可用性上的保障会相对较差,另外,针对高并发的取号操作这个单点的主库可能容易出现性能瓶颈。

而采用类似 snowflake 算法的时间相关的分布式“序号生成器”,虽然在发号性能上一般问题不大,但也存在一些问题。一个是发出的号携带的时间精度有限,一般能到秒级或者毫秒级,比如微博的 ID 生成器就是精确到秒级的,另外由于这种服务大多都是集群化部署,携带的时间采用的服务器时间,也存在时钟不一致的问题(虽然时钟同步上比控制大量的 IM 服务器也相对容易一些)。

终极方案,全局序号生成器+局部唯一序号

从业务层面考虑,对于群聊和多端登录这种场景,没有必要保证全局的跨多个群的绝对时序性,只需要保证某一个群的消息有序即可。这样的话,如果可以针对每一个群有独立一个“ID 生成器”,能通过哈希规则把压力分散到多个主库实例上,大量降低多群共用一个“ID 生成器”的并发压力。

对于大部分即时消息业务来说,可以采用分布式全局序号生成器策略。从产品的角度,可以接受消息时序上存在一定的细微误差,比如同一秒收到同一个群的多条消息,业务上是可以接受这一秒的多条消息。实际上,这种细微误差对于用户来说,基本也是无感知的。

大厂如何管理SeqId?

  • 微信
    微信的聊天和朋友圈的消息时序也是通过一个“递增”的版本号服务来进行实现的。不过这个版本号是每个用户独立空间的,保证递增,不保证连续。

  • 微博
    微博的消息箱则是依赖“全局分布式的时间相关的 ID 生成器”来对私信、群聊等业务进行排序,目前的精度能保证秒间有序。

  • Twitter
    Twitter 采用自己的snowflake算法来实现全局ID生成。核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。

  • Instagram
    先了解一下instagram 的存储策略,采用分片储存( logic Shard ),一个Table可以为多个逻辑分片,而每一个分片都一个UID。
    全局ID生成组成:
    41 bits: Timestamp (毫秒)
    13 bits: 每个 logic Shard 的代号 (最大支持 8 x 1024 个 logic Shards)
    10 bits: sequence number; 每个 Shard 每毫秒最多可以生成 1024 个 ID
    生成 unique ID 时, 41 bits 的 Timestamp 和 Snowflake 类似;此处不做详细介绍。

根据目前各个大厂分享的知识,关于seqid的生成策略基本采用类似的方法,比如:facebook,youtube等。

消息服务端包内整流

虽然大部分情况下,聊天、直播互动等即时消息业务能接受“小误差的消息乱序”,但某些特定场景下,可能需要 IM 服务能保证绝对的时序。比如发送方的某一个行为同时触发了多条消息,而且这多条消息在业务层面需要严格按照触发的时序来投递。

一个例子:用户 A 给用户 B 发送最后一条分手消息同时勾上了“取关对方”的选项,这个时候可能会同时产生“发消息”和“取关”两条消息,如果服务端处理时,把“取关”这条信令消息先做了处理,就可能导致那条“发出的消息”由于“取关”了,发送失败的情况。对于这种情况,我们一般可以调整实现方式,在发送方对多个请求进行业务层合并,多条消息合并成一条;也可以让发送方通过单发送线程和单 TCP 连接能保证两条消息有序到达。

但即使 IM 服务端接收时有序,由于多线程处理的原因,真正处理或者下推时还是可能出现时序错乱的问题,解决这种“需要保证多条消息绝对有序性”可以通过 IM 服务端包内整流来实现。

比如:我们在实现离线推送时,在网关机启动后会自动订阅一个本 IP 的 Topic,当用户上线时,网关机会告知业务层用户有上线操作,这时业务层会把这个用户的多条离线消息 pub 给这个用户连接的那个网关机订阅的 Topic,当网关机收到这些消息后,再通过长连接推送给用户,整个过程大概是下图这样的。
在这里插入图片描述

图片
但是很多时候会出现 Redis 队列组件的 Sharding 和网关机多线程消费处理导致乱序的情况,这样,如果一些信令(比如删除所有会话)的操作被乱序推送给客户端,可能就会造成端上的逻辑错误。

然后再说一下离线推送服务端整流的过程:
在这里插入图片描述

图片
首先,生产者为每个消息包生成一个 packageID,为包内的每条消息加个有序自增的 seqId。

其次,消费者根据每条消息的 packageID 和 seqID 进行整流,最终执行模块只有在一定超时时间内完整有序地收到所有消息才执行最终操作,否则将根据业务需要触发重试或者直接放弃操作。通过服务端整流,服务端包内整流大概就是图中这个样子,我们要做的是在最终服务器取到 TCP 连接后下推的时候,根据包的 ID,对一定时间内的消息做一个整流和排序,这样即使服务端处理多条消息时出现乱序,仍然可以在最终推送给客户端时整流为有序的。

消息接收端整流

携带不同序号的消息到达接收端后,可能会出现“先产生的消息后到”“后产生的消息先到”等问题,消息接收端的整流就是解决这样的一个问题的。

消息客户端本地整流的方式可以根据具体业务的特点来实现,目前业界比较常见的实现方式比较简单,步骤如下:

  • 接收方收到消息后进行判定,如果当前消息序号大于前一条消息的序号就将当前消息追加在会话里;

  • 否则继续往前查找倒数第二条、第三条等,一直查找到恰好小于当前推送消息的那条消息,然后插入在其后展示。

(原文链接:https://mp.weixin.qq.com/s/Ec1JW0r4QLQ-GmIulf7I7g)

全局唯一序号产生算法参考

  • 《微信序列号生成算法》https://blog.csdn.net/weixin_34153893/article/details/89612114

  • Leaf——美团点评分布式ID生成系统(推荐阅读,它给出了一些更加优秀的解决方案)

  • 分布式系统唯一ID生成方案汇总(推荐阅读,借鉴出处)

  • Snowflake–Twitter的分布式自增ID算法snowflake (Java版)(推荐阅读,Snowflake Code Provenance Information)

  • Chronos–XiaoMi/chronos(Chronos,是实现高可用、高性能、提供全局唯一而且严格单调递增timestamp的服务。)

  • 分布式唯一id:snowflake算法思考(对于snowflake时间回拨的解决方案)

超大群

定义

超过10000人的群都可以视为超大群;超大群消息量巨大,根据消息优先级进行处理,普通消息允许丢失;
正常群聊也就几十人到几百人(微信限制最大500人)
群聊中的消息要进行及时扩散,前文中描述的群聊消息转发模式在正常群聊情况下没什么问题;
如果超大群也采用这种模式,系统的性能和消息的延时将会出现一些问题;

例子对比

100人的普通群,每秒1个人发消息;1分钟产生60条消息,每条消息产生100次消息转发;一分钟将产生6000次转发
10000人的超大群,每秒有5人发消息;一分钟讲产生300条消息,每条消息产生10000次消息转发,一分钟将产生300万次转发;

通过以上比对,超大群的消息转发量比普通群多几个数量级;
所以如果通过普通群的模式来处理超大群一定会出现问题,拖垮整个IM服务;

设计原则

超大群的处理方案与开放式聊天室一致,大致如下原则:

  • 放弃消息转发的推送模式,转而采用客户端主动拉消息模式;
  • 服务端根据客户的的拉请求,拉取增量消息;服务端减少大量消息的转发操作;
  • 客户端的消息拉取采用长轮训模式,既能达到及时,又能减少请求次数;
  • 超大群为一种新的形态,独立服务处理,不宜跟普通群放在一起处理;

超大群技术设计

消息框架选型:

  • 长连接+读扩散
  • Tars微服务治理体系

实现准则

  • 客户端与服务端保持一个长连接
  • 客户端定时向服务器发出拉消息请求,获取最新消息;
  • 连接断掉重连后拉取最新消息;
  • 聊天室资料和人数由客户端定时调用接口刷新;
  • 高优先级通道,高优先级消息由专用通道(优先发送,优先拉取),保证第一时间能够通知到聊天室房间;
  • 未解散的聊天室数据保持在redis,同时保存在mysql中,解散后,redis数据清除,mysql中保存;
  • 为了方便消息展示,消息中冗余发送者信息(名称,头像)
  • 读消息服务缓存最新消息数据,定时从redis中同步最新一定消息条数;
  • 所有服务均无状态,可以随时横向扩容;

架构
在这里插入图片描述

  • 接入服务Brokerserver
    所有用户的接入,长连接保持,负责客户端请求的处理和派发,该服务主要有如下功能:
  1. 保持客户端长连接
  2. 解析客户端请求并通过RPC派发到聊天室内部相应服务中
  • 聊天室服务RoomServer
    主要有如下功能:
  1. 用户认证
  2. 聊天室创建,资料修改
  3. 人员进入,退出等聊天室操作处理
  4. 产生系统消息“产生xxx进入聊天室消息
  • 消息保存服务MsgWriteServer
    主要有如下功能:
  1. 解析消息
  2. 消息存储到redis
  3. 根据优先级处理消息
  4. 将消息异步RPC给OLapserver
  • 消息读取服务MsgReadServer
    主要有如下功能:
  1. 负责从内存中根据客户端请求查询并返回数据;
  2. 定时从redis集群中同步最新消息到消息缓存中;
  • 数据统计冷存储服务OlapServer
    主要有如下功能:
  1. 消息数据,聊天室数据mysql存储;
  2. 统计数据的处理;
  3. 针对http服务,统计数据的请求处理;
  • 建聊天室,发送消息,获取聊天室数据时序图

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/722096.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

neo4j删除Property Keys值方法

首先,停止neo4j服务 然后删除安装目录下面databases下面所有文件 重新运行neo4j,发现Property Keys值已经完全清干净了

Docker如何安装Nacos

🍓 简介:java系列技术分享(👉持续更新中…🔥) 🍓 初衷:一起学习、一起进步、坚持不懈 🍓 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正🙏 🍓 希望这篇文章对你有所帮助,欢…

邮件收发原理及部署postfix邮件系统

目录 一、邮件收发原理 1、原理图及名词解释 2、MTA功能介绍 3、POP和IMAP获取邮件介绍 二、部署postfix邮件系统 1、环境准备 2、DNS服务器部署 3、部署Postfix 4、部署Dovecot 三、使用Foxmail测试 1、修改DNS服务器 2、Foxmail登录测试账户 3、发送测试邮件 …

热力图问题

1.python画图怎么使用特殊符号 python画图怎么使用特殊符号?_python中怎么在绘图中加a b c d_sinysama的博客-CSDN博客python画图怎么使用特殊符号_python中怎么在绘图中加a b c dhttps://blog.csdn.net/QAQIknow/article/details/124390075?ops_request_misc%257…

OPPO手机便签怎么设置字体颜色?便签调整字体颜色方法

OPPO是一个非常受年轻人青睐的手机品牌,它的手机不仅外观设计时尚轻薄,而且拍照清晰、系统流畅,并且拥有高中低不同档次的价位可供消费者选择。虽然OPPO手机的使用体验非常不错,但是有一部分用户也遇到了一些问题,例如…

B. Cake Assembly Line - 思维

分析&#xff1a; 推公式&#xff0c;需要每一块蛋糕的范围完全覆盖巧克力范围&#xff0c;假设蛋糕一共移动了距离d&#xff0c;那么则 对于每一个i都有a[i]-wd<b[i]-h<b[i]h<a[i]wd&#xff0c;解得b[i]h-a[i]-w<b[i]-h-a[i]w。只需要判断不等式是否成立就可以求…

平台使用篇 | RflySim飞控底层实验平台配置介绍

本课程提供的实验平台总体可以分成两个部分&#xff1a;硬件平台和软件平台。本讲简要介绍各个部分的基本组成及实验开发流程。 平台使用篇-RflySim飞控底层实验平台配置介绍 01 电脑配置 1.1推荐配置 •系统&#xff1a;Windows 10 x64系统&#xff08;版本大于等于1809&…

IIC通信原理(软件实现)-GD32

IIC通信原理-GD32 硬件连接 数据变换规则 起始信号和结束信号 应答信号 数据帧格式 #include "my_i2c_soft.h" #include "systick.h"void my_i2c_w_SDA(uint8_t bit_value) {gpio_bit_write(I2C_SOFT_PORT, I2C_SOFT_SDA_PIN, (bit_status)bit_val…

【软件下载】音频ASIO驱动下载

一&#xff0c;简介 在高速USB Audio使用中&#xff0c;需要再windows电脑上安装ASIO驱动&#xff0c;用来进行高速音频流的传输&#xff0c;本文主要介绍如何下载安装ASIO驱动。供参考。 二&#xff0c;安装步骤 2.1 软件下载 下载地址&#xff1a;http://www.asio4all.co…

【Python爬虫与数据分析】UDP/TCP通信协议

目录 一、网络编程基础 二、UDP协议 三、TCP协议 一、网络编程基础 数据编码与解码 str -> bytes&#xff1a;encode编码&#xff0c;发送信息的时候用encode编码bytes -> str&#xff1a;decode解码&#xff0c;打印接收的信息用decode解码 test 你好世界en_code…

C语言switch语句与循环结构

1、循环输入5个元素&#xff0c;输出最大值 #include <stdio.h> #include <string.h> int main(int argc, const char *argv[]) {int num,max;for(int i1;i<5;i){printf("请输入第%d个数&#xff1a;",i);scanf("%d",&num);if(i1)maxn…

CSS 两行文字两端对齐与字符间距的处理

前言 &#x1f44f;CSS 文字对齐与字符间距的处理&#xff0c;在这里&#xff0c;你可以了解到文字渐变&#xff0c;letter-spacing&#xff0c;text-align&#xff0c;text-align-last&#xff0c;filter等&#xff0c;速速来Get吧~ &#x1f947;文末分享源代码。记得点赞关…

LabVIEW脉冲检测实现

文章目录 前言一、脉冲检测的原理二、代码实现1、前面板①、输入波形相关参数②、脉冲检测结果 2、程序框图 三、代码自取结论 前言 本节通过 labview 软件实现先导脉冲检测的功能&#xff0c;从而获取先导脉冲的频率、先导脉冲与线性调频信号的延时的相关信息。 一、脉冲检测…

多实例部署和Nginx+Tomcat负载均衡、动静分离

目录 一、Tomcat多实例部署 1.安装好 jdk 2.安装 tomcat 3.配置 tomcat环境变量 4.修改 tomcat2 中的 server.xml文件&#xff0c;要求各 tomcat 实例配置不能有重复的端口号 5.修改各 tomcat 实例中的 startup.sh 和 shutdown.sh 文件&#xff0c;添加 tomcat 环境变量 6.启…

Vue和React的区别?

目录 共同点 1. 数据驱动视图 2. 组件化 3. Virtual DOM 不同点 1. 核心思想不同 2. 组件写法差异 3. diff算法不同 4. 响应式原理不同 5. 其他不同点 首先找到 Vue 和 React 的共性&#xff0c;它们被用于解决什么问题&#xff0c; 然后再挖掘各自独特的个性、设计原…

【计算机视觉 | 目标检测】arxiv 计算机视觉关于目标检测的学术速递(6月 30 日论文合集)

文章目录 一、检测相关(8篇)1.1 Detect Any Deepfakes: Segment Anything Meets Face Forgery Detection and Localization1.2 Sustainable Palm Tree Farming: Leveraging IoT and Multi-Modal Data for Early Detection and Mapping of Red Palm Weevil1.3 Evaluation of Env…

【DP+博弈】ABC195 E

E - Lucky 7 Battle (atcoder.jp) 题意&#xff1a; 思路&#xff1a; Code&#xff1a; #include <bits/stdc.h>//#define int long longusing namespace std;const int mxn2e510; const int mxe1e610; const int mod1e97; const int Inf0x3f3f3f3f;string s,x;int N;…

【设计模式】第十九章:访问者模式详解及应用案例

系列文章 【设计模式】七大设计原则 【设计模式】第一章&#xff1a;单例模式 【设计模式】第二章&#xff1a;工厂模式 【设计模式】第三章&#xff1a;建造者模式 【设计模式】第四章&#xff1a;原型模式 【设计模式】第五章&#xff1a;适配器模式 【设计模式】第六章&…

VI-SLAM系统对比

任何类型的相机对运动物体&#xff0c;视线遮挡&#xff0c;光照剧烈变化等恶劣情况下都不够鲁棒&#xff0c;而惯性测量单元&#xff08;IMU&#xff09;对环境基本没有要求&#xff0c;并且IMU可以提供高频的位姿估计&#xff0c;视觉一般只能提供低频的位姿。依据视觉与IMU的…

前后端实现导出导入功能

目录 导出 1.后端代码 &#xff08;1&#xff09;相关依赖 &#xff08;2&#xff09;自定义实体类 &#xff08;3&#xff09;写一个查询方法list &#xff08;4&#xff09;写导出接口 2.前端代码 3.效果示例 导入 1.后端代码 &#xff08;1&#xff09;写导入接口 …