消息队列

为什么

主要作用:

  • 异步:提高系统响应速度以及吞吐量
  • 削峰填谷:稳定平滑系统流量
  • 解耦:减少服务之间的影响,提高系统整体的稳定性以及扩展性

主要缺点

MQ 是基于事件驱动的。消息由生产者发送到MQ进行排队,然后按FIFO的顺序交由消息的消费者进行处理。其主要缺点:

  • 系统可用性降低:外部依赖增加,稳定性变差,要考虑MQ的高可用
  • 系统复杂度提高:消息链路追踪复杂,顺序也要保证
  • 消息一致性问题

怎么用(生产环境关注公有云服务)

公有云产品

官网消息队列全景:https://www.aliyun.com/product/ons?spm=5176.23056729.J_3207526240.58.3dcc3f06fc3SNi

1668406011013-f56382bd-62f6-439b-bdd5-23a51f064742.png

  • MQTT 是给车联网使用, 适合消息比较小且多的跨端消息传输
  • rabbit 主要是为了原有使用 rabbit 的项目快速迁移,以及增强了 rabbitMQ 的稳定性,降低了运维的成本
  • kafka 主要用来给大数据日志分析使用,
  • eventBridge 主要用来给 SaaS 平台提供事件总线的能力,实现不同系统之间的解耦以及数据的事件通知机制。如果平台和 EventBridge 有集成,例如钉钉有数据变动的时候可以直接通过EventBridge投递到数据库的变更中https://help.aliyun.com/document_detail/434630.html
  • MNS:提供的功能其实也没有那么简陋,相比 rocketMQ 概念确实更少,但是确实没什么需要使用的场景。或许在一些函数计算需要简单使用MQ的场景有用武之地。
  • RocketMQ 这个懂得都懂,做一些业务相关的应用服务,是提供最完善和稳定支持的MQ,所以一般的应用开发MQ场景,无脑使用 RocketMQ

事件总线 EventBridge

事件驱动无服务计算:“事件”可以是云产品的事件,也可以是自定义事件源,“无服务计算”可以是Function、API、K8s Service。

使用场景:

  1. 无服务计算,新 serverless 函数计算等技术快速接入事件处理的能力
  2. 异构系统之间想要实现事件的互相消费的时候

无服务器事件总线服务,最主要是支持不同架构(自建应用服务,阿里云服务,SaaS 应用服务,中心化服务)之间建立关系,也就是支持建立松耦合的事件驱动架构。

使用的时候一般是低代码的配置方式继承或被集成,连接云产品和自己的应用或者SaaS应用。在 Serverless 里面可以支持更多的函数快速接入使用

1668407751549-c4856be2-4ac1-4d1b-8f26-50ac0f676d5b.png

  • 事件源:阿里云服务、SaaS 服务、自定义服务、自定义数据源
  • 事件总线:云服务专用总线、自定义总线
  • 事件规则:过滤器规则可以用来过滤符合要求的事件,转换器可以将CloudEvent转换为事件目标要求的数据格式
  • 事件目标:钉钉、函数计算、消息服务、事件总线、消息队列、短信服务、邮箱服务、HTTPS。每个事件最多可以触发5个事件目标

1668408116354-e20f5a2e-c11a-452a-ae64-5f426cec6c5a.png

微消息队列MQTT

1668408387968-ea01d266-91f9-41c6-9694-f7432f577101.png

云产品 RocketMQ

基本的概念和开源的没有什么区别,只是一些高级特性上有一些优化,比如商业版是可以实现消息只有一次到达的。这里研究原理使用开源版。

实际生产环境最好使用商业版本,是因为:

1668406186314-f05dd91a-2b22-495a-885a-2e0c1353557b.png

ons 最开始是为了屏蔽具体的 MQ的类型,让用户引入依赖的时候不需要关注具体的MQ类型,而是只有一个 ons-client ,但是商业版 RocketMQ 最新版的依赖已经替换为具体的rocketmq-client-java依赖。

开源中间件

RabbitMQ

简单介绍

企业内部用的最经典的消息队列。但是在生产上是有一些稳定性的问题和分布式的问题需要关注的。

基本概念

