前言
本章分析rocketmq在4.x(4.6.0)版本对于HA的相关实现。
4.x版本HA有两种方式:
- Master-Slave模式
支持SYNC_MASTER(同步复制)和ASYNC_MASTER(异步复制)。
当Master故障,需要手动执行MasterSlave切换或Master重新上线。
- DLedger模式
同名broker部署至少3副本行成raft组。
自主选举Leader,Leader节点成为Master Broker,Follower节点成为Slave Broker,实现故障自动failover。
DLedger模式对于raft实现写的很清晰,比sofa-jraft容易读,但是本文不会深入分析raft的实现细节。
接下来按照这个顺序分析:
- broker启动需要恢复哪些数据,哪里会涉及HA逻辑;
- 分析两种HA实现;
- 客户端在master下线时的表现;
一、铺垫
broker启动可分为三步:
- 创建BrokerController;
- BrokerController初始化;
- BrokerController启动;
从全局看一下BrokerController初始化和启动过程中哪里涉及HA逻辑。
1、broker初始化
BrokerController#initialize:
- 加载磁盘文件到内存;
- MessageStore#load除了加载磁盘文件外,会执行一些恢复逻辑;
- 准备完毕后,开启一些后台线程;(忽略)
加载磁盘文件到内存
下面列举了主要加载的文件:逻辑忽略
| 用途 | 内存 | 磁盘文件 |
|---|---|---|
| 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文件,统计两个数据:
- consumequeue记录中对应commitlog的物理offset的最大值;
- consumequeue自己的物理offset的最大值,用于设置MappedFileQueue的顺序读写位置;
2、broker启动
BrokerController#start:
- MessageStore启动;
- broker服务端NettyServer启动;
- broker客户端NettyClient启动;
- 长轮询处理服务启动;
- 客户端心跳检测服务启动;
- ha部分,master-slave模式config同步;
- 最终开始向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进程正产关闭,执行正常恢复逻辑。
- 仅加载最后3个commitlog文件,每个1g,默认情况下最多有3个g的commitlog被mmap;
- 统计commitlog写到哪,设置到MappedFileQueue;
- 每条消息做crc32校验;
- 入参是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角色的组件:
- AcceptSocketService:master侧,1个线程用于接收slave连接,默认监听端口listenePort+1=10912;
- HAConnection:master侧,每个slave连接对应一个HAConnection,负责与这个slave的数据同步;
- GroupTransferService:master侧,仅当master为SYNC_MASTER时才有用,后面再说;
- 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相关配置项:
- dLegerGroup:一般和brokerName一致,代表一个raft组;
- dLegerPeers:组内成员-地址列表;
- dLegerSelfId:当前实例id;
- sendMessageThreadPoolNums:官方建议配置成cpu核数;
- enableDLegerCommitLog:开启DLedger;
与MasterSlave相比,不需要配置brokerRole、brokerId。
相关组件
DLedgerCommitLog
DLedgerCommitLog是DLedger模式下的核心组件,重写了大部分CommitLog的实现。
DLedgerCommitLog成员变量包括:
- DLedgerServer:负责raft选举和复制;
- DLedgerMmapFileStore:数据存储;
- MmapFileList:DLedgerMmapFileStore中data文件集合;
DLedgerMmapFileStore
基于mmap的DLedger存储实现。
最重要的是两个基于mmap实现的文件集合:
- dataFileList:raft日志,可理解为DLedger模式下的commitlog文件,也是1G一个。区别在于除了消息之外,还有raft数据,比如index、term等概念;
- indexFileList:索引,可以根据raft日志的index快速定位一条raft日志项(DLedgerEntry);
其他关于raft日志的内存字段:
- ledgerBeginIndex/ledgerEndIndex:raft日志的最小和最大index;
- committedIndex:已经提交的raft日志index;
- committedPos:已经提交的raft日志的物理offset;
- 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需要恢复两部分数据:
- data和index文件的写进度;
- 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。
- 变更brokerId和brokerRole;
- 关闭延迟消息调度;
- 关闭事物消息回查;
- 开启config同步(topic配置、消费进度) ;
- 发送心跳,为了更新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:
- leader将原生commitlog封装到DLedgerEntry的body中,调用底层存储写raft日志,这一步是同步处理;
- 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:
- 更新内存中term(任期)-id(当前实例)-index(日志index)关系;
- 构建一个future,放入term-index-future,后期可以通过term-index找到future唤醒;
- 唤醒所有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后开始同步逻辑:
- master:AcceptSocketService线程监听ha端口(10912);
- slave:与master建立连接;
- master:收到slave的socket,创建一个HAConnection,开启一个读socket线程ReadSocketService和一个写socket线程WriteSocketService;
- slave:汇报当前commitlog的物理offset,即同步进度;
- master:ReadSocketService收到slave同步进度,WriteSocketService持续向slave发送commitlog buffer(最大32kb);
- 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:
- 获取putMessageLock锁(自旋或ReentrantLock);
- 将message按照原生commitlog序列化,在外边套上raft属性,成为一条raft日志DLedgerEntry;
- 同步,写raft日志(data文件)即commitlog,生成index索引,更新最大raft日志信息(term+index);
- 异步,向所有follower发送raft日志,返回future;
- 释放putMessageLock锁;
- future等待raft过半提交;(3s)
follower:
- 收到raft日志;
- 写raft日志,生成index索引,更新最大raft日志信息(term+index);
- 响应leader;
leader:
- 收到follower响应写raft日志成功过半;
- 更新提交index;---异步更新follower提交index
- 设置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有如下缺点:
- 一个raft组必须至少3副本;
- raft写必须过半ack;
- 原生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恢复后回查后执行二阶段提交或回滚。