消息队列笔记
消息队列模式:
点对点模式:多个生产者向同一个消息队列发送消息,一个消息只能由一个消费者消费
发布/订阅模式:单个消息被多个订阅者并发的获取和处理
消息队列的概念:
主题 topic:对消息进行分类,消息通过topic进行分类,一个topic有多个分区存储消息。
分区 partition:一个主题可以被分为若干个区,不同的分区可以分布在不同机器上,一个分区就是消息队列读取数据的最小粒度。
消费者群组 consumer group:多个消费者组成的群体。
批次:为了提高效率,消息会分批写入消息队列。
Broker:一个独立的消息队列服务器就是一个broker,负责消息的接收、持久化、发送。
broker集群:多个broker。
重平衡:消费者组某个消费者挂掉之后,需要重新分配订阅主题。
kafka:
吞吐量大
kafka:使用了mmap和sendfile
优点:
高吞吐量:一秒几十万条消息,因为是基于内存来缓存最近的消息,隔一段时间才将消息批量写入磁盘。写入磁盘的时候用日志文件来持久化消息,一个分区对应一个日志文件的存储,可以保证持久性和顺序性
低延时:延迟只有几毫秒
高伸缩性:一个主题多个分区,主题的分区可以分布在不同broker上
缺点:
单机超过64个分区,Load会标高,发送消息响应时间会边长。因为分区文件的日志是一个一个按顺序存储到磁盘当中的,所以存储数量多,性能差。
不支持消息路由和延时消息,不支持消息重试
kafka为什么快:
分区partition顺序写,磁盘IO性能好,寻址快。
最近的消息基于内存存储!!!
生产者的消息分页且批量持久化,采用mmap,减少拷贝次数
消费者拉消息的时候使用sendfile,直接从内核缓冲区复制到网卡进行发送。
消息消费:一个消费者组可以消费不同主题的消息,也可以消费同一个主题下多个分区的消息,但是一个分区的消息只能被一个消费者组的消费者消费
Zookeeper:Brocker注册、元数据管理,比如重平衡
如何保证顺序消息:
生成者向主题发送消息可以选择指定的key,根据key的hash和分区数计算索引值路由到分区,如果没有指定key,则均匀分布在各个分区。
全局有序:
一个topic下只有一个partition,消费者也使用单线程消费消息。
生产者发送消息的时候指定路由到一个partition,但是这样会影响吞吐量。
消费者组中,确保一个消费者只由一个消费者消费,可以设置消费者并发度等于或小于分区数。
局部有序:
同一个业务下的消息指定partition Key,这样一个业务下的消息就会被路由到一个partition。
但要避免数据倾斜!
如果要达到严格的顺序消费,还需要设置max.in.flight.requests.pre.connection=1,即生产者与单个Broker的连接上,允许同时未确认的请求的最大数量,这样可以防止消息重试导致消息乱序。
消息重试对消息顺序的影响:
设置max.in.flight.requests.pre.connection=1
如何保证消息不丢失:
生产者:
同步发送模式:将发送消息的应答机制(ACK)设置为all,等所有节点确认后再发送后续数据。
异步发送模式:生产者直接向缓冲区发消息,然后批量写到partition。此时生产者可以设置不限制阻塞超时时间,让生产者保持等待。
Kafka根据应答机制(ACK)确保消息不丢失。生产者向队列写入数据的时候可以设置参数为:0、1、all来确认kafka是否收到消息。
0:不确保消息发送成果
1:只要leader节点应答即可
all:需要leader及所有副本完成备份才应答
如果是向一个不存在的topic下入数据,kafka会自动创建topic与副本,partition和副本默认数量为1.
kafka的多副本机制:
一个partition分区有多副本机制,多副本中有一个leader副本和很多follower副本,消息先发送到leader副本里,然后follower副本从leader副本中拉取消息消费。
一个partition的leader副本的日志文件存储在当前Broker下,对应的follower1、follower2分别存储在其他Brocker下,不同Brocker上的独立目录,物理上完全隔离,作为互备。如果leader节点发生故障,集群中的其他节点通过Zookeeper选举新节点。
kafka的follower为什么不能用于消息消费:
生产者的消息先写入leader节点,消费者从leader节点拉取消息消费,kafka通过偏移量来控制消费者从那个位置开始读取数据,follower节点的数据知识作为备份。
kafka多分区与多副本的作用:
多分区可以提升一个topic下的并发能力,多副本可以提升容灾能力,但是也需要消耗存储空间。
Kafka的Zookeeper的用途:
ZK主要用于管理元数据信息。
Broker会到ZK上注册节点,记录Broker的IP地址与端口信息。
Topic会在ZK的不同Broker节点下注册分区文件夹。例如:/brokers/topics/my-topic/Partitions/1
ZK可以根据partition数量与消费者数量实现负载均衡。
kafka在2.8后引入了基于Raft协议的KRaft模式。
Kafka为什么快
顺序写磁盘:kafka将消息持久化到日志文件中都是append追加写,节省了寻址时间
零拷贝:
生产者的数据采用mmap文件映射到pagecache,再由OS决定刷盘。
消费者拉取信息的时候,kafka采用sendfile将内核缓冲区直接拷贝到网卡。
PageCache:直接和pagecache交换数据,不是和磁盘。
消息压缩:生产者使用消息压缩把消息批量发给Broker。
Kafka如何保证消息不被重复消费:
生产者消息重复发送:生产者做幂等,生产者有唯一id,每发送一条数据都带上一个sequence,消息落盘之后会递增,Broker只需要判断当前消息的sequence是否大于当前最大sequence判断是否已经落盘。
消费者消息重复消费:因为kafka是先消费消息再提交offset,如果消费者消费完毕之后没来得及应答或者应答消息丢失,Broker就会让消息重复消费。所以可以在消费者这边做幂等,例如可以把消息key缓存起来做幂等。
kafka消息消费失败:kafka默认重试次数为10,重试间隔为500ms。达到最大重试次数后消费失败的消息会进入死信队列,接下来处理死信队列信息即可。
RocketMQ:
高可靠性,思路和kafka类似,对消息可靠性传输及事务性做了优化
使用mmap减少用户态到内核态的切换和拷贝次数。
pagecache实际上是和虚拟地址相关联的物理页,写数据的时候会根据虚拟地址找到pagecache,然后写到对应的pagecache里面。
首先NIO下的一个类的map函数会把磁盘上文件的物理地址映射到用户进程的虚拟地址上,然后进程对数据的变动就写道pagecache里面,交给os异步刷盘。pagecache的存在就是可以让读变的更快,异步的写入也会更快 。
优点:
高吞吐量:单一队列能堆积百万条消息。
高伸缩性:
可靠性高:采用同步双写
消息有序性:同一个队列里面可以保证消息有序,但是不同队列不能保证
支持发布订阅、点对点消息模型:支持拉和推两种消息模式
缺点:不支持消息路由、消息有序只在一个队列里有保证
消息模型:发布/订阅模型
不同消费者组都可以消费同一个topic,一个消费者组内部在消费一个topic内消息的时候,不同消费者之间是竞争关系,一条信息只能被一个消费者组的一个消费者消费。
因为一个topic内的信息可以被多个消费者组消费,所以消息消费完之后不会立马删除,而是每个消费者组都记录一个offset偏移量,来确定消费的位置
NameServer:注册中心
Broker:一个独立的消息队列就是一个Broker,存储消息,为消息设置偏移量
主题 topic:消息的一级主题
子主题 tag:消息的二级主题,同一个业务模块不同目的的消息就是相同topic而不同tag
分组 group:一个组可以订阅多个主题
队列 Queue:FIFO的队列,类似于kafka的分区 partition
RocketMQ的事务消息:
二阶段提交+消息回查
二阶段提交的过程:
prepare阶段:生产者先发送半事务消息给MQ,此消息对消费者不可见;
commit or rollback阶段:生产者执行本地事务成功,则发送commit消息给MQ,然后这条消息对消费者是可见的,可以消费;如果生产者执行本地事务失败,则发送rollback消息给MQ,这条消息被删除;如果生产者执行完业务没有发信息给MQ,MQ会进行消息回查。
消息回查:发送方prepare之阶段之后超时未响应,MQ会向发送方发送消息进行事务回查,消息内容包括消息ID和事务ID,发送方根据信息回查事务状态,一共有三种情况:提交、回滚、目前未知。此外发送方还可以通过消息ID和事务ID保证消息回查的幂等性。
RocketMQ的push、poll、pop
push:消息队列主动推消息给消费者,底层依然是基于poll实现的。
消息队列与消费者之间建立TCP长连接(双向连接)或者注册回调,消息队列数据发生变化就推消息给消费者。
优点:确保消息被实时处理,缺点:有的时候会给消费者带来较大的消费压力
poll:消费者根据自身消费能力去消息队列拉消息。消费者轮询消息队列的数据是否发生变化
优点:可以根据自身情况消费消息,缺点:轮询消息队列也会对其造成压力。
RocketMQ集群模式:
单Master多Slave、多Master无Slave、多Master多Slave
RabbitMQ:基于AMQP协议实现,适合对数据一致性、稳定性和可靠性要求高的场景,性能、吞吐量一般
RabbitMQ:
优点:
支持消息路由:通过交换器支持不同种类的消息路由
延时低:
缺点:因为实现机制比较重,导致吞吐量比较低,不支持消息有序,
● 信道(Channel) :消息读写等操作在信道中进行,客户端可以建立多个信道,每个信道代表一个会话任务。
● 交换器(Exchange) :接收消息,按照路由规则将消息路由到一个或者多个队列;如果路由不到,或者返回给生产者,或者直接丢弃。
因为消息生产者并不指定投递到哪个队列,投递工作是交给交换器来做的。
● 路由键(RoutingKey) :生产者将消息发送给交换器的时候,会发送一个 RoutingKey,用来指定路由规则,这样交换器就知道把消息发送到哪个队列。
● 绑定(Binding) :交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个 RoutingKey。
首先,对于发消息并广播给多个消费者这种情况,RabbitMQ 会为每个消费者建立一个对应的队列。也就是说,如果有 10 个消费者,RabbitMQ 会建立 10 个对应的队列。然后,当一条消息被发出后,RabbitMQ 会把这条消息复制 10 份放到这 10 个队列里。
但是单个消费者里如果开多线程进行消费,RabbitMQ 在官方文档中声明了自己是不保证多线程消费同一个队列的消息的顺序性。而不保证的原因,是因为多线程时,当一个线程消费消息报错的时候,RabbitMQ 会把消费失败的消息再入队,此时就可能出现乱序的情况。
缺点:
(1)为了是实现发布订阅功能,会开多个队列,然后队列内的内容是复制的,比较耗性能
(2)多线程下不能保证消费的顺序性
(3)大量的消息都集中在一个队列里限制吞吐量,因为它不像kafka和RocketMQ那样可以分区或者分队列存放一个topic下的消息。
Kafka VS RabbitMQ:一个吞吐量大,一个延时低
(1)消息匹配分发比较方便,通过在消息中指定routing_key,然后通过交换机实现消息匹配分发,但是kafka做消息匹配的成本很高,消费者端拉消息都是直接全部拉下来,在本地区进行匹配。
(2)实现延时队列:给每一条消息自带TTL字段,带有TTL消息先发给交换机,等到了延时时间之后再把消息放到死信队列里面,实现方便,但是kafka的延时队列实现比较麻烦,先把消息放到一个临时的topic里面,然后要一个中间消费者从临时topic中拿信息持久化到磁盘,用时间轮算法,在定时时间到了之后放到另一个topic里面去被消费者消费。
(3)RabbitMQ的消息消费完就删除了,kafka的消息会被持久化到日志文件里面,可以进行消息重放。
(4)RabbitMQ当消息消费失败的时候会移动到队首或者死信队列,重新消费。但是Kafka的消息出现问题之后会停止消费,因为它不允许出现消息空洞现象。所以kafka对于数据要求精度高的场景可以用,但是精度不高的场景难以维护。
(5)kafka吞吐量高适用于实时数据采集、日志采集等场景,RabbitMQ延时低。
Kafka VS RocketMQ:一个吞吐量大、一个可靠性高
rocketmq提供了死信队列,事务消息,以及更好的顺序消息支持,而kafka支持更大的吞吐量。所以kafka适合数据量大的流处理场景,而rocketmq适合对可靠传递要求高的场景
(1)单机同步发送的场景下,Kafka>RocketMQ,Kafka的吞吐量高达17.3w/s,RocketMQ吞吐量在11.6w/s。
(2)RocketMQ可靠性高,使用消息双写机制(同步复制、异步复制),但是kafka多副本备份可能丢数据。
(3)RocketMQ支持事务消息(两阶段提交协议,先发预备消息,收到消费者的确认消息之后才提交事务),kafka也有事务消息,但是要求语义精确,实现起来比较复杂。
(2)kafka的topic的partition过多的时候性能会下滑,因为kafka是一个分区一个文件的存储,topic增加会导致分区增加,这个时候持久化的时候性能低。但是RocketMQ的一个topic下的所有队列都保存在一个文件中,这样性能高。
消息队列的选型:
主要需要从业务需求来考量,明确消息队列的功能,例如:是否支持延时消息、死信队列、事务消息等功能,项目的数据量大不大,是否需要高吞吐量和持久性,最后还要考虑社区活跃度和部署成本等因素。
1、MQ实现定时任务
答:
RocketMQ相比于另外两个消息队列另一优势就是支持定时/延时消息,但此处的定时只支持18个等级的延时信息:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
Message到达MQ后根据标志位判断是否为延时消息,如果是延时消息建立CommitLog的索引(offset+size+tagHash),并将Message中的原本的topic与queueId封装到properties字段内,然后将俩字段改成延时队列的topic与queueid投入到延时队列专属的MessageQueue(这里只存储索引),根据18个延时等级分成18个Queue。
定时调度器ScheduledMessageService为每个Queue生成一个Task去判断各自负责的Queue里面的信息是否到点了。如果到点了则去CommitLog里面恢复原来的topic和queueid,投放到征程的ComsumerQueue里面去被消费。
定时器又有两种:
(1)基于Timer定时器,每一个任务都分配一个定时器,但是这样当任务数量变多的时候会造成资源浪费。
(2)时间轮算法。
单层时间轮:用一个类似于环形队列,队列上每一个元素是一个slot(槽),每一个槽里存放的是一个任务队列,新来的定时任务根据延时的时间存放进对应槽的任务队列里。维护一个指针来顺时针遍历这些槽,当遍历到这个槽的时候就执行这个任务队列里的任务。
缺点:当定时精度高,周期长的时候,槽数会变多,浪费内存,也会导致效率低下。
多层时间轮:多个环形队列嵌套,例如最外层是时(24个槽),里面是分(60个槽),最里面是秒(60个槽)
2、MQ重试机制:
死信队列:消息发送给消费者消费失败后,经过最大次数重试(最后间隔2h)依然失败就会被放入死信队列,不会再正常投递,此时需要人为处理。
生产者端重试:代码里面设置一下重试次数,重试失败了就加队列存储,一般出问题了就得检查MQ
消费者端重试:异常重试:复用了延时队列,后续会重发消息
超时重试:一定时间内没收到答复重发消息
3、MQ顺序消费
生产者:重写select方法,把消息同步发送到固定的MessageQueue
消费者:选择顺序消费模式,然后拿到三把锁:Brocker的MessageQueue(只发给一个消费者)、本地的MessageQueue(消费者中只有一个线程消费)、ProcessQueue(重平衡过程中防止重复消费)
4、MQ消费模分发模式:广播模式(对所有集群的消费者发消息,确保最少被消费依次)、集群模式(只对一个集群发)
3、MQ部署模式:
单Master(可以有多Slave)、多Master(无多Slave)、多Master多Slave
4、如何确保消息不丢失:
生产者:可以采用同步发送(发送消息后阻塞,等待Broker响应),异步发送(重写Onsuccess和OnException方法响应生产者),做延时队列进行补偿。
Brocker:
消息默认保存在内存中,后续再刷盘,默认是异步刷盘,这样会丢消息。所以要保证消息不丢失就同步刷盘。
集群部署,一主多从,采用异步复制可能在主节点挂了丢消息,可以同步复制。
消费者:拿到Brocker消息后,需要给Brocker响应。
5、为什么项目里面调额行为和入账总会用MQ进行解耦
答:一、提升接口响应速度,二、流量控制和削峰填谷
如何保证消息不丢失:
生产者:消息发送分为同步和异步;
同步发送消息会同步阻塞等待Broker返回结果,但此时Broker中的消息保存在内存上,还需要设置同步刷盘才能保证消息的可靠性。
异步发送消息就需要消息生产者重写sendcallback中的onSuccess和onException两个回调方法来给Broker进行消息确认。
消息队列:
消息队列要做集群部署,例如用单Master多Slave(一主多从),并且主从进行同步复制。
消费者:
处理完消息之后返回自动提交消费位移(ACK)。在Spring框架下,消费者这个类只需要实现RocketMQListener,重写onmessage方法即可,MQ客户端会自动返回ACK,如果Broker一定时间内没有响应会重试。
如何保证消息不重复消费:
如何处理消息堆积问题:
生产者:
降低消息生产速度
消息队列:
增加topic下队列数提升消费的并发度
调整消费参数,例如:消息拉取时间间隔
消费者:
增加消费者数量
采用线程池、本地消息存储等方式提升消费速度
如何保证消息顺序性:
生产者:send方法中传入MessageQueueSelector(),其中要重写select方法来指定消息路由规则,例如使用key对queue数量取模,固定发到一个queue里,并且还需要设置同步发送。
消费者:
对Broker、MeaasgeQueue、ProcessQueue上锁。Broker上锁确保只会将消息投递到指定消费者;MeaageQueue加锁确保只有一个线程能消费这些消息;ProcessQueue确保重平衡时不会重复消费;
需要对ProcessQueue加锁是防止重平衡后,此前的消息消费者的offset位点信息还没提交,新来的线程来重复消费。所以新分配到的消费者需要对ProcessQueue加锁,如果加锁成功说明当前没有线程消费消息。
零拷贝:
传统的从磁盘读取数据,发送数据到客户端,存在四次状态切换与四次拷贝。
用户态与内核态、CPU拷贝与DMA拷贝
mmap:用于文件读取,在进程的虚拟地址空间创建映射,将内核缓冲区映射到进程的虚拟地址,相当于进程虚拟地址指向内核缓冲区的物理页,用户态的进程只是通过一个指令级借口将虚拟地址传入,最终得到内核页缓存中的数据,其底层是通过CPU查询TLB缓存或者页表(存在DRAM里)得到物理地址,然后将物理地址交给总线做数据读取。
所以mmap相比传统read系统调用指令,省去一次CPU拷贝。只是需要将虚拟地址传入接口,最后得到数据。
sendfile:用户网络文件传输,DMA将数据拷贝到内核缓冲区,用户态切换为内核态;然后将内核缓冲区数据CPU拷贝到Socket缓冲区;再通过DMA将Socket缓冲区数据拷贝到网卡,最后内核态切换回用户态。
sendfile+SG-DMA:在Linux4.2之后且网卡支持SG-DMA,相比sendfile,会将数据的描述符与数据长度拷贝到Socket缓冲区,然后SG-DMA将内核缓冲区数据拷贝到网卡。这样久只有两次数据拷贝、两次状态切换。
在此过程中CPU不参与数据拷贝,全靠DMA进行拷贝。只适用于纯读取发送与副本同步场景,就是一个极致的搬运工,不能优化数据变更的场景。
浙公网安备 33010602011771号