画板

  • virtual host 虚拟机:一个实际的物理实例上是可以创建多个虚拟机,这些虚拟机在外界看来就是一个个的服务实例。数据权限都是分开的。
  • 集群模式:
    • 普通集群模式:集群节点之间有相同元数据,但是消息只会在某一节点。会在请求过来的时候,如果当前节点没有数据,临时从有数据的机器拉取数据。有单点故障的问题(可用性以及重复消费)。
    • 镜像模式:官方推荐,普通集群模式的增强,消息会主动在集群节点之间同步,每个节点都存在全量消息(最好不要创建太多的队列,否则会降低集群性能)。使用的时候创建虚拟主机,然后添加对应的镜像策略。
  • 队列,创建时候的注意事项:
    • 虚拟机的选择:如果要高可用要选择创建了镜像策略的虚拟主机。
    • 队列的类型
      • 经典队列Classic:单机较高可靠性,可以设置是否持久化(Durable和Transient以及自动删除队列
        • 支持独占队列(只能由声明该队列的Connection连接来进行使用,包括队列创建、删除、收发消息等,并且独占队列会在声明该队列的Connection断开后自动删除。)
      • 仲裁队列Quorum(从3.8.0版本):分布式下更可靠。基于raft,持久化和多备份队列,需要多个节点过半统一才会写到队列。
        • 支持毒消息(一直不被消费)的处理,超过设置的阈值就会删除或者放到配置的死信队列。
        • 处理比较慢,可能会消息积压,不支持独占和临时队列。适合延时要求低,但不丢失的场景。
      • 流式队列Stream(自3.9.0版本):持久化到磁盘并且具备分布式备份的,更适合于消费者多,读消息非常频繁的场景。消息顺序写,调整每个消费者消费进度offset实现多次分发。
        • 一个队列就可以给多个消费者消费同样的消息,不用创建多个队列
        • 允许读取已经消费过的消息,重置 offset
        • 适合存储大量数据,适合高性能吞吐。
    • 消息的类型:会持久化到硬盘 Durable、不持久化 Transient
    • 是否自动删除:至少一个已经连接,并且所有 consumer 都不用之后队列就会被删除。
  • AMQP是一个二进制协议,提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。特点:多信道、协商式,异步,安全,扩平台,中立,高效。RabbitMQ是AMQP协议的Erlang的实现。
  • Connection 连接与 Channel 信道:一旦客户端与Server建立了 Connection(TCP连接,四元组只会有一个连接) 之后,就会分配一个 AMQP 的信道 Channel,进行实际数据的交互,为了减少性能开销,一个Connection 中会建立多个Channel,便于多线程连接,这些连接复用一个 Connection 的 TCP 通道。
  • 交换机Exchange:主要起到路由的作用,消息从 channel 进来之后,通过交换机路由到不同的队列。交换机常见类型有:direct、fanout、headers、topic

简单使用

  • 安装的时候需要先按照对应的 erlang 环境,再安装server
  • 使用方式:原生API、SpringBoot、SpringCloudStream 三种模式
  • 原生API的基本编程模型:具体代码可以参考:https://www.cnblogs.com/nongzihong/p/11927915.html
    • 创建连接、获取Channel
    • 声明队列:声明的队列,如果服务端没有,那么会自动创建。但是如果服务端有了这个队列,那么声明的队列属性必须和服务端的队列属性一致才行。
    • 发送消息:设置传输的规则
    • 消费消息:
      • 被动消费模式:建立一个线程等待 server 推送消息,
      • 主动消费模式:主动获取指定的 message 进行消费
      • autoAck 决定是不是自动确认消费。
    • 关闭连接释放资源
  • 消息场景:
    • hello world:一个队列,一个消费者,不需要交换机,直接指定queue来发送和消费
    • work queues 工作序列:一个队列,多个消费者,依旧没有交换机,负载均衡决定谁消费队列。
      • 关于自动确认:Consumer端的autoAck字段设置的是false,消费后不会自动反馈服务器已消费了message,处理完再调用channel.basicAck通知服务器已经消费了该message。没有ack的message会被服务器重新进行投递。
      • 关于负载均衡策略:可以是轮询,也可以先询问consumer的处理能力来决定是否调用。但是都可能导致server的消息积压
    • direct,fanout,topic等这些Exchange,都是以 routingkey为关键字来进行消息路由到不同的queue的,但是这些Exchange有一个普遍的局限就是 都是只支持一个字符串的形式
      • pub/sub 订阅发布机制:type为 fanout 的exchange。一个交换机多个队列,往所有的队列上发消息。注意这里的队列是逻辑上的,是按照名字维度来的,不同实例的消费者消费的队列名字一样就只有一台机器会处理。这些消费同名队列的消费者可以理解为消费者组。
      • routing 基于key内容的精确匹配路由:type为 direct 的exchange。一个交换机多个队列,交换机将不同路由精确匹配key的消息发到不同的queue。
      • topics 基于主题内容的模糊匹配路由:type为 topic 的exchange。这个是模糊匹配,词之间用.隔开,* 代表一个具体的单词。# 代表0个或多个单词。
    • headers 基于header的路由:Headers 类型的 Exchange 就是一种忽略 routingKey 的路由方式。他通过 Headers 来进行消息路由。发送者可以在发送的时候定义一些键值对,接受者也可以在绑定时定义自己的键值对。当键值对匹配时,对应的消费者就能接收到消息。
      • 匹配的方式有两种,一种是all,表示需要所有的键值对都满足才行。另一种是any,表示只要满足其中一个键值就可以了。而这个值,可以是List、Boolean等多个类型。
      • debug - info - warning - error四个级别的日志分开收集,收集时,每个队列对应一个日志级别,收集对应日志级别和以上的所有日志,就可以使用这个
    • rpc 远程调用:不常用
    • Publisher Confirms 可靠消息发送机制:新消息模型(阿里云MQTT微消息队列云端的SDK就是用的这个包装的),发送者消息确认,保证消息发给server是可靠的,前面都是保证consumer消费是可靠的。开启这个模式需要执行:channel.confirmSelect();
      • 发布单条消息channel.waitForConfirmsOrDie(5_000),同步阻塞channel,等待确认期间,不能再继续发送消息,明显降低集群的发送速度。
      • 异步确认消息channel.addConfirmListener(ConfirmCallback var1, ConfirmCallback var2);Producer在channel中注册监听器来对消息进行确认。
        • sequenceNumer:唯一的序列号,代表唯一的消息。在 RabbitMQ中,他的消息体只是一个二进制数组,并不像RocketMQ一样有一个封装的对象,所以默认消息是没有序列号的。而RabbitMQ提供了一个方法int sequenceNumber = channel.getNextPublishSeqNo());来生成一个全局递增的序列号。然后应用程序需要自己来将这个序列号与消息对应起来。
        • multiple:这个是一个Boolean型的参数。如果是true,就表示这一次只确认了 当前一条消息。如果是false,就表示RabbitMQ这一次确认了一批消息,在 sequenceNumber之前的所有消息都已经确认完成了。

RocketMQ

简单介绍

阿里2016开源后捐赠给Apache,在阿里云上有一个购买即可用的商业版本,商业版本集成了更深层次的功能及运维定制。

阿里最开始使用ActiveMQ,消息逐渐多之后,IO瓶颈。使用 kafka 但是 topic 多的时候就会 partition 也多,IO不行。自研的中间件叫 MetaQ,开源出来叫 RocketMQ。RocketMQ 将所有的消息使用顺序写的方式都追加到 commitLog 文件,每一个分片文件 consumerQueue 只维护索引信息,所以即使 topic 很多的时候也不会有IO问题。

基本概念

画板

  • NameServer 命名服务器 : 提供轻量级的Broker路由服务。broker 启动的时候就会注册,后续使用心跳保持。多个nameServer之间组成集群是相互独立的,完全没有信息的交换。
  • BrokerServer 代理服务器:物理概念。实际处理消息存储、转发等服务的核心组件。通常有两种架构模式:
    • 普通集群:master 负责读写,slave 负责消息冗余保存,响应部分读请求。消息同步方式分为同步同步和异步同步。不master 挂掉之后需要手动设置新的mater
    • Dledger高可用集群:4.5版本引入的,集群会随机选出一个作为master。完成master、slave节点之间的消息同步。master 挂之后会自动选一个新的master
  • Message 消息:物理概念。每条消息必须属于一个主题Topic。有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。并且Message上有一个为消息设置的标志,Tag标签。
    • 需要注意使用的时候不要把自带的 messageId 作为保证消息幂等的工具,最好自己设置业务上的ID,rocket 不保证这个ID在相同消息内容时候的唯一性。
  • Topic主题:逻辑概念,不保存实际消息。实际保存的时候会分片,叫做MessageQueue,对应kafka里面的partition,放在不同的broker。一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者可以订阅一个或者多个Topic消息。消费者组的消费者实例必须订阅完全相同的Topic。

