RocketMQ5源码(三)SlaveActingMaster模式

前言

本章基于rocketmq5.1.1版本,分析slaveActingMaster模式。

master-slave模式下,如果master下线:

  1. 传统master-slave:slaveReadEnable=true,slave能提供有限的消费能力;
  2. controller模式:将slave自动提升为master;
  3. slaveActingMaster模式:让slave具备更全面的消费能力(代理master);

涉及历史文章:

  1. RocketMQ4源码(一)NameServer
  2. RocketMQ4源码(二)普通消息发送
  3. RocketMQ4源码(三)普通消息消费
  4. RocketMQ4源码(四)生产者特性
  5. RocketMQ4源码(五)消费者特性
  6. RocketMQ4源码(六)HA
  7. RocketMQ5源码(一)POP消费
  8. RocketMQ5源码(二)controller模式

一、引入

1、4.x版本master下线影响

RocketMQ4源码(六)HA最后总结了一下Master下线对于客户端的影响:

  1. 对于producer来说,producer路由无法发现master broker下的队列;
  2. 对于consumer来说,只要slave不下线,依然可以发现所有队列;
  3. consumer可以正常进行rebalance、从slave拉消息、提交offset到slave;
  4. 对于顺序消费,如果在全局锁过期前,master不上线,consumer将无法继续执行本地消费逻辑;
  5. 对于延迟消息,master下线期间无法正常调度到目标队列;
  6. 对于事务消息,master下线期间无法发起回查。如果producer发送half消息成功后master下线,producer发送END_TRANSACTION失败,需要等待master恢复后回查后执行二阶段提交或回滚;

2、SlaveActingMaster不改变什么

producer无法发现下线队列

SlaveActingMaster不会改变slave只读的语义,即master下线,该broker组的队列不可写。

MQClientInstance#topicRouteData2TopicPublishInfo:

以4.6版本为例,client侧producer的路由转换,如果一个broker副本组中没有master,忽略这个queue。

DefaultMQProducerImpl#sendDefaultImpl:

发送消息,如果topic下没有正常上线master broker下的队列,那么将抛出No route info异常。

consumer能『正常』消费

master下线后,slave仍然能提供部分消费能力(slaveReadEnable=true),SlaveActingMaster不会改变这些consumer行为。

下面以4.6为例,列举一下consumer集群模式 +普通并发消费需要用到的broker api。

rebalance-获取consumer组成员

MQClientInstance#findConsumerIdList:在rebalance前需要得知consumer组内所有成员id。

BrokerData#selectBrokerAddr:broker优先取master降级取slave。

因为consumer会向所有有关系的broker实例广播心跳,包括slave,所以相关broker都能拿到当前consumer组成员id集合。

rebalance-初始化消费进度

RebalanceImpl#updateProcessQueueTableInRebalance:

对于新分配给consumer队列,需要从broker查询当前队列的消费进度。

MQClientInstance#findBrokerAddressInAdmin:查消费进度选择broker。

这里是4.6版本,不同版本中,查询消费进度的逻辑稍有不同,但是都不会严格选择master。

pull-拉消息

PullAPIWrapper#pullKernelImpl:consumer拉消息。

MQClientInstance#findBrokerAddressInSubscribe:拉消息也并非只能选master,可降级选slave。

提交消费进度

提交消费进度的入口有很多,比如pull同时提交、rebalance移除queue后提交等。

MQClientInstance#startScheduledTask:这里举最常见的例子,每隔5s定时提交消费进度到broker

RemoteBrokerOffsetStore#persistAll:循环consumer内存的offsetTable中的消费进度。

RemoteBrokerOffsetStore#updateConsumeOffsetToBroker:

定时提交消费进度和查消费进度选择broker的逻辑都是一样的。

3、SlaveActingMaster要解决什么

消费进度回退

虽然master下线后,slave能够提供【部分】消费能力。

但是在master重新上线后,可能发生消费进度回退的问题。

