RocketMQ4源码(六)HA

677 阅读25分钟

前言

本章分析rocketmq在4.x(4.6.0)版本对于HA的相关实现。

4.x版本HA有两种方式:

  1. Master-Slave模式

支持SYNC_MASTER(同步复制)和ASYNC_MASTER(异步复制)。

当Master故障,需要手动执行MasterSlave切换或Master重新上线。

  1. DLedger模式

同名broker部署至少3副本行成raft组。

自主选举Leader,Leader节点成为Master Broker,Follower节点成为Slave Broker,实现故障自动failover。

DLedger模式对于raft实现写的很清晰,比sofa-jraft容易读,但是本文不会深入分析raft的实现细节。

接下来按照这个顺序分析:

  1. broker启动需要恢复哪些数据,哪里会涉及HA逻辑;
  2. 分析两种HA实现;
  3. 客户端在master下线时的表现;

一、铺垫

broker启动可分为三步:

  1. 创建BrokerController;
  2. BrokerController初始化;
  3. BrokerController启动;

从全局看一下BrokerController初始化和启动过程中哪里涉及HA逻辑。

1、broker初始化

BrokerController#initialize:

  1. 加载磁盘文件到内存;
  2. MessageStore#load除了加载磁盘文件外,会执行一些恢复逻辑;
  3. 准备完毕后,开启一些后台线程;(忽略)

加载磁盘文件到内存

下面列举了主要加载的文件:逻辑忽略

用途内存磁盘文件
topic配置TopicConfigManager#topicConfigTabletopic-TopicConfig~/store/config/topics.json
消费组消费进度ConsumerOffsetManager#offsetTabletopic@group-queueId-逻辑offset~/store/config/consumerOffset.json
标识上次broker进程是否正常关闭true/false~/store/abort
延迟消费进度ScheduleMessageService#offsetTable延迟级别(queueId+1)-逻辑offset~/store/config/delayOffset.json
commitlog消息(mmap)CommitLog#mappedFileQueue~/store/commitlog/*
consumequeue消费队列(mmap)DefaultMessageStore#consumeQueueTabletopic-queueId-ConsumeQueue(mappedFileQueue)~/store/consumequeue/{topic}/{queueId}/*
checkpoint用于恢复DefaultMessageStore#StoreCheckpoint~/store/checkpoint
index查询索引(mmap)IndexService#indexFileList~/store/index/*

恢复

恢复主要是针对一些内存字段恢复,比如mmap文件的顺序写进度。

DefaultMessageStore#load:在一切文件加载完毕,执行recover恢复。

DefaultMessageStore#recover:

恢复包含三部分数据:consumequeue、commitlog、topic-queueid-最大逻辑offset。

其中commitlog恢复根据ha策略不同实现不同,后面再看。

DefaultMessageStore#recoverConsumeQueue:

遍历每个队列ConsumeQueue执行恢复,返回所有consumequeue中,commitlog的最大物理offset。

ConsumeQueue#recover:

每个ConsumeQueue只取最后的3个文件,即最新的commitlog对应的n条consumequeue记录。

循环这些consumequeue文件,统计两个数据:

  1. consumequeue记录中对应commitlog的物理offset的最大值;
  2. consumequeue自己的物理offset的最大值,用于设置MappedFileQueue的顺序读写位置;

2、broker启动

BrokerController#start:

  1. MessageStore启动
  2. broker服务端NettyServer启动;
  3. broker客户端NettyClient启动;
  4. 长轮询处理服务启动;
  5. 客户端心跳检测服务启动;
  6. ha部分,master-slave模式config同步
  7. 最终开始向nameserver发送心跳,客户端可以发现当前broker;

这里仅关注一下DefaultMessageStore启动和ha部分在启动流程中的位置。

DefaultMessageStore#start:

Step1,获取FileLock,一个store路径只能存在一个broker进程。

Step2,根据consumequeue中目前最大的commitlog物理offset,开始reput(创建consumequeue和index)。

等待reput进度追上commitlog最大物理offset,main线程才继续执行。

Step3,ha部分,master-slave模式下,开启HAService。

Step4,开启各种刷盘线程,创建abort文件。

二、Master-Slave

案例

master

brokerId,0代表是master,非0代表是slave。

brokerRole,ASYNC_MASTER,异步master,可选SYNC_MASTER。

listenPort,接收producer和consumer请求的端口,也与slave同步部分数据,listenPort+1会作为ha端口与slave同步commitlog数据。