简单使用

  • 要启动RocketMQ服务,需要先启动NameServer。注意修改JVM 内存大小
  • 参数调整、调优。从应用程序的堆内存大小,到后面的OS的参数定制。
  • 消费形式:
    • 拉取式消费:主动调用 Consumer 的拉消息方法从Broker服务器拉消息。一旦获取了批量消息,应用就会启动消费过程。
    • 推动式消费:Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。使用比较简单,推模式是拉模式封装出来的。具体的实现方式会在下面介绍
  • 消息模式:
    • 集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。
    • 广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。
  • 编程模型:
    • 发送者:创建生产者,指定生产者组名;指定nameServer地址;启动生产者;创建消息对象,指定其topic、tag以及消息体;发送消息;关闭生产者。
    • 消费者:创建消费者,指定消费者组名;指定nameServer地址;订阅topic和tag,设置回调函数用来处理消息,启动消费者。
  • 消息样例
    • 基本样例:同步发送、异步发送(要使用countDownLatch保证所有消息回调方法都执行完了再关闭生产者,需要在发的时候指定 callBack 的实现)、单向发送(producer.sendOneWay方式来发送消息,没有返回值没有回调
    • 顺序消息:局部有序,不是全局有序。需要自己实现一个QueueSelector,会发送到同一个分区 MessageQueue,消费的时候给consumer注入的 MessageListenerOrderly对象,内部就会通过锁队列的方式保证消息是一个一个队列来取的,取完再取下一个队列的消息
    • 广播消息:在集群状态(MessageModel.CLUSTERING)下,每一条消息只会被同一个消费者组中的一个实例消费到。而广播模式则是把消息发给了所有订阅了对应主题的消费者,而不管消费者是不是同一个消费者组。消费的进度维护在每一个消费者实例中。
    • 延迟消息:有不同的延迟级别,到达时间之后会被转移到真正的 topic 对应的队列去消费,否则只是在系统创建的延迟队列里面存放。
    • 批量(攒批)消息:多条合并为一条发送(List msgs 直接发送出去),减少网络IO,提升吞吐量,默认最大1M,最大支持4MB。超过之后会报错发送失败。
      • 展批消息:如果超出了4M,需要自己手写 Splitter 切割 List msgs ,每次next只返回少量subList去发送即可。保证调用send方法的时候入参是小于4M就好。
    • 过滤消息:consumer在subscribe的时候。除了指定topic,可以使用tag快速过滤消息,
      • 还支持SQL表达式的写法(tag之外的过滤条件可以发送时候给Message设置userProperty)
    • 事务消息:保证发送者本地事务和发消息两个操作之间的原子性,不保证消费者,消费者是否成功需要在处理完业务逻辑之后再返回成功,并且注意不要处理业务逻辑的时候新建线程。

kafka

简单介绍

分布式、支持分区(partition,也就是rocketMQ中的messageQueue的概念,但是这个partition是真正存放数据的而不是索引数据)、多副本(replica),基于zookeeper实现(新版本打算自己实现raft)的分布式消息系统,可以实时的处理大量数据。用scala语言编写, Linkedin于2010年贡献给了Apache。

基本概念

画板

  • Broker:一个kafka节点就是一个broker,一个或多个broker组成一个kafka集群。
  • Topic:逻辑上,根据 topic 对消息进行归类,发消息的时候是必须指定 topic 的
  • Partition:物理上,一个 topic 会被分为多个 partition,每个内部的消息都是有序的。为了应对海量数据场景而设计,同时提高了并行度。
    • replica:是分区的备份,也就是 leader 和 follower 是在分区的概念上区分出来的。
    • 消息日志 commit Log:每一个partition中的消息都是有唯一的编号 offset 的,同一 partition 的消息的编号一定不同,不同partition 中的消息的offset 可能相同。消息消费之后不会被删除,只会根据日志保留时间**log.retention.hours**确认多久删除,默认保留最近一周的日志,消息量大小和性能没有关系。但是分区的多少和性能有关系,因为分区越多就会创建越多的commitLog文件,磁盘随机读写消耗较大。
  • Consumer:每个consumer是基于自己在commit log中的消费进度(offset)来进行工作的。在kafka中,消费offset由consumer自己来维护;我们可以指定消费的 offset的位置。
  • ConsumerGroup:每一个 Consumer都属于一个ConsumerGroup,一条消息可以被多个不同的消费者组消费,但是一个消费者组只能有一个consumer消费该消息,和rocketMQ 一样,消费者组里面消费者的数量不要超出 topic 的分区的数量,否则会有消费者分不到分区。
  • 分布式协调器 zookeeper:将很多集群关键信息记录在zookeeper里,保证自己的无状态,从而在水平扩容时非常方便。

简单使用

Spring Cloud Stream 使用中的一些关键概念

详细参考:https://blog.csdn.net/qq_32734365/article/details/81413218

由于是将MQ的使用抽象了一层统一的编程框架,所以是有助于构建高扩展和事件驱动的微服务系统框架。替换其他MQ方便。

画板

Binder : 外部的消息服务器

  • SCStream是通过Binder来定义一个外部消息服务器。为构造Binding提供了 2 个方法,分别是bindConsumer和bindProducer,它们分别用于构造生产者和消费者。目前官方的实现有 Rabbit Binder 和 Kafka Binder, Spring Cloud Alibaba 内部已经实现了 RocketMQ Binder。
  • 并且支持配置多个Binder访问不同的外部消息服务器,通过**spring.cloud.stream.binders.[bindername].environment.[props]=[value]**的格式来进行配置服务器相关的IP端口和账号等信息。另外,如果配置了多个binder,也可以通过**spring.cloud.stream.default-binder**属性指定默认的 binder。
  • 对于RabbitMQ来说,Binder就是一个Exchange的抽象。默认情况下,RabbitMQ的binder使用了 SpringBoot的ConnectionFactory,支持spring-boot-starter-amqp 组件中提供的对RabbitMQ的所有配置信息。也就是application.properties里以spring.rabbitmq开头的配置。
spring.cloud.stream.binders.testbinder.environment.spring.rabbitmq.host=localhost 
spring.cloud.stream.binders.testbinder.environment.spring.rabbitmq.port=5672 
spring.cloud.stream.binders.testbinder.environment.spring.rabbitmq.username=guest 
spring.cloud.stream.binders.testbinder.environment.spring.rabbitmq.password=guest

Binding:消息交互桥梁

  • Binding 是连接应用程序跟消息中间件的桥梁,用于消息的消费和生产,由binder创建。通过Binding,即可以声明消息生产者,也可以声明消息消费者。
  • spring.cloud.stream.bindings.[bindingname].[props]=[value]
spring.cloud.stream.bindings.output.destination=scstreamExchange 
spring.cloud.stream.bindings.output.group=myoutput
spring.cloud.stream.bindings.output.binder=testbinder
  • @EnableBinding()注解接收一个或者多个接口类型的参数。
    • 接口的格式,内部有对应的输入和输出方法,方法的返回值是MessageChannel�接口及其子类,方法上有注解 @Input 标识了一个输入通道接收消息;@Output注解标识了一个输出通道发布消息。@Input和@Output注解接收一个参数,这个参数将作为通道的名称;如果没有定义名字,默认会使用被注解的方法名。
    • Spring Cloud Stream默认提供了Source、Sink和Process接口。Source可以用于一个单输出通道的应用中。Sink可以用于一个单输入通道的应用中。Processor可以用于既有输入又有输出通道应用中。对应的binding名字分别是 output、input、output和input。
  • 对于上面的 binding 接口,Spring Cloud Stream会生成一个实现接口的bean。我们可以使用对应bean调用对应的input()或者output()方法拿到对应的messageChannel,就可以发送对应的消息了。

Group:分组消费策略

消费者组的概念,在RabbitMQ中不存在,RabbitMQ是通过不同类型的Exchange来实现不同的消费策略。

spring.cloud.stream.bindings.consumer1.group=stream 
spring.cloud.stream.bindings.consuemr2.group=stream 
spring.cloud.stream.bindings.consumer3.group=stream

spring.cloud.stream.bindings.consumer4.group=stream2

partition:分区消费策略

一个或多个生产者将数据发送到多个消费者,并确保有共同特征标识的数据由同一个消费者处理。默认是对消息进行hashCode,然后根据分区个数取余,所以对于相同的消息,总会落到同一个消费者上。

虽然上面说的 rabbitMQ 是没有实现分组消费的,但是 SCS 提供了分区消费的方式,让消息被指定的消费者消费

要使用分组消费策略,需要在生产者和消费者两端都进行分组配置。

# 生产者
#指定参与消息分区的消费端节点数量 
spring.cloud.stream.bindings.output.producer.partition-count=2 
#只有消费端分区ID为1的消费端能接收到消息 
spring.cloud.stream.bindings.output.producer.partition-key-expression=1

# 消费者1
#启动消费分区 
spring.cloud.stream.bindings.input.consumer.partitioned=true 
#参与分区的消费端节点个数 
spring.cloud.stream.bindings.input.consumer.instance-count=2 
#设置该实例的消费端分区ID 
spring.cloud.stream.bindings.input.consumer.instance-index=1

# 消费者2
#启动消费分区 
spring.cloud.stream.bindings.input.consumer.partitioned=true 
#参与分区的消费端节点个数 
spring.cloud.stream.bindings.input.consumer.instance-count=2 
#设置该实例的消费端分区ID 
spring.cloud.stream.bindings.input.consumer.instance-index=0

两个消费者实例会组成一个消费者组。而生产者发送的消息,只会被消费者1消费到(生产者的partition-key-expression 和消费者的 instance-index 匹配)。

原理就是增加了分区的设置后,每个分区都会在rabbitMQ创建单独的 queue,并且拼接对应的index在名字后面,发消息的时候会发到带索引值的队列上,不是原有队列上,就完成了分区消费。

分组的方式不仅仅可以是index,还可以自定义,分组表达式可以放到消息的header当中。将生产者端的分组表达式配置为header['partitonkey']

#生产者端设置 
spring.cloud.stream.bindings.output.producer.partition-keyexpression=header['partitionkey']
// 在发送消息时,给消息指定一个header属性,来控制控制分组消费结果。
Message message = MessageBuilder.withPayload(str).setHeader("partitionKey", 0).build(); 
source.output().send(message);

原理剖析(关注开源中间件)

RabbitMQ

消息零丢失方案

  • 生产者保证消息不丢失:同步确认和异步确认。
    • 方式1:同步确认就是在生产者设置一个等待确认的完成时间(Channel.waitForConfirmsOrDie())。
    • 方式2:异步就是在生产者中注入 ConfirmListener(channel.addConfirmListener(ConfirmCallback var1, ConfirmCallback var2))第一个函数是在生产者发送消息时调用,第二个函数则是生产者收到Broker的消息确认请求时调用。两个函数需要通过sequenceNumber自行完成消息的前后对应。sequenceNumber的生成方式需要通过channel的序列获取。int sequenceNumber = channel.getNextPublishSeqNo();
    • 方式3:手动事务:一般不会使用手动事务的方式控制逻辑,会造成业务阻塞。channel.txSelect()开启事务; channel.txCommit() 提交事务; channel.txRollback() 回滚事务;
  • 存盘不丢失:对于经典队列需要设置持久化队列,对于新的2个类型都是持久化队列无需设置。
  • 主从消息同步不丢失:普通集群消息分散存储并且是不会主动同步消息的,所以可能丢失。镜像集群数据会主动同步到其他集群。
  • 消费者不丢失:自动应答和手动应答模式。自动应答是消费者处理完业务之后就会自动应答,业务异常就会消息重试,可能会导致一直重试,手动应答可以提高消息的可靠性。

消息幂等方案

  • 自动重试的次数需要配置。spring.rabbitmq.listener.simple.retry
  • 消息体中设置 MessageID 在客户端做幂等判断

消息顺序方案

唯一比较好的方法就是:单队列+ 单消息推送。只发到一个队列里面,变为一组有序的消息。但这是消耗性能的,尽量避免需要保证顺序。

消费的时候,队列保证只有一个消费者,设置单次推送的消息数为1 spring.rabbitmq.listener.simple.prefetch=1

数据堆积问题

尽量让消息的消费速度和生产速度保持一致,因为数据堆积的时候,整体性能会下降,新版本的队列有优化但是还不够完善。

  • 生产者:降低生产的速度。尽量多采用批量消息的方式,降低IO的频率。
  • Server:尝试使用 Stream 等新的队列,使用分片队列。
  • 消费者:增加消费者数量,加机器,短时间消费掉积压的消息。需要注意其他组件的性能压力。单个消费者可以提高消费线程数量 spring.rabbitmq.listener.simple.concurrency=5
  • 紧急方案:无法调整消费者端,可以紧急上线一个消费者组,专门将消息快速转移到Redis或者数据库,然后再慢慢处理。

RocketMQ

整体交互架构

1662263559302-0ff7eb91-aba3-49ad-9a32-0e9841fabb7a.png

生产常见消息方案

顺序消息

局部有序,而不是全局有序。

  • 发送端:发送者会采用轮询的方式将消息发到不同的分区也就是MessageQueue,所以发到一个分区就可以保证是有序的。
    • 将Topic配置成只有一个MessageQueue队列(默认是4个)。
    • 或者可以自己实现一个 MessageSelector 来指定消息根据指定的分片键到达某一个队列
  • 消费端:消费者会从不同的分区也就是MessageQueue拿消息,多个队列之间是乱序的。要取完一个队列之后再取下一个,给 consumer 注入的 MessageListenerOrderly 对象,在RocketMQ内部就会通过锁队列的方式保证消息是一个一个队列来取的。

广播消息

相比集群模式一个消费者组只有一个实例消费到消息,广播模式不会区分组的概念,只要订阅主题就会发送给这个消费者。消费的进度是维护在消费者端的。

批量消息

将多条消息合并成一个批量消息,一次发送出去。这样的好处是可以减少网络IO,提升吞吐量。一个批次消息的大小不要超过默认最大值1MB,否则需要分批发送(自己切割)。这些消息应该有相同的Topic,相同的waitStoreMsgOK。而且不能是延迟消息、事务消息等。

过滤消息

在大多数情况下,可以使用Message的Tag属性来简单快速的过滤信息。一个应用可以就用一个Topic,而应用中的不同业务就用TAG来区分。

复杂场景可以使用SQL表达式来对消息进行过滤。只有推模式的消费者可以使用SQL过滤。拉模式是用不了的。

消息过滤是在Broker端进行的还是在Consumer端进行的?过滤是在broker进行的,代码里面是写在consumer的,建立连接的时候会将自己的过滤字段传递给broker。

延迟消息

message.setDelayTimeLevel(3); 设置延迟级别,开源版本不支持任意时间的延迟,1到18分别对应 messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。在CommitLog.putMessage 写入消息时,会判断消息的延迟级别,然后修改Message的Topic和 Queue,达到转储Message的目的。Broker中会创建一个默认的Schedule_Topic主题,这个主题下有18个队列,对应18个延迟级别。消息发过来之后,会先把消息存入 Schedule_Topic主题中对应的队列。然后等延迟时间到了,再转发到目标队列,推送给消费者进行消费。

这里的延迟级别在事务消息的回查以及消费消息的重试机制里面都有使用到。

画板

事务消息

  1. 是保证最终一致性的两阶段提交的实现,保证本地事务执行与消息发送两个操作一起成功或者一起失败。在生产者中会指定事务监听器,事务监听器会设置本地事务的执行逻辑以及回查时候的逻辑。
  2. 使用方法:在TransactionMQProducer中指定了一个 TransactionListener事务监听器,TransactionListenerImpl implements TransactionListener
  3. 使用限制:事务消息不支持延迟消息和批量消息。
    1. 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限制。如果已经检查某条消息超过则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。
    2. 事务性消息可能不止一次被检查或消费。
  4. 实现机制:会发送一个半消息,存到内部的一个 RMQ_SYS_TRANS_HALF_TOPIC 这个Topic,对消费者是不可见的。半消息的作用更多是嗅探server是否可用。事务回查或者提交检查通过之后,再将消息转存到目标topic,就可见了。
    1. 一般本地事务失败的话都会将消息缓存起来,回查的时候再次执行。注意避免无限重试。
  5. 使用场景:需要保证消息不丢失发到server,或者要实现定时的任务回查(不使用延迟队列,直接设置回查间隔时间和次数)。重试15次依旧失败,会放到死信队列里面。

如何自己实现事务消息?

画板

为什么推模式其实是拉模式?

broker给消费者提供的推模式和拉模式的消费方式本质上都是拉模式,推模式只是一个定时的拉模式,这样的问题就是有一直空轮询的问题,并且消息到达的时候也无法及时发现,只能等待下一次consumer请求。

为此,RocketMQ 实现了长轮询 long polling,拉取请求过来的时候,如果没有消息,就不返回,不会直接给一个空的响应,会将这个pull请求缓存起来,默认是挂起15秒,消息到达的时候从缓存里面将请求拿出来,然后将消息发给consumer。

生产常见问题方案

消息不丢失方案

  • 生产者:事务消息机制保证消息零丢失,
  • broker:同步刷盘+Dledger主从架构保证MQ主从同步时不会丢消息。Dledger会通过两阶段提交 + 超半数同意就ACK的方式保证文件在主从之间成功同步。
  • 消费者:先处理本地业务,再返回消费ACK,并且不要在处理逻辑中新建线程异步处理。
  • 整个 Server 不可用的时候,重试多次失败,在调用方要有自己的降级方案,恢复过来之后再将消息重新发送出去。

但是同步的方式对于性能的消耗是比较大的,所以非必要最好是采用异步的方式刷盘。

消息积压方案

注意积压是处理不过来,不是一直消费失败,一直失败那最后会变为死信的。

  • 方案一:通过增加Consumer 的服务节点数量来加快消息的消费,极限的情况是把Consumer的节点个数设置成跟MessageQueue的个数相同。
  • 方案二:创建一个新的Topic,配置足够多的MessageQueue。然后把所有消费者节点 的目标Topic转向新的Topic,并紧急上线一组新的消费者,只负责消费旧Topic中的消息,并转储到新的Topic中,这个速度是可以很快的。然后在新的Topic上,就可以通过增加消费者个数来提高消费速度了。

消息重试触发与处理

  • 广播模式的消息, 是不存在消息重试的机制的,只是继续消费新的消息。因为 broker 不维护消费进度,是每一个消费者自己维护的。
  • 普通的消息:
    • 如何触发重试:当消费者消费消息失败后,通过设置返回**Action.ReconsumeLater**状态达到消息重试的结果,或者返回 null,消息将重试,或者直接抛出异常, 消息将重试。直接返回**Action.CommitMessage**就不会重试了。
    • 如何处理重试:重试的消息会进入一个 “%RETRY%”+ConsumeGroup 的队列中。RocketMQ默认允许每条消息最多重试16次,每次重试的间隔时间跟延迟消息的延迟级别是对应的。不过取的是延迟级别的后16级别。messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h ,指的是与上次重试间隔的时间。
    • 重试次数:如果消息重试16次后仍然失败,消息将不再投递。转为进入死信队列。消息最大重试次数的设置对相同GroupID下的所有Consumer实例有效。consumer.setMaxReconsumeTimes(20); 将重试次数设定为20次。当定制的重试次数超过16次后,消息的重试时间间隔均为2小时。这个次数设置是最后启动的Consumer会覆盖之前启动的Consumer的配置。
    • MessageId:这些重试消息的MessageId始终都是一样的。但是在4.9.1版本中,每次重试MessageId都会重建。如果需要保证幂等性,需要自己设置业务的上的幂等ID。

死信队列产生时机和处理

  • 死信队列的名称是%DLQ%+ConsumGroup一个死信队列对应一个ConsumGroup,而不是对应某个消费者实例。如果一个ConsumeGroup没有产生死信,RocketMQ就不会为其创建相应的死信队列。一个死信队列包含了这个ConsumeGroup里的所有死信消息,而不区分该消息属于哪个Topic。
  • 删除机制:死信队列的有效期跟正常消息相同。默认3天,对应broker.conf中的 fileReservedTime属性。超过这个最长时间的消息都会被删除,而不管消息是否消费过。
  • 处理死信消息:一般需要人工去查看死信队列中的消息,错误原因进行排查。然后对死信消息进行处理,比如转发到正常的Topic重新进行消费,或者丢弃。死信的topic 权限permission默认是2,2 不可读不可写 4 可读不可写 6 可读写,默认是2是因为需要人工干预。他里面的消息是无法读取的,在控制台和消费者中都无法读取。 需要手动将死信队列的权限配置成6,才能被消费(可以通过mqadmin指定或者web控制台)。

消息幂等必要性和方式

在MQ系统中,对于消息幂等有三种实现语义:

  • at most once 最多一次:每条消息最多只会被消费一次 ,对于RocketMQ,最好实现,可以异步可以 sendOneWay
  • at least once 至少一次:每条消息至少会被消费一次,对于RocketMQ,同步发送和事务发送都可以
  • exactly once 刚刚好一次:每条消息都只会确定的消费一次,对于RocketMQ,需要业务系统自己保证。商业版是有明确支持的。

为什么需要幂等?

  • 发消息重复:接收到但是返回的时候应答生产者失败,就会再次发送,导致收到系统内容的并且MessageID相同的消息。
  • 投递消息重复:消费好了,但是返回服务端的时候闪断,会在恢复之后重新投递之前处理过的消息。就会收到内容和messageId都一致的消息。
  • 负载均衡消息重复:重启或者扩容的时候,会触发 Rebalance,此时可能会收到重复的消息。

如何处理?

  • 将有业务含义的ID作为message的Key来进行传递。

高性能的原因

使用读写队列实现读写分离

  • 控制台创建 topic 的时候,可以设置读写队列的数量,读队列主要用来维护 offset ,写队列主要是用来保存消息。
  • 读写队列的数量限制:一般一个topic的读写队列数量都是一致的,1:1。借助了读写分离的思想,如果读数量大于写的数量,就会有的分区没有消息,造成浪费,如果写数量大于读的数量就会导致有的写分区的消息无法被同步到读队列。
  • 扩缩容操作保证消息不丢失:一般数量不一致是需要改变分区数量的时候,为了保证消息不丢失,减少的时候先减少写队列,再减少读队列。

消息持久化:高效的追加写和索引结构

画板

indexFile文件结构:https://blog.csdn.net/roykingw/article/details/120086520

过期删除机制:节省空间

  • 删除标准:文件的保留时间,而并不关心文件当中的消息是否被消费过。所以,RocketMQ的消息堆积也是有时间限度的。同样的kafka也是即使没有消费达到删除阈值就会删除。
  • 删除时机:RocketMQ内部有一个定时任务,对文件进行扫描。DefaultMessageStore.addScheduleTask -> DefaultMessageStore.this.cleanFilesPeriodically()
    • 用户可以在broker.conf中deleteWhen属性指定文件删除操作的执行时间。 默认是凌晨四点。
    • 如果磁盘空间的使用率达到一定的阈值,也会触发过期文件删除。broker的磁盘空间不要少于4G。

高效写文件:零拷贝技术 + 顺序写 + 刷盘机制

画板

  • CPU 和 DMA 拷贝数据的不同:CPU自己负责导致调度被各种IO占满,DMA负责IO操作让CPU只需要管理DMA即可。DMA 使用 channel 通道而不是数据总线,使用自己的IO指令,适合大型IO操作,避免占用过多总线影响数据读写性能
  • mmap:java中实现通过 java.nio.channels.FileChannel**map**** **方法完成映射。
    • 普通的文件复制都是:磁盘数据被DMA先拷贝到内核作为文件页缓存,内核数据再被CPU拷贝到用户内存,也就是java的堆内存,再反向拷贝回去。需要4次拷贝。java.nio.HeapByteBuffer映射的就是JVM的一块堆内内存,操作都会先操作自己定义的 byte 数组,没有使用零拷贝。
    • mmap 的拷贝方式:用户态不保存用户文件,只保存文件的元数据,包括文件的内存起始地址,文件大小等。直接通过操作映射,在内核完成数据的复制。4次拷贝变为3次拷贝。java.nio.DirectByteBuffer则映射的是一块堆外内存,只保存了一个内存地址,所有数据读写都是过unsafe魔法类直接交由内核完成。由于mmap映射机制还是需要用户态保存文件的映射信息,复制的时候也需要用户态参与。
    • 适用场景:适合操作小文件,文件太大,在用户内存里面的映射信息太大,通常建议不要超过2GB。RocketMQ限制CommitLog大小1GB,也是为了方便文件映射。
  • sendFilejava.nio.channels.FileChannel**transferTo**方法完成。
    • 早期的sendfile实现机制其实还是依靠CPU进行页缓存与socket缓存区之间的数据拷贝
    • 后期只从页缓存拷贝一个带有文件位置和长度等信息的文件描述符FD给socket缓冲区,真实的数据内容,会交由DMA控制器,从页缓存中打包异步发送到socket中。
    • 使用场景:不需要用户态参与,这种机制的传输效率是非常稳定的。sendfile机制非常适合大数据的复制转移。
  • 更多关于二者的区别:参考https://xiaolincoding.com/os/8_network_system/zero_copy.html#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E9%9B%B6%E6%8B%B7%E8%B4%9D
  • 关于顺序写:org.apache.rocketmq.store.CommitLog#DefaultAppendMessageCallback中的 doAppend方法会以追加的方式将消息先写入到一个堆外内存 byteBuffer中,然后再通过fileChannel写入到磁盘。避免磁盘上有很多的碎片
  • mq 的刷盘机制配置:刷盘方式是通过Broker配置文件里的 flushDiskType 参数设置的,这个参数被配置成 SYNC_FLUSH同步刷盘ASYNC_FLUSH异步刷盘中的一个。同步刷盘机制会更频繁的调用fsync,所以吞吐量相比异步刷盘会降低,但是数据的安全性会得到提高。
    • 操作系统的页缓存机制:PageCache缓存以 4K 大小为单位。在Linux中,对于有数据修改的 PageCache,会标记为Dirty(脏页)状态。当Dirty Page的比例达到一定的阈值时, 就会触发一次刷盘操作,关机也会触发。应用程序可以自行调用Linux中是fsync这个系统调用,完成PageCache的强制刷盘。

消息主从复制

集群部署中:master节点和多个slave节点消息复制的方式分为同步复制和异步复制。配置方式:消息复制方式是通过Broker配置文件里的brokerRole参数进行设置的,这个参数可以被设置成ASYNC_MASTER、 SYNC_MASTER、SLAVE三个值中的一个。

  • 同步复制是等 Master 和 Slave 都写入消息成功后才反馈给客户端写入成功的状态。故障容易恢复数据。但是同步复制会增大数据写入的延迟,降低系统的吞吐量。
  • 异步复制是只要 Master 写入消息成功,就反馈给客户端写入成功的状态。然后再异步将消息复制给Slave节点。系统拥有较低的延迟和较高的吞吐量。但是如果master节点故障就可能会造成数据丢失。

高可用集群:Dledger集群

  1. Dledger是使用Raft算法来进行master节点选举的。是可以实现 master的崩溃自动选举的,使用原本的主从是需要手动配置的。
  2. 优化master节点往slave节点的消息同步机制。采用Raft协议进行多副本的消息同步,可以保证在大多数同步到slave节点的时候就返回客户端响应,加速的同时不会丢失数据。

各角色节点主要原理

源码阅读的一些细节笔记: 《RocketMQ源码》

1668503706444-fcb87c71-328c-41ed-bff6-e615105c85fc.png

  • broker:这个里面存放的就是RocketMQ的Broker相关的代码,这里的代码可以用来启动Broker进程
  • client:这个里面就是RocketMQ的Producer、Consumer这些客户端的代码,生产消息、消费消息的代码都在里面
  • common:这里放的是一些公共的代码
  • dev:这里放的是开发相关的一些信息
  • distribution:这里放的就是用来部署RocketMQ的一些东西,比如bin目录 ,conf目录,等等
  • example:这里放的是RocketMQ的一些例子 ,抄代码的地方
  • filter:这里放的是RocketMQ的一些过滤器的东西
  • logappender和logging:这里放的是RocketMQ的日志打印相关的东西
  • namesvr:这里放的就是NameServer的源码
  • openmessaging:这是开放消息标准,这个可以先忽略
  • remoting:这个很重要,这里放的是RocketMQ的远程网络通信模块的代码,基于netty实现的
  • srvutil:这里放的是一些工具类
  • store:这个也很重要,这里放的是消息在Broker上进行存储相关的一些源码
  • style、test、tools:这里放的是checkstyle代码检查的东西,一些测试相关的类,还有就是tools里放的一些命令行监控工具类

NameServer 原理

nameServer 一是维护 Broker 的服务地址并进行及时的更新。二是给 Producer 和 Consumer 提供服务获取 Broker 列表。

NameServer的启动入口为NamesrvStartup类的main方法,NameServer的核心就是一个NamesrvController对象,响应客户端请求。在Controller的启动以及关闭过程中,会逐步启动RocketMQ的各种内部服务。

1662263991147-0cae7643-2ba8-40f0-a3b1-17723fec1c7d.png

简单的总结图:

画板

Broker 原理

主要负责消息存储、转发。以及和NameServer之间的心跳注册

Broker启动的入口在BrokerStartup这个类的main方法,有 BrokerController 负责响应请求,各种Service组件负责具体业务,然后还有负责消息存盘的功能模块则类似 于Dao。

1662264379598-5961edad-9536-480b-b84b-0188bed2d965.png

画板

生产者启动以及发送消息 原理

工厂设计模式,生产者和消费者客户端的启动流程最终都是统一的,全是交由mQClientFactory对象来启动。而不同之处在于 这些客户端在启动过程中,按照服务端的要求注册不同的信息。例如生产者注册到 producerTable,消费者注册到consumerTable,管理控制端注册到 adminExtTable。

1662264518531-787b84d4-bd5d-403f-99cb-4b6592a13d19.png

消费者启动以及消费消息 原理

1662264536507-96053b0e-b632-4057-bacd-c953b4a7e60e.png

负载均衡

  • Producer:Producer发送消息时,
    • 轮询:默认会轮询目标Topic下的所有MessageQueue,并采用 递增取模的方式往不同的MessageQueue上发送消息,以达到让消息平均落在不同 的queue上的目的。
    • 有序消息:生产者在发送消息时,可以指定一个MessageQueueSelector。通过这个对 象来将消息发送到自己指定的MessageQueue上。这样可以保证消息局部有序。

1668502018567-97d994d3-0186-4e23-9a9b-e17666b7cb32.png

  • Consumer:是以MessageQueue为单位来进行负载均衡。每一个消费者都接管一个 topic 下的几个分区,也就是每一个消费者都负责消费对应topic下的几个message queue。这个负责关系是在实例数量变更之后立马生成的。分为集群模式和广播模式。
    • 集群模式:只需要给订阅这个 topic 的 Consumer Group 下的一个消费者即可。采用主动拉取消费消息,拉的时候需要指定是那一条消息队列message queue。
      • 实例的数量有变更,会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量分配 queue 给每一个实例。都会将MessageQueue和消费者ID进行排序后,使用内置的分配的算法分配,分别对应 AllocateMessageQueueStrategy下的六种实现类,可以在consumer中直接set来指定。默认是 AllocateMessageQueueAveragely策略也就是平均分配,按组内的消费者个数平均分配c1:{q1,q2},c2:{q3,q4},c3{q5,q6}常用的还有AllocateMessageQueueAveragelyByCircle平均轮询分配,按组内的消费者一个一个轮询分配c1:{q1,q4},c2:{q2,q5},c3:{q3,q6}
        • AllocateMachineRoomNearby: 将同机房的Consumer和Broker优先分配在一起。
        • AllocateMessageQueueAveragely:平均分配。将所有MessageQueue平均分给每一个消费者
        • AllocateMessageQueueAveragelyByCircle:轮询分配。轮流给一个消费者分配一个MessageQueue。
        • AllocateMessageQueueByConfig:不分配,直接指定一个messageQueue列表。类似于广播模式,直接指定所有队列。
        • AllocateMessageQueueByMachineRoom:按逻辑机房的概念进行分配。又是对BrokerName和ConsumerIdc有定制化的配置。
        • AllocateMessageQueueConsistentHash。这个一致性哈希策略只需要指定一个虚拟节点数,是用的一个哈希环的算法
    • 广播模式:每一条消息都会投递给订阅了Topic的所有消费者实例。Consumer分配Queue时,所有 Consumer都分到所有的Queue消费者的消费偏移量不再保存到broker当中,而是保存到客户端当中,由客户端自行维护自己的消费偏移量。

文件存储原理

1668505607762-fbe95cfc-f6ee-4e0a-a0c2-a7cabe0b6060.png

即使是同步刷盘,如果是一批消息,不会一个个刷到磁盘,而是会收集一批次的刷盘请求统一一次刷盘。

文件也是有定时任务会定期删除commitLog、indexFile以及ConsumerQueue。

如果服务异常宕机,会造成CommitLog和ConsumeQueue、IndexFile文件不一致,有消息写入CommitLog后,没有分发到索引文件,这样消息就丢失了。 DefaultMappedStore的load方法提供了恢复索引文件的方法。

commitLog 的主从复制:https://blog.csdn.net/qq_25145759/article/details/115865245

Netty 作为底层通讯工具在 RocketMQ中的使用

画板

所有远程通信功能都由remoting模块实现。

  • Broker 对于Producer和Consumer来说是RemotingServer, 对于NameServer来说,是RemotingClient

:::color1

  • **RocketMQ 基于netty 维护了一个服务码和Processor之间的映射关系。不管是 RemotingServer 还是RemotingClient 都会维护一个 processorTable,格式是 ****<requestCode, Pair<NettyRequestProcessor, ExecutorService>>**

:::

一些关于 RocketMQ 比较关键的问题

1668525305668-abbdee0e-3056-4d60-a305-3c36b2bf2bd4.png

《消息队列重点问题》

Kafka

整体架构

服务端(brokers)和客户端(producer、consumer)之间通信通过TCP协议来完成。

画板

生产常见方案

消息不丢失方案

  • 消息发送端:
    • acks=0:(消费者最多收到一次消息,0-1次)producer不需要broker的响应就可以发下一条。性能要求高但是不要求非常准确的报表可以使用。
    • **acks=1: 至少要等待leader已经成功将数据写入本地log,但是不需要等待所有follower写入。可以继续发送下一条消息。这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失。 **
    • acks=-1或all:(消费者至少收到一次消息,1-多次)这意味着leader需要等待所有备份(min.insync.replicas配置的备份个数)都成功写入日志,这是最强的数据保证。一般除非是金融级别使用。当然如果 min.insync.replicas配置的是1则也可能丢消息,跟acks=1情况类似。
  • 消息消费端: 如果消费这边配置的是自动提交,万一消费到数据还没处理完,就自动提交offset了,但是此时你consumer直接宕机了,未处理完的数据丢失了,下次也消费不到了。

消息幂等方案

  • 消息发送端:发送端发送超时,实际broker可能已经接收到消息,但发送方会重新发送消息。
    • 在生产者加上参数 props.put(“enable.idempotence”, true) 即可,默认是false不开启。kafka每次发送消息会生成PID和Sequence Number,并将这两个属性一起发送给broker,broker会将PID和 Sequence Number跟消息绑定一起存起来,下次如果生产者重发相同消息,broker会检查PID和Sequence Number,如果相同不会再接收。
  • 消息接收端:消费这边配置的是自动提交,刚拉取了一批数据处理了一部分,但还没来得及提交,服务挂了,下次重启又会拉取相同的一批数据重复处理。
    • 一般上面两个情况都是消费端做消费幂等处理的,可以在消息里面加一个有业务含义的 messageId。

消息顺序方案

如果发送端配置了重试机制,kafka不会等之前那条消息完全发送成功才去发送下一条消息,导致原本有序的消息因为重试乱序。

  • topic内所有partition有序:Kafka只在partition的范围内保证消息消费的局部顺序性,可以通过将topic的partition数量设置为1,将consumer group中的 consumer instance数量也设置为1,但是这样会影响性能
  • 可以设置一个消费者:接收到所有partition的消息后将需要保证顺序消费的几条消费发到内存队列,一个内存队列开启一个线程顺序处理消息。

消息堆积问题

  • 原因1:发太快来不及处理
    • 和上面的RocketMQ一样的处理方式
  • 原因2:消费不成功
    • 转移到死信队列

延时队列

需要自己实现,实现的思路就是rocketMQ已经提供的方式,需要自己写一个定时任务去扫描自己的定义出来的延时队列。

消息回溯

可以指定从多久之前的消息回溯消费,这种可以用 consumer 的 offsetsForTimes、seek等方法指定从某个offset偏移的消息开始消费。

事务消息

Kafka的事务主要是保障一次发送多条消息要么同时成功要么同时失败。

想要实现像 Rocketmq 保障本地事务(比如数据库)与 mq 消息发送的事务一致性,要自己开发。

高性能的原因有哪些?

  • 日志是顺序写的
  • 数据传输是零拷贝的,使用的是上面说的sendfile方式
  • 读写数据是批量处理和压缩传输的

如何实现广播与单独消费?

这里和rockemq是一致的,广播就让每一个消费者属于一个独立的消费者组,单独消费就让所有队列属于一个消费者组。

消费者数量与topic中分区数的关系?

这里和rockemq是一致的,consumer group中的consumer instance的数量不能比一个Topic中的partition的数量多,否则,多出来的 consumer消费不到消息。

Broker 如何记录消费消息的offset?

每个 consumer 会自己记录消费的 topic 下的分区的 offset。并且会定期将自己消费分区的 offset 提交给 kafka 内部topic:**__consumer_offsets**,提交过去的时候,key是 consumerGroupId+topic+分区号,value就是当前offset的值

日志存储方式

一个 topic 的一个分区的消息对应存储到一个文件夹下,文件夹以topic名称+分区号命名,消息在分区内是分段(segment)存储,Kafka Broker 有一个参数,log.segment.bytes,限定了每个日志段文件的大小,最大就是 1GB。一个日志段文件满了,就自动开一个新的日志段文件来写入,避免单个文件过大,影响文件的读写性能,这个过程叫做 log rolling,正在被写入的那个日志段文件,叫做 active log segment。

分段主要是为了删除的时候方便,以及加载到内存的时候方便。一个段日志主要有三类文件:

  • index 文件:offset 与 消息的映射文件,按照 offset 查询使用这个
  • Log 文件:实际的消息 和 offset
  • timeindex 文件:消息和时间的映射关系,按照时间查询使用这个

分区一致性的 ISR 机制

In-Sync Replicas 是一个副本的列表,里面存储的都是能跟 leader 数据一致的副本,确定一个副本在isr列表中,有2个判断条件:副本和Leader的交互时间差大于阈值或者副本和leader之间的信息条数差值超过阈值就会导致被移除ISR列表。

这个列表主要是为了维护一个优质副本的列表,方便在替换leader的时候快速找到优质副本。均衡了确保数据不丢失以及吞吐率,保证所有机器都同步太麻烦,一个都不保证又不安全。

消费者 Rebalance 流程

**消费者组中的消费者数量变化、消费的topic分区有变化、消费组订阅了更多的 topic **的时候,会重新分配消费者和分区的关系。

画板

rebalance过程中,消费者无法从kafka消费消息,这对kafka的TPS会有影响,如果kafka集群内节点较多,比如数百个,那重平衡可能会耗时极多,所以应尽量避免在系统高峰期的重平衡发生。

生产者发布消息流程

0b39cf14c02f2fa2018ba8c4c1eea341.svg

HW与LEO如何保证生产消息的时候不丢失?

  • LEO:log-end-offset,代表当前分区的消息所在位置的最高水位。
  • HW:HighWatermark ,高水位,一个ISR中所有分区最小的LED就是这个ISR的高水位,消费者最多只可以消费到 HW 的位置,leader收到消息之后会更新自己的LEO,等到所有的ISR分区都同步了这个消息之后,才会让HW移动到这个消息所在的位置,此时消费者才可以消费这个消息。
  • 对于broker之间的读取请求,是没有HW限制的。

核心控制器 Kafka Controller

在Kafka集群中会有一个或者多个broker,其中有一个broker会被选举为控制器(Kafka Controller),主要用来选举分区的 Leader,管理分区和副本的状态、管理集群的元数据。

选举 Controller 机制

启动的时候,每个broker都会通过分布式锁的方式在zk上创建一个/controller临时节点,成功就是 Controller。

宕机的时候zk的临时节点就会消失,由于其他broker都会监听这个临时节点,所以就会再次抢锁,再次选出。

如何监听 topic broker 的变化

1660695420427-8394c573-132e-4386-afb7-0a8e66f941ca.png

为zk中的/brokers/ids/节点(broker增减)、/brokers/topics节点(topic增减)、/admin/delete_topics节点(删除topic)、/brokers/topics/[topic]节点(topic的分区变化)添加各种 Listener,Controller 会监听相应的变化,之后将其同步到其他的节点上。

分区选举 Leader 机制

感知到一个 topic 的分区的Leader挂了,就会从ISR列表里挑第一个broker作为leader(第一个broker最先放进ISR 列表,可能是同步数据最多的副本),如果参数unclean.leader.election.enable为true,代表在ISR列表里所有副本都挂了的时候可以在ISR列表以外的副本中选leader,提高可用性,但是选出的新leader有可能数据少很多。

副本进入ISR列表有条件:必须能与zookeeper保持会话以及跟leader副本网络连通;副本超过**replica.lag.time.max.ms**时间都没有跟leader同步过的一次会被移出ISR列表。

选型总结

RabbitMQ RocketMQ kafka MQTT EventBridge MNS
优点 可靠性高,功能全面 高吞吐,高性能,高可用,功能全面 吞吐量大,性能好,集群高可用 跨端支持 跨端,Serverless、低代码配置 概念简单,模型少,接入快速,
缺点 吞吐量低,消息积累会影响性能。不好定制。 开源功能不如云上(延迟队列支持自定义时间,幂等消费只有云上有),只支持java 功能单一,丢数据? 安全性和海量数据处理有待提升 主要用于跨系统的事件通知,不用于系统内部的消息异步 不支持复杂场景使用,没有很复杂的高级特性
使用场景 小规模,内部系统调用 几乎所有的场景 日志分析,大数据采集,报警报告搜集,运营指标 物联网场景、跨端场景 跨系统事件通知,开放平台提供事件通知能力给ISV serverless想使用简单的消息
  • 面向互联网的应用场景,更加注重MQ的吞吐量,需要将消息尽快的保存下来,再供后端慢慢消费。Kafka是第一个场景的不二代表
  • 针对企业内部的应用场景,更加注重MQ的数据安全性,在复杂多变的业务场景下,每一个消息都需要有更加严格的安全保障。RabbitMQ作为一个老牌产品,是第二个场景最有力的代表。

posted on 2025-10-13 01:09  chuchengzhi  阅读(14)  评论(0)    收藏  举报

导航

杭州技术博主,专注分享云计算领域实战经验、技术教程与行业洞察, 打造聚焦云计算技术的垂直博客,助力开发者快速掌握云服务核心能力。

褚成志 云计算 技术博客