master重新上线后,会触发两个事情:

  1. slave从master同步consumerOffset.json,即master消费进度覆盖slave消费进度;
  2. 容易触发立即rebalance,即master上线,consumer发现master后,向master发送心跳,master感知消费组成员变更,通知组内所有consumer立即rebalance;

如果凑巧,consumer将内存消费进度及时同步给重新上线后的master,也未必会发生消费进度回退。

如果不凑巧,consumer rebalance移除了原来分配给自己的queue,将缓存消费进度提交给slave,而slave恰巧又从master同步了老的消费进度,那么将发生消费进度回退。

总而言之,消费进度是否回退,取决于consumer缓存的消费进度是否能够正常提交到重新上线的master

二级消息消费中断

这里二级消息指的是,需要broker通过定时任务调度的系统消息,比如延迟消息、事务消息、5.xPOP消息。

DefaultMessageStore#handleScheduleMessageService:4.x只有master broker能调度延迟消息。

BrokerController#startProcessorByHa:4.x只有master broker能调度事务消息回查。

队列全局锁不可用

以4.6顺序消费为例。

RebalanceImpl#updateProcessQueueTableInRebalance:

在消费者rebalance时,需要先获取queue所在broker的锁,才能真正分配queue给当前consumer。

ConsumeMessageOrderlyService#start:

每隔20s,consumer会对所有分配给自己的顺序消费队列进行锁续期。

ProcessQueue#isLockExpired:

队列锁超过30s未向broker续期认为过期。在锁过期后,consumer无法执行顺序消费逻辑。

consumer侧,无论是首次分配queue需要上锁,还是定时续期,锁相关api只能请求master broker执行。

二、使用案例

NameServer

nameserver侧需要开启slaveActingMaster支持。

ini 复制代码
supportActingMaster=true

Broker

对于master broker,需要开启slaveActingMaster支持。

ini 复制代码
brokerClusterName = MyDefaultCluster
brokerName = broker-sam
brokerId = 0
brokerRole = ASYNC_MASTER
namesrvAddr = 127.0.0.1:9876
listenPort = 30911
storePathRootDir=/tmp/rmqstore/broker-sam-0
storePathCommitLog=/tmp/rmqstore/broker-sam-0/commitlog
# 开启slaveActingMaster支持
enableSlaveActingMaster=true

对于slave broker,除了开启slaveActingMaster支持,还可以选择开启二级消息远程逃逸。

ini 复制代码
brokerClusterName = MyDefaultCluster
brokerName = broker-sam
brokerId = 3
brokerRole = SLAVE
namesrvAddr = 127.0.0.1:9876
listenPort = 30921
storePathRootDir = /tmp/rmqstore/broker-sam-3
storePathCommitLog = /tmp/rmqstore/broker-sam-3/commitlog
# 开启slave备读
slaveReadEnable=true
# 开启slaveActingMaster支持
enableSlaveActingMaster=true
# 二级消息使用远程逃逸
enableRemoteEscape=true

三、路由管理(NameServer侧)

1、broker注册

broker注册到nameserver会携带:

1)是否开启slaveActingMaster标志;

2)活跃超时时间,默认10s;

RouteInfoManager#registerBroker:nameserver处理broker注册请求。

如果master未上线,允许broker组内最小brokerId的slave设置topic配置,只不过topic会设置为只读。

这保证了只要有slave上线,路由数据就一定存在,而在client端,producer会过滤不可写队列,consumer可以发现这些代理队列。

注:4.x只允许master注册时设置topic配置。

nameserver侧,将broker是否支持slaveActingMaster存储在BrokerData中。

2、broker注销

RouteInfoManager#cleanTopicByUnRegisterRequests:

当broker组内master下线,且有slave在线,同样会将topic下队列设置为只读。

3、topic路由查询

RouteInfoManager#pickupTopicRouteData:nameserver根据topic查询路由。

当满足三个条件时,返回broker组内最小brokerId作为代理master地址

  1. nameserver开启slaveActingMaster;
  2. broker组内master下线;
  3. broker开启slaveActingMaster;

客户端对于这种代理行为无感。

对于consumer来说,是brokerId=0的地址被替换成了一个slave的地址

