前言
本章基于rocketmq5.1.1版本,分析slaveActingMaster模式。
master-slave模式下,如果master下线:
- 传统master-slave:slaveReadEnable=true,slave能提供有限的消费能力;
- controller模式:将slave自动提升为master;
- slaveActingMaster模式:让slave具备更全面的消费能力(代理master);
涉及历史文章:
- RocketMQ4源码(一)NameServer
- RocketMQ4源码(二)普通消息发送
- RocketMQ4源码(三)普通消息消费
- RocketMQ4源码(四)生产者特性
- RocketMQ4源码(五)消费者特性
- RocketMQ4源码(六)HA
- RocketMQ5源码(一)POP消费
- RocketMQ5源码(二)controller模式
一、引入
1、4.x版本master下线影响
在RocketMQ4源码(六)HA最后总结了一下Master下线对于客户端的影响:
- 对于producer来说,producer路由无法发现master broker下的队列;
- 对于consumer来说,只要slave不下线,依然可以发现所有队列;
- consumer可以正常进行rebalance、从slave拉消息、提交offset到slave;
- 对于顺序消费,如果在全局锁过期前,master不上线,consumer将无法继续执行本地消费逻辑;
- 对于延迟消息,master下线期间无法正常调度到目标队列;
- 对于事务消息,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重新上线后,会触发两个事情:
- slave从master同步consumerOffset.json,即master消费进度覆盖slave消费进度;
- 容易触发立即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地址:
- nameserver开启slaveActingMaster;
- broker组内master下线;
- 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:
- changeSpecialServiceStatus:二级消息任务调度管理;
- onMasterOffline:如果下线的是master,关闭ha连接,停止同步;
- onMasterOnline:如果最小brokerId是master,代表master上线,建立ha连接,开始同步;
- 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侧的事务消息处理逻辑如下
- 拉op消息;
- 如果half和op消息匹配,直接提交两者offset;
- 如果不匹配,将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上线视角来看
- 查询nameserver组内成员情况;
- 如果组内成员存在,代表代理master正在工作,执行预上线逻辑;
- 如果组内成员不存在,startService开启二级消息调度服务,并解除隔离isIsolated=false;
BrokerPreOnlineService#prepareForMasterOnline:循环组内所有slave
- 向slave发送自己的地址,用于后续数据同步;
- 等待slave与自己建立HAConnection;
- 从slave反向同步消费进度;
- 组内所有slave处理完成,startService启动二级消息调度,解除隔离,正式上线;
BrokerController#startService:
2、Slave预上线
BrokerPreOnlineService#prepareForBrokerOnline:以slave上线视角来看,有四种情况
- master在线,执行slave预上线逻辑;
- 组内存在其他成员,当前slave的id最小,startService成为代理master上线;
- 组内存在其他成员,当前slave的id非最小,startService什么都不做,直接解除隔离状态上线;
- 组内无其他成员,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个副本成功复制后,才能响应客户端消息发送成功。
关键在于两个数字的计算:
- 运行时inSync副本数:追上master的副本数量;
- 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:
- 不开启自动降级(默认),则ack数量=配置inSyncReplicas;
- 开启自动降级,则ack数量
-
- 优先取min(配置inSyncReplicas,运行insync副本数);
- 如果上述结果小于minInSyncReplica(默认1),则取minInSyncReplica,代表自动降级底线要到达n个副本ack;
总结
SlaveActingMaster模式主要解决了两个问题:
- master下线期间,增强slave提供的消费能力;
- master重新上线后,避免消费进度回退;
路由管理
为了解决这两个问题,出现了代理master角色。
本质上代理master broker仍然是slave,自己的brokerId并未发生变化。
nameserver做了特殊处理,对客户端屏蔽了代理master的存在:
- broker注册/注销 :如果broker组内master下线,队列被设置为只读;
- 查询路由 :如果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上 ,称为消息逃逸。
逃逸有两种方式:
- 优先使用本地逃逸 ,当使用container模式( RIP-31),一个进程可启动多个broker实例,返回当前进程中的一个master角色broker投递消息;
- 其次使用远程逃逸 ,需要配置enableRemoteEscape=true(默认false),从nameserver找topic下其他可写队列(其他master broker)投递消息;
事务消息:
代理master在匹配half消息(一阶段)和op消息(二阶段提交/回滚)阶段。
不会执行回查逻辑,将未收到二阶段结果的half消息逃逸到其他master上。
延迟消息:
在消息到期后,逃逸到其他master上。
POP消费消息有三类消息需要逃逸:
- consumer拉消息,checkpoint消息;
- consumer消费成功,ack消息;
- 匹配checkpoint和ack消息,重新投递checkpoint消息;
预上线
slaveActingMaster,通过预上线机制,解决消费进度回退问题。
master
- 查询nameserver组内成员情况;
- 循环所有slave,与slave建立ha连接,从每个slave反向同步消费进度等数据;
- 开启二级消息调度服务;
- 解除隔离,正式上线;
slave
查询nameserver组内成员情况:
- 如果组内master在线,直接请求master获取master的ha地址,建立ha连接;
- 如果组内master下线,自己是最小broker ,直接成为代理master,开启二级消息调度;
- 如果组内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自己。
参考文档:
欢迎大家评论或私信讨论问题。
本文原创,未经许可不得转载。
欢迎关注公众号【程序猿阿越】。