RocketMQ5源码(三)SlaveActingMaster模式

264 阅读18分钟

前言

本章基于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支持。

supportActingMaster=true

Broker

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

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支持,还可以选择开启二级消息远程逃逸。

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

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

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

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