brokerClusterName=Cluster1M1S
brokerName=broker1
brokerId=0
brokerRole=ASYNC_MASTER
namesrvAddr=localhost:9876
listenPort=18991
storePathRootDir=/tmp/rocketmq-4.6.0/Cluster1M1S/broker1
storePathCommitLog=/tmp/rocketmq-4.6.0/Cluster1M1S/broker1/commitlog

slave

brokerClusterName和brokerName需要和master保持一直。

brokerId,非0是slave。

brokerClusterName=Cluster1M1S
brokerName=broker1
brokerId=1
brokerRole=SLAVE
namesrvAddr=localhost:9876
listenPort=19991
storePathRootDir=/tmp/rocketmq-4.6.0/Cluster1M1S/broker1S
storePathCommitLog=/tmp/rocketmq-4.6.0/Cluster1M1S/broker1S/commitlog

1、CommitLog恢复

正常恢复

CommitLog#recoverNormally:

当abort文件不存在,代表上次broker进程正产关闭,执行正常恢复逻辑。

  1. 仅加载最后3个commitlog文件,每个1g,默认情况下最多有3个g的commitlog被mmap;
  2. 统计commitlog写到哪,设置到MappedFileQueue;
  3. 每条消息做crc32校验;
  4. 入参是consumequeue中最大的commitlog物理offset,如果consumequeue刷盘进度大于实际commitlog,截断consumequeue中的记录(忽略);

异常恢复

CommitLog#recoverAbnormally:(一直被标记为废弃)

如果abort文件存在,代表上次broker进程未正常关闭。

非正常恢复与正常恢复有两点区别。

第一点,找commitlog。

从后往前找到第一个与checkpoint匹配的commitlog文件,如果找不到取第一个文件。

匹配逻辑大致是,从后往前找第一个commitlog文件,要求文件中第一个消息存储时间小于等于consumequeue的最大存储时间(logicsMsgTimestamp)。

checkpoint文件存储了commitlog、consumequeue、index最后一条刷盘数据的存储时间。

每次consumequeue和index刷盘都会顺便刷盘checkpoint。

第二点,非正常恢复从匹配的commitlog文件开始,向后重新构建consumequeue和index。

2、Slave发现Master

BrokerController#doRegisterBrokerAll:

master/slave都会发送心跳给NameServer,包含自己的普通地址(10911)ha地址(10912)

对于slave可以收到nameserver返回master的两个地址,从而可以进行后续通讯。

RouteInfoManager#registerBroker:

nameserver侧保证只在slave(brokerId非0)的心跳响应里回复master的地址信息

3、SlaveSynchronize(config同步)

BrokerController#start:

slave在Broker启动阶段从master同步config目录下的数据

BrokerController#handleSlaveSynchronize:

SlaveSynchronize每隔10s从master同步config数据。

SlaveSynchronize#syncAll:包括topic配置消费进度延迟消费进度

SlaveSynchronize#syncTopicConfig:

其实config目录下的数据,都会走master普通通讯地址同步,即和producer和consumer一样,走的都是10911端口。

以topic配置为例,通过GET_ALL_TOPIC_CONFIG请求获取整个topics.json数据。先写内存,然后刷盘。

4、HAService(commitlog同步)

DefaultMessageStore构造阶段,如果非DledgerBroker模式,创建HAService。

HAService负责Master与Slave之间数据同步,其成员变量同时包含Master和Slave角色的组件:

  1. AcceptSocketService:master侧,1个线程用于接收slave连接,默认监听端口listenePort+1=10912;
  2. HAConnection:master侧,每个slave连接对应一个HAConnection,负责与这个slave的数据同步;
  3. GroupTransferService:master侧,仅当master为SYNC_MASTER时才有用,后面再说;
  4. HAClient:slave侧,HAService中唯一的slave组件,1个线程用于与master通讯;

HAService#start:broker启动阶段,在DefaultMessageStore#start中,会启动HAService。

Master监听HA端口

AcceptSocketService#beginAccept:AcceptSocketService打开HA端口10912。

AcceptSocketService#run:master等待slave与自己建立连接。

Slave连接Master

HAService.HAClient#run:

通过nameserver返回的心跳响应,slave能够发现master的ha地址,尝试与master建立连接。

如果连接失败,间隔5s重试。

HAService.HAClient#connectMaster:

slave在与master建立连接后,确定commitlog最大物理offset作为初始同步offset

