RocketMQ各部分角色
解析:
RocketMQ由四部分组成,先来直观地了解一下这些角色以及各自的功能。分布式消息队列是用来高效地传输消息的,它的功能和现实生活中的邮局收发信件很类似,我们类比地说一下相应的模块。现实生活中的邮政系统要正常运行,离不开下面这四个角色,一是发信者,二是收信者,三是负责暂存、传输的邮局,四是负责协调各个地方邮局的管理机构。
对应到RocketMQ中,这四个角色就是Producer、Consumer、Broker和NameServer。启动RocketMQ的顺序是先启动NameServer,再启动Broker,这时候消息队列已经可以提供服务了,想发送消息就使用Producer来发送,想接收消息就使用Consumer来接收。很多应用程序既要发送,又要接收,可以启动多个Producer和Consumer来发送多种消息,同时接收多种消息。为了消除单点故障,增加可靠性或增大吞吐量,可以在多台机器上部署多个NameServer和Broker,为每个Broker部署一个或多个Slave。
了解了四种角色以后,再介绍一下Topic和Message Queue这两个名词。一个分布式消息队列中间件部署好以后,可以给很多个业务提供服务,同一个业务也有不同类型的消息要投递,这些不同类型的消息以不同的Topic名称来区分。所以发送和接收消息前,先创建Topic,针对某个Topic发送和接收消息。有了Topic以后,还需要解决性能问题。如果一个Topic要发送和接收的数据量非常大,需要能支持增加并行处理的机器来提高处理速度,这时候一个Topic可以根据需求设置一个或多个Message Queue, Message Queue类似分区或Partition。Topic有了多个Message Queue后,消息可以并行地向各个Message Queue发送,消费者也可以并行地从多个Message Queue读取消息并消费。
答案:
1)创建Topic的代码是在org.apache.rocketmq.tools.command.topic里的UpdateTopicSubCommand类中,创建Topic的命令是updateTopic
2)其中b和c参数比较重要,而且他们俩只有一个会起作用(-b优先), b参数指定在哪个Broker上创建本Topic的Message Queue, c参数表示在这个Cluster下面所有的Master Broker上创建这个Topic的Message Queue,从而达到高可用性的目的。具体的创建动作是通过发送命令触发的
3)创建Topic的命令被发往对应的Broker, Broker接到创建Topic的请求后,执行具体的创建逻辑
4)注意最后一步是向NameServer发送注册信息,NameServer完成创建Topic的逻辑后,其他客户端才能发现新增的Topic,相关逻辑在org.apache.rocketmq.namesrv.routeinfo的RouteInfoManager类中的registerBroker函数里,首先更新Broker信息,然后对每个Master角色的Broker,创建一个QueueData对象。如果是新建Topic,就是添加QueueData对象;如果是修改Topic,就是把旧的QueueData删除,加入新的QueueData。
RocketMQ如何保证高可用
答案:
1)master和slave 配合,master 支持读、写,slave 只读,producer 只能和 master 连接写入消息,consumer 可以连接 master 和 slave。
2)当 master 不可用或者繁忙时,consumer 会被自动切换到 slave 读。即使 master 出现故障,consumer 仍然可以从 slave 读消息,不受影响。
3)创建 topic 时,把 message queue 创建在多个 broker 组上(brokerName 一样,brokerId 不同),当一个 broker 组的 master 不可用后,其他组的
RocketMq消费者消费模式有几种
答案:
1)集群消费:一条消息只会投递到一个 Consumer Group 下面的一个实例。
2)广播消费:消息将对一个Consumer Group 下的各个 Consumer 实例都投递一遍。即使这些 Consumer 属于同一个Consumer Group ,消息也会被 Consumer Group 中的每个 Consumer 都消费一次。
RocketMQ保证消息有序
解析:
1)全局顺序消息:
RocketMQ在默认情况下不保证顺序,比如创建一个Topic,默认八个写队列,八个读队列。这时候一条消息可能被写入任意一个队列里;在数据的读取过程中,可能有多个Consumer,每个Consumer也可能启动多个线程并行处理,所以消息被哪个Consumer消费,被消费的顺序和写入的顺序是否一致是不确定的。要保证全局顺序消息,需要先把Topic的读写队列数设置为一,然后Producer和Consumer的并发设置也要是一。简单来说,为了保证整个Topic的全局消息有序,只能消除所有的并发处理,各部分都设置成单线程处理。这时高并发、高吞吐量的功能完全用不上了。在实际应用中,更多的是像订单类消息那样,只需要部分有序即可。在这种情况下,我们经过合适的配置,依然可以利用RocketMQ高并发、高吞吐量的能力。
2)部分顺序消息:
要保证部分消息有序,需要发送端和消费端配合处理。在发送端,要做到把同一业务ID的消息发送到同一个Message Queue;在消费过程中,要做到从同一个Message Queue读取的消息不被并发处理,这样才能达到部分有序。消费端通过使用MessageListenerOrderly类来解决单Message Queue的消息被并发处理的问题。Consumer使用MessageListenerOrderly的时候,下面四个Consumer的设置依旧可以使用:setConsumeThreadMin、setConsumeThreadMax、setPullBatchSize、setConsumeMessageBatchMaxSize。前两个参数设置Consumer的线程数,PullBatchSize指的是一次从Broker的一个Message Queue获取消息的最大数量,默认值是32, ConsumeMessageBatchMaxSize指的是这个Consumer的Executor(也就是调用MessageListener处理的地方)一次传入的消息数(List msgs这个链表的最大长度),默认值是1。上述四个参数可以使用,说明MessageListenerOrderly并不是简单地禁止并发处理。在MessageListenerOrderly的实现中,为每个Consumer Queue加个锁,消费每个消息前,需要先获得这个消息对应的Consumer Queue所对应的锁,这样保证了同一时间,同一个Consumer Queue的消息不被并发消费,但不同Consumer Queue的消息可以并发处理。
答案:
一个topic下有多个queue,为了保证发送有序,rocketmq提供了MessageQueueSelector队列选择机制
1)可使用hash取模法,让同一个订单发送到同一个queue中,再使用同步发送,只有消息A发送成功,再发送消息B
2)rocketmq的topic内的队列机制,可以保证存储满足FIFO,剩下的只需要消费者顺序消费即可
3)rocketmq仅保证顺序发送,顺序消费由消费者业务保证
RocketMQ事务消息的实现机制
解析:
RocketMQ采用两阶段提交的方式实现事务消息,TransactionMQProducer处理上面情况的流程是,先发一个“准备从B银行账户增加一万元”的消息,发送成功后做从A银行账户扣除一万元的操作,根据操作结果是否成功,确定之前的“准备从B银行账户增加一万元”的消息是做commit还是rollback,
具体流程如下:
1)发送方向RocketMQ发送“待确认”消息。
2)RocketMQ将收到的“待确认”消息持久化成功后,向发送方回复消息已经发送成功,此时第一阶段消息发送完成。
3)发送方开始执行本地事件逻辑。
4)发送方根据本地事件执行结果向RocketMQ发送二次确认(Commit或是Rollback)消息,RocketMQ收到Commit状态则将第一阶段消息标记为可投递,订阅方将能够收到该消息;收到Rollback状态则删除第一阶段的消息,订阅方接收不到该消息。
5)如果出现异常情况,步骤4)提交的二次确认最终未到达RocketMQ,服务器在经过固定时间段后将对“待确认”消息发起回查请求。
6)发送方收到消息回查请求后(如果发送一阶段消息的Producer不能工作,回查请求将被发送到和Producer在同一个Group里的其他Producer),通过检查对应消息的本地事件执行结果返回Commit或Roolback状态。
7)RocketMQ收到回查请求后,按照步骤4)的逻辑处理。上面的逻辑似乎很好地实现了事务消息功能,它也是RocketMQ之前的版本实现事务消息的逻辑。但是因为RocketMQ依赖将数据顺序写到磁盘这个特征来提高性能,步骤4)却需要更改第一阶段消息的状态,这样会造成磁盘Catch的脏页过多,降低系统的性能。所以RocketMQ在4.x的版本中将这部分功能去除。系统中的一些上层Class都还在,用户可以根据实际需求实现自己的事务功能。客户端有三个类来支持用户实现事务消息,第一个类是LocalTransactionExecuter,用来实例化步骤3)的逻辑,根据情况返回LocalTransactionState. ROLLBACK_MESSAGE或者LocalTransactionState.COMMIT_MESSAGE状态。第二个类是TransactionMQProducer,它的用法和DefaultMQProducer类似,要通过它启动一个Producer并发消息,但是比DefaultMQProducer多设置本地事务处理函数和回查状态函数。第三个类是TransactionCheckListener,实现步骤5)中MQ服务器的回查请求,返回LocalTransactionState.ROLLBACK_MESSAGE或者LocalTransactionState.COMMIT_MESSAGE。
答案:
1)RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址
2)RocketMQ第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改消息的状态。
3)RocketMQ会定期扫描消息集群中的事务消息,如果发现了Prepared消息,它会向消息发送端(生产者)确认,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
RocketMQ的重复消费问题
解析:
对分布式消息队列来说,同时做到确保一定投递和不重复投递是很难的,也就是所谓的“有且仅有一次”。在鱼和熊掌不可兼得的情况下,RocketMQ选择了确保一定投递,保证消息不丢失,但有可能造成消息重复。消息重复一般情况下不会发生,但是如果消息量大,网络有波动,消息重复就是个大概率事件。比如Producer有个函数setRetryTimesWhenSendFailed,设置在同步方式下自动重试的次数,默认值是2,这样当第一次发送消息时,Broker端接收到了消息但是没有正确返回发送成功的状态,就造成了消息重复。
答案:
解决消息重复有两种方法:第一种方法是保证消费逻辑的幂等性(多次调用和一次调用效果相同);
另一种方法是维护一个已消费消息的记录,消费前查询这个消息是否被消费过。这两种方法都需要使用者自己实现。
RocketMQ延迟消息?如何实现的?
解析:
RocketMQ支持发送延迟消息,Broker收到这类消息后,延迟一段时间再处理,使消息在规定的一段时间后生效。延迟消息的使用方法是在创建Message对象时,调用setDelayTimeLevel(intlevel)方法设置延迟时间,然后再把这个消息发送出去。目前延迟的时间不支持任意设置,仅支持预设值的时间长度(1s/5s/10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h)。比如setDelayTimeLevel(3)表示延迟10s。
答案:
同解析内容
RocketMQ的消费类型
解析:
根据使用者对读取操作的控制情况,消费者可分为两种类型。一个是DefaultMQPushConsumer,由系统控制读取操作,收到消息后自动调用传入的处理方法来处理;另一个是DefaultMQPullConsumer,读取操作中的大部分功能由使用者自主控制。
1)DefaultMQPushConsumer的使用
使用DefaultMQPushConsumer主要是设置好各种参数和传入处理消息的函数。系统收到消息后自动调用处理函数来处理消息,自动保存Offset,而且加入新的DefaultMQPushConsumer后会自动做负载均衡。
DefaultMQPushConsumer需要设置三个参数:一是这个Consumer的GroupName,二是NameServer的地址和端口号,三是Topic的名称。
2)DefaultMQPushConsumer的处理流程
DefaultMQPushConsumer主要功能实现在DefaultMQPushConsumerImpl类中,消息的处理逻辑是在pullMessage这个函数里的PullCallBack中。在PullCallBack函数里有个switch语句,根据从Broker返回的消息类型做相应的处理。
DefaultMQPushConsuer的源码中有很多PullRequest语句,比如DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest)。为什么“PushConsumer”中使用“PullRequest”呢?这是通过“长轮询”方式达到Push效果的方法,长轮询方式既有Pull的优点,又兼具Push方式的实时性。
Push方式是Server端接收到消息后,主动把消息推送给Client端,实时性高。对于一个提供队列服务的Server来说,用Push方式主动推送有很多弊端:首先是加大Server端的工作量,进而影响Server的性能;其次,Client的处理能力各不相同,Client的状态不受Server控制,如果Client不能及时处理Server推送过来的消息,会造成各种潜在问题。Pull方式是Client端循环地从Server端拉取消息,主动权在Client手里,自己拉取到一定量消息后,处理妥当了再接着取。Pull方式的问题是循环拉取消息的间隔不好设定,间隔太短就处在一个“忙等”的状态,浪费资源;每个Pull的时间间隔太长,Server端有消息到来时,有可能没有被及时处理。“长轮询”方式通过Client端和Server端的配合,达到既拥有Pull的优点,又能达到保证实时性的目的。
源码中有这一行设置语句requestHeader.setSuspendTimeoutMillis(brokerSus-pendMaxTimeMillis),作用是设置Broker最长阻塞时间,默认设置是15秒,注意是Broker在没有新消息的时候才阻塞,有消息会立刻返回。
从Broker的源码中可以看出,服务端接到新消息请求后,如果队列里没有新消息,并不急于返回,通过一个循环不断查看状态,每次waitForRunning一段时间(默认是5秒),然后后再Check。默认情况下当Broker一直没有新消息,第三次Check的时候,等待时间超过Request里面的BrokerSuspendMaxTimeMillis,就返回空结果。在等待的过程中,Broker收到了新的消息后会直接调用notifyMessageArriving函数返回请求结果。“长轮询”的核心是,Broker端HOLD住客户端过来的请求一小段时间,在这个时间内有新消息到达,就利用现有的连接立刻返回消息给Consumer。“长轮询”的主动权还是掌握在Consumer手中,Broker即使有大量消息积压,也不会主动推送给Consumer。长轮询方式的局限性,是在HOLD住Consumer请求的时候需要占用资源,它适合用在消息队列这种客户端连接数可控的场景中。
3)DefaultMQPushConsumer的流量控制
PushConsumer的核心还是Pull方式,所以采用这种方式的客户端能够根据自身的处理速度调整获取消息的操作速度。因为采用多线程处理方式实现,流量控制的方面比单线程要复杂得多。PushConsumer有个线程池,消息处理逻辑在各个线程里同时执行。
Pull获得的消息,如果直接提交到线程池里执行,很难监控和控制,比如,如何得知当前消息堆积的数量?如何重复处理某些消息?如何延迟处理某些消息?RocketMQ定义了一个快照类ProcessQueue来解决这些问题,在PushConsumer运行的时候,每个Message Queue都会有个对应的ProcessQueue对象,保存了这个Message Queue消息处理状态的快照。ProcessQueue对象里主要的内容是一个TreeMap和一个读写锁。TreeMap里以Message Queue的Offset作为Key,以消息内容的引用为Value,保存了所有从MessageQueue获取到,但是还未被处理的消息;读写锁控制着多个线程对TreeMap对象的并发访问。有了ProcessQueue对象,流量控制就方便和灵活多了,客户端在每次Pull请求前会做三个判断来控制流量。会判断获取但还未处理的消息个数、消息总大小、Offset的跨度,任何一个值超过设定的大小就隔一段时间再拉取消息,从而达到流量控制的目的。此外ProcessQueue还可以辅助实现顺序消费的逻辑。
4)DefaultMQPullConsumer
使用DefaultMQPullConsumer像使用DefaultMQPushConsumer一样需要设置各种参数,写处理消息的函数,同时还需要做额外的事情。
主要处理额外的三件事情:
(1)获取Message Queue并遍历一个Topic包括多个Message Queue,如果这个Consumer需要获取Topic下所有的消息,就要遍历多有的Message Queue。如果有特殊情况,也可以选择某些特定的Message Queue来读取消息。
(2)维护Offsetstore从一个Message Queue里拉取消息的时候,要传入Offset参数(long类型的值),随着不断读取消息,Offset会不断增长。这个时候由用户负责把Offset存储下来,根据具体情况可以存到内存里、写到磁盘或者数据库里等。
(3)根据不同的消息状态做不同的处理拉取消息的请求发出后,会返回:FOUND、NO_MATCHED_MSG、NO_NEW_MSG、OFFSET_ILLEGAL四种状态,需要根据每个状态做不同的处理。比较重要的两个状态是FOUNT和NO_NEW_MSG,分别表示获取到消息和没有新的消息。
实际情况中可以把while(true)放到外层,达到无限循环的目的。因为PullConsumer需要用户自己处理遍历Message Queue、保存Offset,所以PullConsumer有更多的自主性和灵活性。
答案:
rocketmq不管是推模式还是拉模式底层都是拉模式,推模式也是在拉模式上做了一层封装.。
消息存储在broker中,通过topic和tags区分消息队列。producer在发送消息时不关心consumer对应的topic和tags,只将消息发送到对应broker的对应topic和tags中。
推模式中broker则需要知道哪些consumer拥有哪些topic和tags,但在consumer重启或更换topic时,broker无法及时获取信息,可能将消息推送到旧的consumer中。对应consumer主动获取topic,这样确保每次主动获取时他对应的topic信息都是最新的。
RocketMQ消息积压相关
解析:
(1)提高消费并行度在同一个ConsumerGroup下(Clustering方式),可以通过增加Consumer实例的数量来提高并行度,通过加机器,或者在已有机器中启动多个Consumer进程都可以增加Consumer实例数。注意总的Consumer数量不要超过Topic下ReadQueue数量,超过的Consumer实例接收不到消息。此外,通过提高单个Consumer实例中的并行处理的线程数,可以在同一个Consumer内增加并行度来提高吞吐量(设置方法是修改consumeThreadMin和consumeThreadMax)。
(2)以批量方式进行消费某些业务场景下,多条消息同时处理的时间会大大小于逐个处理的时间总和,比如消费消息中涉及update某个数据库,一次update10条的时间会大大小于十次update1条数据的时间。这时可以通过批量方式消费来提高消费的吞吐量。实现方法是设置Consumer的consumeMessageBatchMaxSize这个参数,默认是1,如果设置为N,在消息多的时候每次收到的是个长度为N的消息链表。
(3)检测延时情况,跳过非重要消息Consumer在消费的过程中,如果发现由于某种原因发生严重的消息堆积,短时间无法消除堆积,这个时候可以选择丢弃不重要的消息,使Consumer尽快追上Producer的进度。
答案:
1)提高消费并行读
同一个Consumer Group下,通过增加Consumer实例的数量来提高并行度,超过订阅队列数的Consumer实例无效。
提高单个Consumer的消费并行线程,通过修改Consumer的consumerThreadMin和consumerThreadMax来设置线程数。
2)批量方式消费
通过设置Consumer的consumerMessageBathMaxSize这个参数,默认是1,一次只消费一条消息,例如设置N,那么每次消费的消息条数小于等于N
3)丢弃非重要消息
当消息发生堆积时,如果消费速度跟不上生产速度,可以选择丢弃一些不重要的消息
4)优化消息消费的过程
对于消费消息的过程一般包括业务处理以及跟数据库的交互,可以试着通过一些其他的方法优化消费的逻辑。
5)临时解决方案:
新建一个topic,写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的queue中。临时用一部分机器来部署consumer,每一批consumer消费一个临时queue的数据。等快速消费完积压数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息。
RocketMQ刷盘相关
解析:
RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式。
❑ 异步刷盘方式:在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。
❑ 同步刷盘方式:在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。
同步刷盘还是异步刷盘,是通过Broker配置文件里的flushDiskType参数设置的,这个参数被配置成SYNC_FLUSH、ASYNC_FLUSH中的一个。
答案:
同解析内容
上述问题解析参考自:
答案部分来自此书,部分来自网络整理,后续完善。