对于producer来说,由于broker注册和注销的特殊操作,代理master的topic是只读的,不会向这些队列发送消息。

四、轻量级心跳

1、broker发送心跳请求

传统模式下,broker每隔30s发送注册请求到nameserver,注册请求中包含当前broker的topic配置。

slaveActingMaster模式下,需要间隔时间更短的心跳请求来感知broker状态,提出了轻量级心跳。

BrokerController#scheduleSendHeartbeat:

broker确定角色上线后,每隔1s发送轻量级心跳给所有nameserver

BrokerOuterAPI#sendHeartbeat:轻量级心跳请求中只包含broker信息。

RouteInfoManager#updateBrokerInfoUpdateTimestamp:

nameserver侧,在注册请求处理完成后,brokerLiveTable中才存在broker存活情况,所以轻量级心跳实际还是依赖于普通注册请求的。

这里直接更新心跳时间,不需要上锁。

注:普通broker注册注销都需要上一个范围很大的锁。

2、NameServer存活探测

NamesrvController#startScheduleService:

nameserver侧仍然是每隔5s扫描broker存活信息

RouteInfoManager#scanNotActiveBroker:

关键在于心跳超时时间的判定

未开启slaveActingMaster,心跳超时时间是nameserver侧写死的120s;

即broker每隔30s发送注册请求,nameserver每隔5s扫描,如果超过120s没收到broker注册请求,判定为下线。

开启slaveActingMaster后,心跳超时时间由broker注册请求指定,默认brokerNotActiveTimeoutMillis=10s

即broker每隔30s发送注册请求,broker每隔1s发送轻量级心跳,nameserver每隔5s扫描,如果超过10s未收到broker注册或轻量级心跳,判定为下线,执行broker注销逻辑。

五、slave角色变更

1、感知broker组成员变化

由于只有brokerId最小的slave才能成为代理master,所以broker需要感知broker组成员变化。

nameserver推送

RouteInfoManager#notifyMinBrokerIdChanged:

当broker上下线时,nameserver可以感知broker组最小id变化,通知所有broker组成员。

但是默认NamesrvConfig设置notifyMinBrokerIdChanged=false,所以nameserver并不会主动推送最小brokerId变化请求给broker

broker定时拉取

BrokerController#start:broker启动后,每隔1s从nameserver拉取当前broker组成员情况

默认情况下broker只能通过这个定时任务来感知broker组成员变化。

RouteInfoManager#getBrokerMemberGroup:nameserver返回当前存活的所有broker地址。

BrokerController#syncBrokerMemberGroup:

broker收到成员信息,更新存活副本数量 ,更新最小brokerId

2、slave角色变更

BrokerController#updateMinBroker:

只有slave角色需要处理最小brokerId变化。

如果组内最小broker下线(minBrokerId>this.minBrokerIdInGroup),这即可能是master,也可能是上一个代理master。

BrokerController#onMinBrokerChange:

  1. changeSpecialServiceStatus:二级消息任务调度管理;
  2. onMasterOffline:如果下线的是master,关闭ha连接,停止同步;
  3. onMasterOnline:如果最小brokerId是master,代表master上线,建立ha连接,开始同步;
  4. pullRequestHoldService#notifyMasterOnline:如果最小brokerId是master,代表master上线,唤醒长轮询pull消息线程,让客户端从master拉消息;

BrokerController#changeSpecialServiceStatus:

如果当前slave成为最小brokerId晋升为代理master,开启延迟、事务、POP的二级消息调度任务;

如果当前slave成为非最小brokerId降级为普通slave,关闭延迟、事务、POP的二级消息调度任务。

六、二级消息逃逸

代理master仅仅启动二级消息调度任务还是不够的,因为这类消息往往需要二次发送消息写入commitlog,比如延迟消息需要替换真实topic和queue写入真实消息。

而代理master本质是slave,不应该提供写服务。

所以提出了消息逃逸 的概念,即将这类消息写到真实master

1、两种逃逸方式

EscapeBridge#putMessage:以事务消息为例。