Master接受Slave连接

HAService.AcceptSocketService#run:

Master收到一个Slave连接后,封装为HAConnection并启动,继续等待下一个Slave连接。

HAConnection会为每个slave连接开启一个读socket线程ReadSocketService和一个写socket线程WriteSocketService

Slave汇报同步进度

HAService.HAClient#run:

slave每隔5s(haSendHeartbeatInterval) 发送同步进度给master,作为ha心跳。

然后等待master回复。

HAService.HAClient#reportSlaveMaxOffset:

slave只会发送8个字节的long给master,即当前slave同步进度(commitlog物理offset)。

Master接收Slave同步进度

HAConnection.ReadSocketService线程的主要作用是读取slave的同步进度

如果超过20s未收到过slave进度,连接将被master关闭,slave侧可以重连。

HAConnection.ReadSocketService#run:

HAConnection.ReadSocketService#processReadEvent:

使用一个1M的buffer接收slave数据,如果满了从头复用。

HAConnection.ReadSocketService#processReadEvent:

master持续从socket中读取发来的slave同步进度,更新到内存中。

其中slaveAckOffset每次都会更新,而slaveRequestOffset只有首次连接更新一次。

对于SYNC_MASTER角色,还会唤醒PutMessage线程。(后面再看SYNC_MASTER)

Master同步commitlog

HAConnection.WriteSocketService#run:

在没有收到slave的初始同步offset前,WriteSocketService线程会一直等。

当收到slave的初始offset后,master会持续向slave传输commitlog

收到slave的初始同步offset(slaveRequestOffset)后,master决策从哪个offset开始同步

如果slave请求offset是0,代表slave没有任何commitlog文件,master将从最后一个commitlog文件的初始位置开始同步

如果slave请求的offset非0,从slave请求offset开始同步。

master同步给slave的数据包如下:

头部12字节,8个字节表示这段commitlog的起始offset,4个字节表示这段commitlog的大小。

体size字节,一段commitlog。

master在写空闲时(lastWriteOver=true),每5s发送ha心跳给slave,ha心跳只包含协议头12字节。

在数据包没有写完前,会持续执行transferData方法,向socket写数据。

如果一个数据包写完,从commitlog查询offset后的buffer。

如果不存在新的commitlog,等100ms后再次循环,也可被SYNC_MASTER唤醒。

如果存在新的commitlog,则写一个数据包,并更新下次同步位置nextTransferFromWhere。

注意,每次同步的commitlog大小不超过32kb,可通过haTransferBatchSize设置。

HAConnection.WriteSocketService#transferData:写数据包,先写头后写体。

Slave接收commitlog

HAService.HAClient#run:接收commitlog。

HAService.HAClient#processReadEvent:从socket中读取master数据包。

HAService.HAClient#dispatchReadRequest:

slave先判断master数据包中的offset是否与自己的最大offset一致。

如果不一致,返回false,与master断开连接,重新建立连接后,重新确定同步开始位置。

HAService.HAClient#dispatchReadRequest:

双边offset对齐的情况下,slave将数据写入自己的commitlog(内存pagecache),并向master再次汇报同步进度

DefaultMessageStore#appendToCommitLog:

slave写入commitlog,并唤醒reput线程,生成consumequeue和index。

5、SYNC_MASTER

CommitLog#putMessage:

当master写commitlog成功后,如果master角色是SYNC_MASTER(默认ASYNC_MASTER),需要等待slave同步完成,才能响应客户端。

主流程

CommitLog#handleHA:

1)HAService#isSlaveOk:判断slave是否可用,如果slave不可用,返回SLAVE_NOT_AVAILABLE;

2)HAService#putRequest:将本次写入的offset放入HAService的GroupTransferService中;

3)HAService#getWaitNotifyObject#wakeupAll:唤醒所有slave的WriteSocketService,如果slave之前commitlog已经同步完成,WriteSocketService会阻塞等待5s,所以需要唤醒一下;

4)GroupCommitRequest#waitForFlush:等待slave同步成功,超时时间5s,如果超时返回FLUSH_SLAVE_TIMEOUT;

SYNC_MASTER写消息HA的整体流程如下,需要GroupTransferService控制唤醒PutMessage线程。

SYNC_MASTER需要PutMessage线程等待slave同步,本质上还是由Read/WriteSocketService持续同步commitlog。

ASYNC_MASTER不需要图中notify流程,只需要让每个HAConnection持续同步commitlog即可。

