前言
日常的技术设计与开发中,我们经常使用到分布式消息队列RocketMQ,用来解耦、异步、削峰填谷等等。RocketMQ使用起来非常简单,尤其是使用阿里云的或是经过封装的client,便捷的同时又可以保证可用性和消息的可靠性。
使用RocketMQ过程中,大家可能经常会想一些问题,比如RocketMQ是如何保证消息的即时性?如何保证消息不丢失?如何避免消息重复消费?(分区)顺序消息是如何实现的?如何实现的负载均衡?等等问题。如果你对这些问题也有疑问,并有兴趣了解其基本的原理。那可以接着看下去,希望能够有所收获。
本文就通过介绍RocketMQ中消息的生产和消费的过程来回答以上几个问题。之前分享过RocketMQ的各个组件和其之间的关系,阅读之前希望你在使用过RocketMQ的同时,已经了解如Producer、Consumer、Broker、Master、Slave、Topic、CommitLog、ConsumeQueue等等RocketMQ中的基本组件或概念。
开始介绍之前,可以先回忆下关于RocketMQ消息的生产和消费的一张经典的图,下面的介绍也将围绕该图展开。
消息的生产
消息在Producer
回忆一下我们在使用RocketMQ的Producer进行消息生产时的步骤:
- 设置RocketMQ的配置,如NameServer地址等,启动Producer
- 构造消息内容,调用Producer的发消息的方法
看起来,非常得简单。那么,RcoketMQ在这中间做了什么呢,我们一步一步看。
Producer的启动
首先,启动Producer,需要设置NameServer的地址,Producer在生产消息最终要做的事情是将消息根据Topic等发送到Broker,NameServer的作用便是让Producer发现Broker、Topic信息。Producer启动后,将与NameServer集群的一台机器建立长连接,定时从NameServer上拉取最新的Broker信息、Topic信息、Queue信息,做好发送消息的准备。
Producer发消息的过程
我们封装好消息体,调用启动了的producer的发送方法,需要传入消息发送到的Topic、消息过滤使用的Tag。Producer所做的步骤大致如下:
- Producer会根据Topic找到从NameServer获取到的Topic和Queue信息,找到这条消息可以发送到的Queue的集合(因为一个Topic可以在多个Broker上分别有多个Queue)
- 根据Queue集合,以某种策略确定需要发送到的Queue,这一步策略的选择可以实现消息生产的负载均衡(让Producer集群中不同的机器均衡的向不同的Queue生产消息),也可以自定义Queue的选择策略实现全局顺序消息(所有Producer都将消息固定发送到一个Queue)或分区顺序消息(根据分区将属于一个分区的消息全部发送到一个特定Queue)
- Producer会根据这个Queue所在的Broker,选择与对应Broker之间的长连接将消息内容传输到Broker端(通过netty进行发送消息的远程调用操作)
消息在Broker
Broker是RocketMQ在Server端的核心组件,生产的消息都要存储在Broker端,最终再被消费。所有消息通过Producer端发送,经过网络传输,会在Broker端经过一系列存储和分发的过程。
日常开发过程中,我们一般不会直接和Broker打交道,但为了了解RocketMQ内部的核心原理,Broker我们不可能绕过去。
Broker的启动
Broker启动时像Producer启动一样需要设置NameServer集群地址,Broker启动后会将自身注册到NameServer集群上以供Producer和Consumer发现。每台Broker机器都将和所有NameServer机器保持长连接,并定时发送心跳。
消息需要发送前会创建Topic,需要指定Topic要创建在那些Broker上,需要创建多少Queue(队列)。Broker收到该请求后便会存储对应的Topic信息,并创建对应的Queue队列文件和目录,准备进行消息的存储和分发到不同的Queue。
Broker存储消息
Producer通过与Broker之间的长连接传输要发送的消息,Broker收到消息后首先,向文章开始时的那张图那样,将消息存储到CommitLog文件,然后根据消息对应具体的Topic和QueueId,将消息的概要信息(在CommitLog中的Offset、Size、tagHashCode)存储到对应的ConsumeQueue文件中。
Broker保证不丢失消息
Broker存储消息时,虽然最终向上面介绍的那样都需要持久化到CommitLog文件,但在接收到一个生产消息的请求时可以选择多种策略决定什么时候返回发送成功,如:
- 同步/异步刷盘;同步刷盘能够保证Master硬盘不损坏的情况消息不丢,但每条消息发送都需要持久化,会较大影响性能。一般我们会选择异步刷盘。
- 同步/异步复制到Slave;消息必须由Master同步给Slave后才返回发送成功,保证高可靠。一般我们会选择同步复制。
生产环境,推荐做法是采用异步刷盘保证效率,同时使用同步复制到Slave的方式,只要Master和Slave不同时宕机,便可以保证发送消息高效的同时不在Broker端丢失消息。
消息的消费
Consumer的启动
同样,回忆一下我们平常如何使用Consumer进行消息的订阅和消费。
首先,类似Producer,需要设置NameServer等配置,不同的是,需要指定消费组Group和订阅的消息的Topic和过滤Tag。
Consumer消费消息的步骤大致如下:
- 与Producer类似,Consumer启动后会去NameServer拉取所订阅Topic的Queue信息
- 确定自己需要从哪些Queue消费消息,在这一步会做消费端的负载均衡,Consumer同样可以集群部署,同一个Group下的不同Consumer会根据策略消费不同ConsumeQueue的消息,但是一个ConsumeQueue只能被一个Consumer消费,负载均衡的粒度是ConsumeQueue。
- 与确定好需要消费的ConsumeQueue对应的Broker建立长连接,进行消息的拉取和消费
消息的拉取与消费
Consumer消费消息时,采用的是主动拉取消息的方式。
虽然RocketMQ提供了PushConsumer这样的实现类用于订阅消费消息,但是内部实现依然采用的是主动Pull的方式拉取消息,不过在实现的背后根据策略使用Long Poll长轮询的方式去pull,让用户体验就像push消息的方式,这样的方式兼顾了两点:
- 既保证了消息消费的即时性,只要Consumer消费能力足够,新消息便能被即时拉到。
- 也保证主动权在Consumer手中,不至于因为消费能力小于生产能力的情况导致大量消息堆积在某个Consumer。
Consumer拉取消息及消费消息的步骤大致如下:
- 使用与Broker的长连接,底层netty发起远程调用,指令是拉取消息,参数包括QueueId,Offset(消息在Queue中的偏移量)等;注意拉取消息时,Consumer会判断当前已拉取未消费的消息量等等来决定是否应该立即拉取。
- Broker收到Consumer拉取消息的请求,根据Queue和Offset确定是否有新的消息,如果没有执行3,如果有执行4
- 没有新的消息,Broker会hold住这个拉取消息的请求一段时间,如果这段时间有新的消息,执行4,如果没有,这段时间之后返回没有消息的响应,Consumer会再次发起一个拉取消息的请求,执行1
- 在Queue的参数Offset后有新的消息,Broker会将一定量的消息返回给Consumer,并带上下次拉取消息的Offset:nextBeginOffset
- Consumer拉取消息后,对于一个ConsumeQueue的消息会存储在一个TreeMap中,key为该条消息在ConsumeQueue中的Offset,value为该条消息的内容
- Consumer有一个消费消息使用的线程池,消息可以并发进行消费,以提高消费效率。对于顺序消息,消费时使用锁控制其串行消费。线程池从TreeMap中取消息(并不移除),执行我们写的消息消费业务代码
- 消息消费完成(无论成功还是失败),将这条消息对应的Offset和Value从TreeMap中移除,同时判断这个Offset是否在TreeMap中最小,如果最小,说明这个Offset之前的消息全部消费完成,会将这个队列的ConsumeOffset更新
- 如果消息消费完成但是消费失败且需要重试,Offset同样会更新,但是Consumer会将消息重新投递到一个重试队列中,以简化Offset的维护过程
- OffsetTable,使用OffsetStore类进行存储,消费过程中存储在Consumer的内存中,支持持久化到Broker,定时同步,同时在Consumer正常关闭的情况下会进行一次同步。
大致理解整个过程并不算复杂,可以发现,只要消息生产成功,存储在了Broker,就算Consumer处于故障状态,最终启动完成后通过之前消费到的Offset也能成功消费到所有未消费的消息;即使Consumer消费某条消息过程中宕机了,再次启动也会根据之前的Offset消费到未消费完成的消息,保证了消息不在Broker和Consumer之间丢失。
RocketMQ采用了nextBeginOffset的方式来防至消息的重复拉取和消费,为了支持消息消费失败重试且不影响Offset的提交,使用了一个单独的重试队列。
同时,我们需要注意,Offset最初只存储在Consumer的内存中,向Broker进行同步,如果消息消费完成,Offset没有同步到Broker,Consumer异常退出,为了消息不丢失,Consumer会使用Broker中的Offset进行再次消费,这样便可能出现重复消费,所以消息消费的逻辑应该保证幂等。
总结
- 首先提出了几个RocketMQ使用过程中想要了解的几个问题
- 分别从Producer和Broker角度介绍了消息的生产过程
- 分别从Consumer和Broker角度介绍了消息的消费过程
- 在介绍消息的生产和消费过程中穿插简要回答了前言中提出的几个问题
参考
《RocketMQ实战原理与解析》杨开元
Offset管理:www.jianshu.com/p/b4970f59a…