优先使用本地逃逸 ,当使用container模式( RIP-31),一个进程可启动多个broker实例,peekMasterBroker返回当前进程中的一个master角色broker投递消息;

其次使用远程逃逸 ,需要配置enableRemoteEscape=true(默认false),从nameserver找topic下其他可写队列(其他master broker)投递消息。

2、事务消息回查

具体事务消息回查逻辑见RocketMQ4源码生产者特性

首先需要注意的是,代理master是只读服务。

事务消息生产者无法执行二阶段endTransaction(oneway请求,发送op消息)。

处理一半的事务消息(只发送了half消息),只能通过broker端回查本地事务来确定最终状态

TransactionalMessageServiceImpl#check:在代理master侧的事务消息处理逻辑如下

  1. 拉op消息;
  2. 如果half和op消息匹配,直接提交两者offset;
  3. 如果不匹配,将half消息逃逸到其他master broker

所以代理master不会执行回查逻辑,仅仅是将未收到二阶段结果的half消息投递到其他真实master上

3、延迟消息

具体延迟消息调度逻辑见RocketMQ4源码生产者特性

ScheduleMessageService.DeliverDelayedMessageTimerTask#executeOnTimeUp:

延迟消息到期后投递到真实用户topic。

EscapeBridge#asyncPutMessage:同样这里也会优先选择本地逃逸,其次选择远程逃逸。

4、POP消费

POP消费逻辑见RocketMQ5 POP消费

PopBufferMergeService#putCkToStore:

broker侧,处理consumer拉消息 请求,发送checkpoint消息会走EscapeBridge处理二级消息逃逸逻辑。

AckMessageProcessor#processRequest:

broker侧,处理consumer消费成功 请求,发送ack消息会走EscapeBridge处理二级消息逃逸逻辑。

PopReviveService#reviveRetry:

broker侧,消费receive topic中的checkpoint和ack消息进行匹配

如果checkpoint超时(60s)未匹配到ack,代表consumer可能未消费成功。

重新投递checkpoint到pop重试topic(%RETRY%{group}_{topic}),走EscapeBridge处理二级消息逃逸逻辑。

七、队列锁(顺序消费)

代理master上线后,在顺序consumer可以发现brokerId=0的代理master,获取队列锁。

考虑到master重新上线(或最小brokerId变化)后,可能造成消费组内2个consumer成功获取同一个队列的锁。

如c1获取真实master上queue1的锁,c2获取代理master上queue1的锁,最终造成queue1非独占消息非严格有序。

在5.x版本提出了quorum锁,默认lockInStrictMode=false,未开启quorum锁。

AdminBrokerProcessor#lockBatchMQ:

broker侧,如果开启quorum锁,且配置副本数totalReplicas>1,则需要过半副本获取锁成功,才表示获取锁成功

八、预上线

1、Master预上线

master预上线主要是为了解决slave提供消费能力造成消费进度回退的问题

SlaveActingMaster模式下,broker会启动BrokerPreOnlineService线程处理预上线逻辑。

在预上线完成前,broker处于isIsolated=true状态,不会向nameserver发送注册和轻量级心跳。

BrokerPreOnlineService#prepareForBrokerOnline:以master上线视角来看

  1. 查询nameserver组内成员情况;
  2. 如果组内成员存在,代表代理master正在工作,执行预上线逻辑;
  3. 如果组内成员不存在,startService开启二级消息调度服务,并解除隔离isIsolated=false

BrokerPreOnlineService#prepareForMasterOnline:循环组内所有slave

  1. 向slave发送自己的地址,用于后续数据同步;
  2. 等待slave与自己建立HAConnection;
  3. 从slave反向同步消费进度
  4. 组内所有slave处理完成,startService启动二级消息调度,解除隔离,正式上线;

BrokerController#startService:

2、Slave预上线

BrokerPreOnlineService#prepareForBrokerOnline:以slave上线视角来看,有四种情况

  1. master在线,执行slave预上线逻辑
  2. 组内存在其他成员,当前slave的id最小,startService成为代理master上线;
  3. 组内存在其他成员,当前slave的id非最小,startService什么都不做,直接解除隔离状态上线;
  4. 组内无其他成员,startService成为代理master上线;