slave可用

当master写commitlog成功,而slave不可用时,返回SLAVE_NOT_AVAILABLE,不会等待slave同步。

对于producer客户端来说,属于一种特殊的成功。

HAService#isSlaveOK:当满足下面两个条件的时候,认为slave可用:

1)至少存在一个slave与master建立连接

2)所有slave中,最大同步进度不落后master超过256MB

如果slave进程存在,只要与master建立连接后,就会持续同步commitlog。

一般情况下返回SLAVE_NOT_AVAILABLE是表明存在严重的主从延迟。

等待slave同步

HAService.GroupTransferService#doWaitTransfer:

GroupTransferService线程将写入消息的offset(GroupCommitRequest)与slave最大同步进度对比

当slave最大同步进度大于等于写入消息offset,或5s超时,唤醒对应putMessage线程

三、DLedgerBroker

案例

参考官方提供的案例。

节点n0:

brokerClusterName=RaftCluster
brokerName=RaftNode00
listenPort=30911
namesrvAddr=127.0.0.1:9876
storePathRootDir=/tmp/rmqstore/node00
storePathCommitLog=/tmp/rmqstore/node00/commitlog
enableDLegerCommitLog=true
dLegerGroup=RaftNode00
dLegerPeers=n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913
dLegerSelfId=n0
sendMessageThreadPoolNums=16

节点n1:

brokerClusterName=RaftCluster
brokerName=RaftNode00
listenPort=30921
namesrvAddr=127.0.0.1:9876
storePathRootDir=/tmp/rmqstore/node01
storePathCommitLog=/tmp/rmqstore/node01/commitlog
enableDLegerCommitLog=true
dLegerGroup=RaftNode00
dLegerPeers=n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913
dLegerSelfId=n1
sendMessageThreadPoolNums=16

节点n2省略,一个组内至少3副本。在1个副本下线的情况下,raft组还能正常工作。

DLedger相关配置项:

  1. dLegerGroup:一般和brokerName一致,代表一个raft组;
  2. dLegerPeers:组内成员-地址列表;
  3. dLegerSelfId:当前实例id;
  4. sendMessageThreadPoolNums:官方建议配置成cpu核数;
  5. enableDLegerCommitLog:开启DLedger;

与MasterSlave相比,不需要配置brokerRole、brokerId。

相关组件

DLedgerCommitLog

DLedgerCommitLog是DLedger模式下的核心组件,重写了大部分CommitLog的实现。

DLedgerCommitLog成员变量包括:

  • DLedgerServer:负责raft选举和复制;
  • DLedgerMmapFileStore:数据存储;
  • MmapFileList:DLedgerMmapFileStore中data文件集合;

DLedgerMmapFileStore

基于mmap的DLedger存储实现。

最重要的是两个基于mmap实现的文件集合:

  1. dataFileList:raft日志,可理解为DLedger模式下的commitlog文件,也是1G一个。区别在于除了消息之外,还有raft数据,比如index、term等概念
  2. indexFileList:索引,可以根据raft日志的index快速定位一条raft日志项(DLedgerEntry);

其他关于raft日志的内存字段:

  1. ledgerBeginIndex/ledgerEndIndex:raft日志的最小和最大index;
  2. committedIndex:已经提交的raft日志index;
  3. committedPos:已经提交的raft日志的物理offset;
  4. ledgerEndTerm:最大raft日志项对应的任期term,与ledgerEndIndex同时更新;

其中committedIndex会被记录在一个checkpoint文件中,在数据恢复阶段加载到内存。

DLedgerRoleChangeHandler

DLedgerRoleChangeHandler会根据raft选举结果,调用BrokerController切换当前broker实例角色。

1、CommitLog恢复

DLedgerCommitLog#recover:

broker初始化阶段,DLedgerCommitLog按照最大consumequeue恢复内存数据。

为了支持原生CommitLog切换DLedger,dividedCommitLogOffset指向DLedger模式的起始物理offset,在这之前的commitlog都可以走父类来获取。

DLedgerFileStore#recover:

DLedger需要恢复两部分数据:

  1. data和index文件的写进度;
  2. raft相关,如:最新raft日志的ledgerEndIndex/ledgerEndTerm(用于选举),已经提交的index;

2、CommitLog启动

DLedgerCommitLog#start:

broker启动阶段,原生CommitLog会开启刷盘线程,而DLedger模式下会启动DLedgerServer。

DLedgerServer#startup:

