基础知识
异步 解耦 削峰填谷
消息队列有两种模型:队列模型和发布/订阅模型。
RabbitMQ
采用队列模型,RocketMQ
和Kafka
采用发布/订阅模型。为了提高并发度,往往发布/订阅模型还会引入队列或者分区的概念。即消息是发往一个主题下的某个队列或者某个分区中。RocketMQ
中叫队列,Kafka
叫分区,本质一样。
通过多队列全量存储相同的消息,即数据的冗余可以实现一条消息被多个消费者消费。
RabbitMQ
就是采用队列模型,通过 Exchange
模块来将消息发送至多个队列,解决一条消息需要被多个消费者消费问题。
一般我们称发送消息方为生产者 Producer
,接受消费消息方为消费者Consumer
,消息队列服务端为Broker
。
消息从Producer
发往Broker
,Broker
将消息存储至本地,然后Consumer
从Broker
拉取消息,或者Broker
推送消息至Consumer
,最后消费。
在物理上除了副本拷贝之外,一条消息在
Broker
中只会有一份,每个消费组会有自己的offset
即消费点位来标识消费到的位置。在消费点位之前的消息表明已经消费过了。当然这个offset
是队列级别的。每个消费组都会维护订阅的Topic
下的每个队列的offset
。
经典方案
如何保证消息不丢失?
持久化订阅 异步confirm
分别是生产消息、存储消息和消费消息。我们从这三个阶段分别入手来看看如何确保消息不会丢失。
生产者发送消息至Broker
,需要处理Broker
的响应,不论是同步还是异步发送消息,同步和异步回调都需要做好try-catch
,妥善的处理响应,如果Broker
返回写入失败等错误消息,需要重试发送。当多次发送失败需要作报警,日志记录等。
存储消息阶段需要在消息刷盘之后再给生产者响应,假设消息写入缓存中就返回响应,那么机器突然断电这消息就没了,而生产者以为已经发送成功了。
如果Broker
是集群部署,有多副本机制,即消息不仅仅要写入当前Broker
,还需要写入副本机中。那配置成至少写入两台机子后再给生产者响应。这样基本上就能保证存储的可靠了。一台挂了还有一台还在呢(假如怕两台都挂了..那就再多些)。这样就能保证在生产消息阶段消息不会丢失。
在消费者真正执行完业务逻辑之后,再发送给Broker
消费成功,这才是真正的消费了。
`生产者`需要处理好`Broker`的响应,出错情况下利用重试、报警等手段。
`Broker`需要控制响应的时机,单机情况下是消息刷盘后返回响应,集群多副本情况下,即发送至两个副本及以上的情况下再返回响应。
`消费者`需要在执行完真正的业务逻辑之后再返回响应给`Broker`。
但是要注意消息可靠性增强了,性能就下降了,等待消息刷盘、多副本同步后返回都会影响性能。因此还是看业务,例如日志的传输可能丢那么一两条关系不大,因此没必要等消息刷盘再响应。
如何处理重复消息?
幂等,分布式锁
如何保证消息的有序性?
有序性分:全局有序和部分有序。
要保证消息的全局有序,首先只能由一个生产者往Topic
发送消息,并且一个Topic
内部只能有一个队列(分区)。消费者也必须是单线程消费这个队列。这样的消息就是全局有序的!
不过一般情况下我们都不需要全局有序,即使是同步MySQL Binlog
也只需要保证单表消息有序即可。
部分有序我们就可以将Topic
内部划分成我们需要的队列数,把消息通过特定的策略发往固定的队列中,然后每个队列对应一个单线程处理的消费者。这样即完成了部分有序的需求,又可以通过队列数量的并发来提高消息处理效率。
如何处理消息堆积?
假如逻辑我们已经都优化了,但还是慢,那就得考虑水平扩容了,增加Topic
的队列数和消费者数量,注意队列数一定要增加,不然新增加的消费者是没东西消费的。一个Topic中,一个队列只会分配给一个消费者。
当然你消费者内部是单线程还是多线程消费那看具体场景。不过要注意上面提高的消息丢失的问题,如果你是将接受到的消息写入内存队列之后,然后就返回响应给Broker
,然后多线程向内存队列消费消息,假设此时消费者宕机了,内存队列里面还未消费的消息也就丢了。
- 考虑 消息的重复消费、消息丢失、保证消费顺序 分布式事务
Kafka
Producer: 消息的生产者,负责往Kafka集群中发送消息;
Consumer: 消息的消费者,主动从Kafka集群中拉取消息。
Consumer Group: 每个Consumer属于一个特定的Consumer Group,新建Consumer的时候需要指定对应的Consumer Group ID。
Broker: Kafka集群中的服务实例,也称之为节点,每个Kafka集群包含一个或者多个Broker(一个Broker就是一个服务器或节点)。
Message: 通过Kafka集群进行传递的对象实体,存储需要传送的信息。
Topic: 消息的类别,主要用于对消息进行逻辑上的区分,每条发送到Kafka集群的消息都需要有一个指定的Topic,消费者根据Topic对指定的消息进行消费。
Partition: 消息的分区,Partition是一个物理上的概念,相当于一个文件夹,Kafka会为每个topic的每个分区创建一个文件夹,一个Topic的消息会存储在一个或者多个Partition中。
Segment: 一个partition当中存在多个segment文件段(分段存储),每个Segment分为两部分,.log文件和 .index 文件,其中 .index 文件是索引文件,主要用于快速查询.log 文件当中数据的偏移量位置;
.log文件: 存放Message的数据文件,在Kafka中把数据文件就叫做日志文件。一个分区下面默认有n多个.log文件(分段存储)。一个.log文件大默认1G,消息会不断追加在.log文件中,当.log文件的大小超过1G的时候,会自动新建一个新的.log文件。
.index文件: 存放.log文件的索引数据,每个.index文件有一个对应同名的.log文件。
后面我们会对上面的一些核心概念进行更深入的介绍。在介绍完Kafka的核心概念之后,我们来看一下Kafka的对外提供的基本功能,组件及架构设计。
Kafka Controller
它的作用是管理和协调整个Kafka集群
- 主题的管理,创建和删除主题;
- 分区管理,增加或重分配分区;
- 分区
Leader
选举; - 监听
Broker
相关变化,即Broker
新增、关闭等; - 元数据管理,向其他
Broker
提供元数据服务;
消息推&拉模式
一般而言我们在谈论推拉模式的时候指的是 Comsumer 和 Broker 之间的交互。默认的认为 Producer 与 Broker 之间就是推的方式,即 Producer 将消息推送给 Broker,而不是 Broker 主动去拉取消息。
推模式
优点
- 消息实时性高, Broker 接受完消息之后可以立马推送给 Consumer。
- 对于消费者使用来说更简单,简单啊就等着,反正有消息来了就会推过来。 缺点
- 推送速率难以适应消费速率,推模式的目标就是以最快的速度推送消息,当生产者往 Broker 发送消息的速率大于消费者消费消息的速率时,消费不过来。当推送速率过快就像 DDos 攻击一样消费者就傻了。
- 并且不同的消费者的消费速率还不一样,身为 Broker 很难平衡每个消费者的推送速率,如果要实现自适应的推送速率那就需要在推送的时候消费者告诉 Broker ,我不行了你推慢点吧,然后 Broker 需要维护每个消费者的状态进行推送速率的变更
拉模式
拉模式指的是 Consumer 主动向 Broker 请求拉取消息,即 Broker 被动的发送消息给 Consumer。
优点
- 拉模式主动权就在消费者身上了,消费者可以根据自身的情况来发起拉取消息的请求。
- 拉模式可以更合适的进行消息的批量发送,基于推模式可以来一个消息就推送,也可以缓存一些消息之后再推送,但是推送的时候其实不知道消费者到底能不能一次性处理这么多消息。而拉模式就更加合理,它可以参考消费者请求的信息来决定缓存多少消息之后批量发送。 缺点
- 消息延迟
- 消息忙请求,忙请求就是比如消息隔了几个小时才有,那么在几个小时之内消费者的请求都是无效的,在做无用功。
RocketMQ 和 Kafka 都选择了拉模式,当然业界也有基于推模式的消息队列如 ActiveMQ。因此有了优化拉模式的操作
- 长轮询: rocketmq后台会有个 RebalanceService 线程,这个线程会根据 topic 的队列数量和当前消费组的消费者个数做负载均衡,每个队列产生的 pullRequest 放入阻塞队列 pullRequestQueue 中。然后又有个 PullMessageService 线程不断的从阻塞队列 pullRequestQueue 中获取 pullRequest,然后通过网络请求 broker,这样实现的准实时拉取消息
Kafka 在拉请求中有参数,可以使得消费者请求在 “长轮询” 中阻塞等待 简单的说就是消费者去 Broker 拉消息,定义了一个超时时间,也就是说消费者去请求消息,如果有的话马上返回消息,如果没有的话消费者等着直到超时,然后再次发起拉消息请求。
事务消息
rocketmq
kafka Kafka 的事务消息和 RocketMQ 的事务消息又不一样了,RocketMQ 解决的是本地事务的执行和发消息这两个动作满足事务的约束。
而 Kafka 事务消息则是用在一次事务中需要发送多个消息的情况,保证多个消息之间的事务约束,即多条消息要么都发送成功,要么都发送失败,就像下面代码所演示的。 Kafka 的事务基本上是配合其幂等机制来实现 Exactly Once 语义的,所以说 Kafka 的事务消息不是我们想的那种事务消息,RocketMQ 的才是。
自己实现消息中间件
- 消息中间件的几个重要角色,分别是生产者、消费者、Broker、注册中心。
- 消息中间件数据流转过程,无非就是生产者生成消息,发送至 Broker,Broker 可以暂缓消息,然后消费者再从 Broker 获取消息,用于消费。
- 注册中心用于服务的发现包括:Broker 的发现、生产者的发现、消费者的发现,当然还包括下线,可以说服务的高可用离不开注册中心。
- 通信讲起:各模块的通信可以基于 Netty 然后自定义协议来实现,注册中心可以利用 zookeeper、consul、eureka、nacos 等等,也可以像 RocketMQ 自己实现简单的 namesrv
- 为了考虑扩容和整体的性能,采用分布式的思想,像 Kafka 一样采取分区理念,一个 Topic 分为多个 partition,并且为保证数据可靠性,采取多副本存储,即 Leader 和 follower,根据性能和数据可靠的权衡提供异步和同步的刷盘存储。
- 利用选举算法保证 Leader 挂了之后 follower 可以顶上,保证消息队列的高可用。问到选举算法,所以可能会问 Bully 算法、Raft 算法、ZAB 算法等等。
- 为了提高消息队列的可靠性利用本地文件系统来存储消息,并且采用顺序写的方式来提高性能。可根据消息队列的特性利用内存映射、零拷贝进一步的提升性能,还可利用像 Kafka 这种批处理思想提高整体的吞吐。
RocketMQ进阶
- Producer:就是消息生产者,可以集群部署。它会先和 NameServer 集群中的随机一台建立长连接,得知当前要发送的 Topic 存在哪台 Broker Master上,然后再与其建立长连接,支持多种负载平衡模式发送消息。
- Consumer:消息消费者,也可以集群部署。它也会先和 NameServer 集群中的随机一台建立长连接,得知当前要消息的 Topic 存在哪台 Broker Master、Slave上,然后它们建立长连接,支持集群消费和广播消费消息。
- Broker:主要负责消息的存储、查询消费,支持主从部署,一个 Master 可以对应多个 Slave,Master 支持读写,Slave 只支持读。Broker 会向集群中的每一台 NameServer 注册自己的路由信息。
- NameServer:是一个很简单的 Topic 路由注册中心,支持 Broker 的动态注册和发现,保存 Topic 和 Borker 之间的关系。通常也是集群部署,但是各 NameServer 之间不会互相通信, 各 NameServer 都有完整的路由信息,即无状态。
底层存储
顺序读写
mmap文件内存映射
RocketMQ&Kafka应用
其他文章
activeMQ
https://blog.csdn.net/name_is_wl/article/details/82120329