BrokerPreOnlineService#prepareForSlaveOnline:master存活时,slave需要执行预上线逻辑。

slaveActingMaster,slave预上线,直接请求master获取master的ha地址,建立ha连接后,才解除隔离,允许注册到nameserver。

注:传统slave上线,通过注册nameserver获取master的ha地址,然后建立ha连接。

九、quorum write

CommitLog#asyncPutMessage:broker写消息。

SYNC_MASTER 同步复制模式下,slaveActingMaster也有quorum write机制,即n个副本成功复制后,才能响应客户端消息发送成功。

关键在于两个数字的计算:

  1. 运行时inSync副本数:追上master的副本数量;
  2. ack副本数:需要复制成功的副本数量;

1、inSync副本数

追上master的副本数量inSyncReplicas=min(存活副本数量,追上master的副本数量)。

存活副本数量,broker通过BrokerController#syncBrokerMemberGroup每1秒获取broker组成员信息得到。

DefaultHAService#inSyncReplicasNums:

追上master的副本数量=与master建立连接且commitlog同步进度落后不超过256MB( haMaxGapNotInSync )的slave +master自己

2、ack副本数

CommitLog#calcNeedAckNums:根据当前insync副本数量,计算得到最终需要ack的副本数量。

默认enableAutoInSyncReplicas=false,未开启自动降级,最终ack数量=配置inSyncReplicas,默认为1,SYNC_MASTER退化为ASYNC_MASTER

如果要正确开启SYNC_MASTER语义,至少需要配置inSyncReplicas大于1

  1. 不开启自动降级(默认),则ack数量=配置inSyncReplicas;
  2. 开启自动降级,则ack数量
    1. 优先取min(配置inSyncReplicas,运行insync副本数);
    2. 如果上述结果小于minInSyncReplica(默认1),则取minInSyncReplica,代表自动降级底线要到达n个副本ack;

总结

SlaveActingMaster模式主要解决了两个问题:

  1. master下线期间,增强slave提供的消费能力;
  2. master重新上线后,避免消费进度回退;

路由管理

为了解决这两个问题,出现了代理master角色。

本质上代理master broker仍然是slave,自己的brokerId并未发生变化。

nameserver做了特殊处理,对客户端屏蔽了代理master的存在

  1. broker注册/注销 :如果broker组内master下线,队列被设置为只读
  2. 查询路由 :如果broker组内master下线,选择最小brokerId的slave将这个slave的brokerId设置为0返回

轻量级心跳

为了及时察觉broker下线,提出了轻量级心跳

传统模式下,broker每隔30s发送注册请求到nameserver,注册请求中包含当前broker的topic配置。

开启slaveActingMaster后,broker额外会每隔1s发送轻量级心跳给所有nameserver,心跳包中仅包含name、addr等信息。

nameserver每隔5s扫描 ,如果超过10s未收到broker注册或轻量级心跳,判定为下线,执行broker注销逻辑(路由表变更)。

感知broker组成员变化

传统master-slave,slave之间互相不需要有感知。

但是在slaveActingMaster模式下,只有最小brokerId能成为代理master,所以所有broker实例都需要知道目前组内的成员存活情况,才能正确切换broker自己的角色。

nameserver推。

nameserver在察觉到broker下线后,可以推送NOTIFY_MIN_BROKER_ID_CHANGE给组内其他broker。但是默认配置notifyMinBrokerIdChanged=false,nameserver不会推送消息给broker。

broker拉。

broker启动后,每隔1s从nameserver拉取当前broker组成员情况,更新当前最小brokerId,决策自己的角色。

slave角色变更

当组内minBrokerId变化,slave的角色可能变化。

如果新的minBrokerId是自己,启动二级消息调度(事务、延迟、POP),反之关闭。

如果下线的minBroker是master,关闭ha连接。

二级消息处理

二级消息:需要broker二次投递的消息,如事务消息、延迟消息、POP消费消息。