启动DLedger所有组件。

3、raft角色维持(选举)

DLedgerLeaderElector#startup:开启StateMaintainer线程。

DLedgerLeaderElector#maintainState:

StateMaintainer会根据当前节点的角色状态,执行不同的逻辑。

MemberState

MemberState维护了当前实例的状态:

  • role:角色,刚启动是candidate,需要经过选举后成为leader或follower;
  • leaderId:当前实例认同的leader的id;
  • currTerm:当前所处任期term;
  • currVoteFor:处于candidate状态下票选的leader;
  • ledgerEndIndex/ledgerEndTerm:最大raft日志对应index和term,比如在sofa-jraft中就是LogId,可以代表唯一一条raft日志;
  • peerMap:raft组成员id-通讯地址;

对于raft角色状态变更,MemberState都会加synchronized同步。

在运行过程中多处对MemberState加了synchronized同步,比如写commitlog,保证逻辑的一致性。

当任期或选票发生变化,都会持久化到currterm文件中。

Leader

DLedgerLeaderElector#maintainAsLeader:

Leader每隔2s向所有成员发送心跳。

DLedgerLeaderElector#sendHeartbeats:

心跳包含当前leader的任期term。

当master未收到过半成功响应,如果有人任期term比自己大或心跳超时6s,进入candidate状态重新选举。

Follower

DLedgerLeaderElector#maintainAsFollower:

每隔4s,follower检测leader是否心跳超时6s,如果超时,进入candidate状态重新选举。

Candidate

DLedgerLeaderElector#maintainAsCandidate:

每轮选举term任期加一。

DLedgerLeaderElector#voteForQuorumResponses:

candidate状态向所有成员发送VoteRequest,包含当前term任期,最新raft日志的term任期和index。

DLedgerLeaderElector#handleVote:

无论自己给自己投票,还是收到其他成员选票,都走这里。

根据raft各种逻辑判断,比如term大小、raft日志大小(term+index)。

最终如果验证通过,更新自己票选的leader,返回ACCEPT。

DLedgerLeaderElector#maintainAsCandidate:

收到过半ACCEPT,当前实例转换为leader,开始向成员发送心跳。

DLedgerLeaderElector#handleHeartBeat:

其他成员收到心跳,发现心跳中任期term与自己在同一轮,且自己还未发现leader,则转换为follower。

4、broker角色变更

DLedgerRoleChangeHandler:

当raft角色变更,broker角色会发生变更,提交一个任务单线程顺序处理

Slave

DLedgerRoleChangeHandler#handle:

raft角色从leader变成candidate,broker角色变成slave。

raft角色变成follower,broker角色变成slave。

BrokerController#changeToSlave:broker角色变更为slave。

  1. 变更brokerId和brokerRole;
  2. 关闭延迟消息调度;
  3. 关闭事物消息回查;
  4. 开启config同步(topic配置、消费进度)
  5. 发送心跳,为了更新nameserver中的路由;

注意这里的brokerId是通过selfId计算得到的,见DLedgerCommitLog构造。

id = Integer.valueOf(dLedgerConfig.getSelfId().substring(1)) + 1;

Master

DLedgerRoleChangeHandler#handle:

raft角色变成leader,等待consumequeue进度追上commitlog,commit进度追上写raft日志进度,broker角色变成master。

BrokerController#changeToMaster:

broker成为master角色,和slave类似。

5、写commitlog

主流程

DLedgerCommitLog#putMessage:

DLedgerCommitLog和父类一样,在获取锁成功的情况下写commitlog。

区别是DLedger将原始commitlog数据封装为一个AppendEntry请求,提交到DLedgerServer处理,返回一个future。

刷盘策略、同步策略都由DLedgerServer控制。

DLedgerServer#handleAppend:

  1. leader将原生commitlog封装到DLedgerEntry的body中,调用底层存储写raft日志,这一步是同步处理
  2. DLedgerEntryPusher复制给follower,等待过半ack,这一步是异步处理,返回future;

DLedgerCommitLog#putMessage:

3s内没收到DLedgerServer的响应(follower过半ack),都算作UNKNOWN_ERROR返回。

DefaultMQProducerImpl#sendDefaultImpl:

对于producer发送普通消息,这些异常都会映射到SERVICE_NOT_AVAILABEL和SYSTEM_ERROR,都会被捕获,支持重试。

Append(Leader)

DLedgerMmapFileStore#appendAsLeader:

写raft日志需要先获取MemberState锁