当master下线,二级消息调度处于停滞状态。

slaveActingMaster模式下,代理master能将这类消息投递到其他broker组的存活master上 ,称为消息逃逸

逃逸有两种方式:

  1. 优先使用本地逃逸 ,当使用container模式( RIP-31),一个进程可启动多个broker实例,返回当前进程中的一个master角色broker投递消息;
  2. 其次使用远程逃逸 ,需要配置enableRemoteEscape=true(默认false),从nameserver找topic下其他可写队列(其他master broker)投递消息;

事务消息

代理master在匹配half消息(一阶段)和op消息(二阶段提交/回滚)阶段。

不会执行回查逻辑,将未收到二阶段结果的half消息逃逸到其他master上。

延迟消息

消息到期后,逃逸到其他master上。

POP消费消息有三类消息需要逃逸:

  1. consumer拉消息,checkpoint消息;
  2. consumer消费成功,ack消息;
  3. 匹配checkpoint和ack消息,重新投递checkpoint消息;

预上线

slaveActingMaster,通过预上线机制,解决消费进度回退问题。

master

  1. 查询nameserver组内成员情况;
  2. 循环所有slave,与slave建立ha连接,从每个slave反向同步消费进度等数据
  3. 开启二级消息调度服务;
  4. 解除隔离,正式上线;

slave

查询nameserver组内成员情况:

  1. 如果组内master在线,直接请求master获取master的ha地址,建立ha连接;
  2. 如果组内master下线,自己是最小broker ,直接成为代理master,开启二级消息调度;
  3. 如果组内master下线,自己不是最小broker,作为普通slave,直接上线;

队列锁

对于顺序消费 ,传统模式下,如果master没有及时恢复,则对应队列在consumer侧的队列锁超时(30s),导致队列不可消费。

slaveActingMaster模式下,由于nameserver侧将最小broker提升为代理master,consumer可以发现代理master执行锁api

此外,在5.x版本提出了quorum锁,默认broker侧lockInStrictMode=false,未开启quorum锁。

如果开启quorum锁,且配置副本数totalReplicas >1,则需要过半副本获取锁成功,才表示获取锁成功

quorum write

SYNC_MASTER 同步复制模式下,slaveActingMaster也有quorum write机制,即n个副本成功复制后,才能响应客户端消息发送成功。

默认情况下 ,需要inSyncReplicas =1个副本同步成功,SYNC_MASTER实际效果和ASYNC_MASTER一致

如果要保持SYNC_MASTER语义,至少配置inSyncReplicas=2

此外,支持自动降级enableAutoInSyncReplicas=true ,支持降级为运行时inSync副本数,但是降级副本数不能少于minInSyncReplica(默认1)。

运行时inSync副本数 ,即追上master的副本数量,与master建立连接且commitlog同步进度落后不超过256MB( haMaxGapNotInSync )的slave +master自己

参考文档

  1. docs/cn/SlaveActingMasterMode.md
  2. RIP-32

欢迎大家评论或私信讨论问题。

本文原创,未经许可不得转载。

欢迎关注公众号【程序猿阿越】。

相关推荐
陈平安Java and C5 小时前
MyBatisPlus
java
秋野酱5 小时前
如何在 Spring Boot 中实现自定义属性
java·数据库·spring boot
安的列斯凯奇6 小时前
SpringBoot篇 单元测试 理论篇
spring boot·后端·单元测试
Bunny02126 小时前
SpringMVC笔记
java·redis·笔记
架构文摘JGWZ6 小时前
FastJson很快,有什么用?
后端·学习
BinaryBardC6 小时前
Swift语言的网络编程
开发语言·后端·golang
feng_blog66886 小时前
【docker-1】快速入门docker
java·docker·eureka
邓熙榆6 小时前
Haskell语言的正则表达式
开发语言·后端·golang
东阳马生架构7 小时前
RocketMQ原理—3.源码设计简单分析下
rocketmq
枫叶落雨2228 小时前
04JavaWeb——Maven-SpringBootWeb入门
java·maven