写的过程中,leader仍然是leader,term任期都不变,否则抛出异常。

第一步,填充DLedgerEntry,写入data文件(pagecache)。

一个DLedgerEntry对应data文件里一条数据,即raft日志。

消息作为body部分被一同写入raft日志。

第二步,写index文件(pagecache)。

index顺序增长,而每个index索引项定长32。

假设index=10,index索引项的起始物理offset=10*32,读32长度得到整个索引项。

根据索引项中的raft日志物理offset和大小,能从data文件中快速定位到一个raft日志项DLedgerEntry。

第三步,更新内存中最大raft日志的index和term。

Wait Ack(Leader)

DLedgerEntryPusher#waitAck:

  1. 更新内存中term(任期)-id(当前实例)-index(日志index)关系;
  2. 构建一个future,放入term-index-future,后期可以通过term-index找到future唤醒;
  3. 唤醒所有EntryDispatcher

返回future。

Push Append(Leader)

在leader端,每个follower会对应一个EntryDispatcher线程负责raft日志同步。

在leader刚选举完成时,需要先经过compare状态比对raft日志;

比对日志有差异,进入truncate状态截断raft日志;

比对日志无差异或truncate完成,进入append状态,可以正常同步日志。

假设现在leader和follower的raft日志已经对齐,进入append状态。

DLedgerEntryPusher.EntryDispatcher#doAppend:先执行append。

DLedgerEntryPusher.EntryDispatcher#doAppendInner:

leader构建一个PushEntry请求,请求类型是APPEND,发送给follower。

follower如果返回成功,更新follower对应term(任期)-id(成员id)-index(raft日志index)。

唤醒QuorumAckChecker线程。

Append(Follower)

DLedgerEntryPusher.EntryHandler#handlePush:

follower接收PushEntry请求,通讯线程将请求和响应future放入一个writeRequestMap直接返回。

DLedgerEntryPusher.EntryHandler#doWork:

EntryHandler线程消费writeRequestMap,处理写raft日志。

DLedgerEntryPusher.EntryHandler#handleDoAppend:

follower调用DLedgerStore存储DLedgerEntry。

和master一样,写data和index文件(pagecache),更新最大raft日志index和term。

顺便也可能更新committedIndex,根据请求中当前leader已提交index。

Commit(Leader)响应客户端

DLedgerEntryPusher.QuorumAckChecker#doWork:

QuorumAckChecker线程,检测当前term下不同节点已经成功写raft日志成功的index。

最终得到最大的过半写raft日志成功的quorumIndex,更新内存中当前term的提交index。

DLedgerEntryPusher.QuorumAckChecker#doWork:

commit结束后,唤醒阻塞等待的写commitlog线程,即可响应客户端

Push Commit(Leader)

DLedgerEntryPusher.EntryDispatcher#doCommit:

对于每个follower,处于Append状态的EntryDispatcher线程会构建COMMIT类型的PushEntry请求给follower。

注意,并不是每次append发送给follower后立即commit,至少间隔1秒才commit,提高效率

Commit(Follower)

DLedgerEntryPusher.EntryHandler#handleDoCommit:

更新内存term对应committedIndex。

刷盘

leader和follower整个流程中,不存在刷盘,都是写pagecache。

FlushDataService负责将所有DLedger相关文件刷盘。

其中data和index文件10ms刷一次,checkpoint(committedIndex)文件3s刷一次。

6、consumequeue构建

DLedgerCommitLog在原始CommitLog结构上新增了raft字段。

DLedgerCommitLog#checkMessageAndReturnSize:

为了保持整体逻辑的一致,在构建DispatchRequest时,DLedgerCommitLog做了适配处理。

最终comsumequeue中的物理offset对应真实commitlog项的offset(去除raft头) ,而非raft日志项的offset。

7、消费拉消息

DLedgerCommitLog#getMessage:

由于consumequeue的适配操作,在实际拉消息时,正常根据offset获取一段buffer即可。

四、Master下线对客户端的影响

普通发送

MQClientInstance#topicRouteData2TopicPublishInfo:

对于发送消息来说,producer路由不会包含非master的队列

master-slave模式下:

master侧,下线后不继续发送心跳;

producer侧,有缓存路由,缓存失效前如果轮训到下线master会发生网络异常;

producer侧,缓存路由刷新后(比如30s定时刷新),不会轮训到下线master broker,如果topic仅存在于这组broker上,那么会返回topic路由不存在;

DLedger模式下:

broker处于candidate状态,仍然会持续发送心跳,但是心跳包中的brokerId都是-1,也不会被producer发现。

比如刚创建BrokerController时,DLedger模式会强制设置brokerId为-1。

BrokerStartup#createBrokerController:

普通消费

consumer侧

消费侧,master-slave和DLedger模式的表现差不多。

消费路由角度来看,消费者不关心broker数据,只关心queue数据。

MQClientInstance#topicRouteData2TopicSubscribeInfo:

只要slave角色还存活,消费者都能发现queue进行rebalance

拉消息角度来看。

MQClientInstance#findBrokerAddressInSubscribe:

消费者优先走指定brokerId(默认master),如果指定brokerId下线,从同名broker中选一个。

所以可以从slave拉消息。

提交offset角度来看。

MQClientInstance#findBrokerAddressInAdmin:

从4.6.0代码实现来看,选择了同名broker中随机的一个节点,未必是master。

根据HashMap#entrySet的实现来看,大部分未发生哈希冲突的情况下,第一个遍历到的就是id=0的节点。

所以默认走master,如果master下线走slave。

如果发生哈希冲突,比如brokerId存在16,且brokerId=16在brokerId=0之前加入map,那么将默认选择brokerId=16的slave节点。

这个逻辑在新版本(4.9.3)得到修复(ISSUE-3603),会走和拉消息一样的选broker方式,优先选master。

消费失败重试角度来看。

DefaultMQPushConsumerImpl#sendMessageBack:

优先会重新投递到消息原始存储的broker,即下线master。

由于master下线,这里会发生异常,最终走普通消息发送流程。

普通消息发送支持重试,所以可以轮训到其他master broker(如果存在其他master broker)。

broker侧

拉消息角度来看。

broker会返回一个suggestWhichBrokerId字段,建议下次从哪个broker拉消息。

大部分情况下,从slave拉消息都会响应建议从master拉

但是消费者MQClientInstance#findBrokerAddressInSubscribe发现master下线了,仍然会从slave拉。

PullMessageProcessor#processRequest:

一般拉消息的同时支持顺便提交offset(集群消费),但是从slave拉消息不支持同时提交offset

提交offset角度来看。

slave和master处理的逻辑都一样,先写内存后刷盘。

所以在master重新上线期间,消费进度都存储在slave里。

无论是master-slave还是DLedger,所以这部分消费进度可能会在master重新上线后覆盖(config同步)。

但是如果客户端处于稳定状态,客户端内存有正确的消费进度,通过后续的定时或拉消息能将正确的offset提交给master。

除了极端场景下,会出现重复消费情况。

比如master下线,slave收到新offset,master上线,consumer宕机未同步最新offset给master,那么这些新offset将被consumer重复消费。

特性

广播消费

参照普通消费,几乎无影响。

顺序消费

producer侧,和普通消息一致,无法发送给线下master broker。

producer路由刷新后,可能导致消息hash到其他queue,可能导致乱序。

RebalanceImpl#lockAll:

consumer侧,每隔20s进行锁续期。

只会找queue对应的master broker,如果找不到master broker,consumer侧会维持现状(不改变ProcessQueue分配和状态)

ConsumeMessageOrderlyService.ConsumeRequest#run:

但是如果master长期不上线(DLedger不重新选出leader),ProcessQueue会因为锁未及时续期而超时。

从而导致无法执行用户代码的消费逻辑,消息将积压在客户端(拉消息不判断锁过期,但有消费流控)。

延迟消息

producer侧,和普通消息一致,无法发送给线下master broker。

DefaultMessageStore#handleScheduleMessageService:

在不存在master角色的情况下,延迟消息在broker端无法调度到目标队列。

待master重新上线(DLedger产生新主),延迟消息会被正常调度。

事务消息

broker侧,无法调度消息回查。

BrokerController:

producer侧,假设一阶段执行成功(master未下线,发送half消息成功),二阶段END_TRANSACTION失败(master下线,发送op消息失败),不会抛出异常(oneway和try-catch)。

待master重新上线(DLedger新leader拥有最大raft日志,half消息不丢),broker重新调度消息回查,执行二阶段提交或回滚。

总结

config目录同步(Master-Slave&DLedger)

config目录下包括:topic配置消费进度延迟消费进度等。

同步由slave角色发起。

SlaveSynchronize每隔10s,请求mater的普通通讯地址(10911)获取相关配置,进行全量同步。

commitlog同步(Master-Slave)

master的ha地址(普通地址+1=10912)用于commitlog同步。

slave向nameserver发送心跳,能够获取master的普通通讯地址和ha地址。

slave发现master后开始同步逻辑:

  1. master:AcceptSocketService线程监听ha端口(10912);
  2. slave:与master建立连接;
  3. master:收到slave的socket,创建一个HAConnection,开启一个读socket线程ReadSocketService和一个写socket线程WriteSocketService
  4. slave:汇报当前commitlog的物理offset,即同步进度;
  5. master:ReadSocketService收到slave同步进度,WriteSocketService持续向slave发送commitlog buffer(最大32kb);
  6. slave:收到commitlog,写入本地commitlog,汇报master当前同步进度;

如果master是SYNC_MASTER角色。

PutMessage线程需要阻塞等待slave复制完成,才能被GroupTransferService唤醒。

其中,slave复制完成仅需要一个slave复制完成,不需要所有slave复制完成。

commitlog同步(DLedger)

DLedger模式通过MemeberState维护当前节点的状态。

MemberState包括当前节点raft角色最大raft日志当前leader集群成员列表等等。

选举

当master下线或节点刚启动,raft角色=candidate,发起一轮投票(term+1)。

每个candidate选择term相同且raft日志(term+index)最大的节点作为leader,当一个candidate收到过半选票后,成为leader。

成为leader后,向所有candidate发送心跳,candidate发现心跳中任期term与自己在同一轮,且自己还未发现leader,则转换为follower。

raft leader会成为master,开启事务消息回查和延迟消息调度。

raft follower会成为slave,关闭事务消息回查和延迟消息调度,开启config目录同步。

compare&truncate

这部分没有细说,主要是为了raft follower和leader的raft日志对齐,忽略。

写commitlog

写commitlog就是一个raft写流程,区别是在commit后,不需要apply,即不需要通过raft日志更新状态机。

leader:

  1. 获取putMessageLock锁(自旋或ReentrantLock);
  2. 将message按照原生commitlog序列化,在外边套上raft属性,成为一条raft日志DLedgerEntry;
  3. 同步,写raft日志(data文件)即commitlog,生成index索引,更新最大raft日志信息(term+index);
  4. 异步,向所有follower发送raft日志,返回future;
  5. 释放putMessageLock锁;
  6. future等待raft过半提交;(3s)

follower:

  1. 收到raft日志;
  2. 写raft日志,生成index索引,更新最大raft日志信息(term+index);
  3. 响应leader;

leader:

  1. 收到follower响应写raft日志成功过半;
  2. 更新提交index;---异步更新follower提交index
  3. 设置future结果,唤醒PutMessage线程;

整个过程中,所有写文件都是写pagecache,最终需要FlushDataService线程刷盘。

对于data和index每隔10ms刷盘一次,提交index(checkpoint)每隔3s刷盘一次。

consumequeue构建和消费

为了适配原生commitlog逻辑,consumequeue中的commitlog物理offset还是实际的commitlog位置,即去除raft头后的位置。

缺点

4.x的DLedgerBroker能提供自动failover能力,但是也有一些缺点,所以5.x版本提供了DLedgerController。

RIP-44提出4.x版本的DLedger Broker有如下缺点:

  1. 一个raft组必须至少3副本;
  2. raft写必须过半ack;
  3. 原生commitlog的存储和复制能力无法复用;

对于原生commitlog能力,官方提到两个例子。

一个是transientStorePool,属于异步刷盘的优化项,用于解决异步刷盘时pagecache刷脏页压力过大。

一个是zero-copy,应该指的是主从复制的时候存在内存拷贝。master-slave模式下,master直接从mmap中取buffer写socket,而DLedger模式下,master和slave对于raft日志项DLedgerEntry需要进行多次内存拷贝。

Master下线对客户端的影响

对于producer来说,producer路由无法发现master broker下的队列。

对于consumer来说,只要slave不下线,依然可以发现所有队列。

consumer可以正常进行rebalance、从slave拉消息、提交offset到slave。

对于顺序消费,如果在全局锁过期前,master不上线,consumer将无法继续执行本地消费逻辑。

对于延迟消息,master下线期间无法正常调度到目标队列。

对于事务消息,master下线期间无法发起回查。如果producer发送half消息成功后master下线,producer发送END_TRANSACTION失败,需要等待master恢复后回查后执行二阶段提交或回滚。