⼀、Dledger⽂件一致性协议
1、Deldger⾼可⽤集群下的消息⼀致性问题
RocketMQ提供了两种集群机制,⼀种是固定⻆⾊的主从集群,另⼀种是⾃⾏选主的Dledger集群。在之前章节中,也带大家简单看了⼀下主从集群的消息同步过程,那么你有没有好奇过Dledger⾼可⽤集群⼜是怎么进⾏消息同步的呢?
很明显的⼀点,Dledger⾼可⽤集群的主节点是⾃主选举产⽣的,他的⽂件同步会要⽐主从同步复杂很多。消息数据不能简单的只是从主节点往从节点同步,⽽是需要在整个集群内达成⼀致。
在⼀个Server集群中,数据写⼊到Server集群中的⼀个节点,然后希望从集群中任何⼀个节点都能读到写⼊的数据,这种问题就称为分布式数据⼀致性问题。
这个问题实现起来会⾯临⼏个核⼼的问题:
- 服务稳定性问题:各个Server状态不稳定,随时可能宕机。
- ⽹络抖动问题:Server之间的⽹络如果发⽣抖动,集群内的某些请求就可能丢失。
- ⽹速问题:数据在Server之间的传输速度不⼀致,难以保证数据的顺序。分布式场景是要保证集群内最终反馈出来的数据是⼀致的。但是数据的变化通常跟操作顺序有关。所以,还需要引⼊操作⽇志集,并保证⽇志的顺序,才能最终保证集群对外数据的⼀致性。
- 快速响应:尽快向客户端给出写⼊操作的响应结果。响应时间不能简单依赖于集群中最慢的节点。
所以,这个不起眼的问题,其实是IT界中⼀个非常有难度的问题。解决这样的问题,也有⼀系列的算法。
- 弱⼀致性算法:DNS系统、Gossip协议(使⽤场景:Fabric区块链,Cassandra,RedisCluster,Consul)
- 强⼀致性算法:Basic-Paxos、Multi-Paxos包括Raft系列(Nacos的JRaft,Kafka的Kraft以及RocketMQ的Dledger)、ZAB(Zookeeper)
这其中,RocketMQ的Dledger集群其实也是基于Raft协议诞⽣的⼀种分布式⼀致性协议。接下来就先了解下Raft协议,再来看看RocketMQ中的Dledger是怎么实现的。
2、理解Raft协议的基本流程
Raft协议是分为两个阶段⼯作的:Election选举和Log Replicaion⽇志同步。也就是说Raft要解决的其实是两个事情,⼀个是集群中选举产⽣主节点。另一个是在集群内进行数据同步。Raft算法的基本⼯作流程是这样的:
这⾥重点是需要理解Log⽇志和State Machine状态机。Log⽇志就是保存在Server上的操作⽇志,其中每个条⽬称为Entry。Entry中的操作,最终都会落地到StateMachine中。Raft算法的核心就是要保证所有节点上的Entry顺序一致。
- 多个Server基于他的⼀致性协议,会共同选举产⽣⼀个 Leader,负责响应客户端的请求。
- Leader 通过⼀致性协议,将客户端的指令转发到集群所有节点上。
- 每个节点将客户端的指令以 Entry 的形式保存到⾃⼰的 Log ⽇志当中。此时 Entry 是uncommited状态。
- 当有多数节点共同保存了 Entry 后,就可以执⾏ Entry 中的客户端执⾏,提交到State Machine 状态机中。此时 Entry 更新为commited状态。
为此,Raft协议给每个节点设定了三种不同的状态,Leader,Follower和Candidate。
- Leader : 1、选举产⽣。多数派决定。2、向 Follower 节点发送⼼跳,Follower 收到⼼跳就不会竞选Leader。3、响应客户端请求。集群内所有的数据变化都从 Leader 开始。4、向 Follower 同步操作⽇志。 具体实现时,有的产品会让发到 Follower 上的请求转发到 Leader 上去。 也有的直接拒绝
- Follower:1、参与选举投票。2、同步 Leader 上的数据。3、接收 Follower 的⼼跳。如果 Follower ⻓期没有发送⼼跳,就转为 Candidate,竞选 Leader。
- Candidate:没有 Leader 时,发起投票,竞选 Leader。
Raft协议为了保证同⼀时刻,集群当中最多只会有⼀个主节点,防⽌脑裂问题,还会增加⼀个Term任期的概念。
时间被划分为多个任期。每个任期都以选举开始。选举成功后,由⼀名Leader管理集群,直到任期结束。有些选举失败了,没有选举出Leader,那就进⼊下⼀个任期,开始下⼀次选举。
他们的状态变化过程是这样的:
- 所有节点启动时都从 Follower 状态开始。
- 每个Follower 设定了⼀个选举过期时间Election Timeout 。Follower持续等待 Leader 的⼼跳请求。如果超过选举过期时间,就转为 Candidate,向其他节点发起投票,竞选 Leader。为了防⽌所有节点在同⼀时间过期,这个选举过期时间通常会设定为⼀个随机值,⼀般在 150ms到 300ms之间。
- Candidate 开始新⼀个任期的选举。每个 Candidate 会投⾃⼰⼀票,然后向其他节点发起投票 RPC 请求。然后等待其他节点返回投票结果。等待时⻓也是Election Timeout。
- 每个节点在每⼀个任期内有⼀次投票的资格。他们会响应 Candidate 的投票 RPC 请求。按照⼀定的规则进⾏投票。返回⽀持 或者 不⽀持。
- Candidate 收到其他节点的投票 RPC 响应之后,会重置他的 Election Timeout,继续等待其他响应。⼀旦某⼀个 Candidate 接收到了超过集群⼀半节点的投票同意结果后,就会转为 Leader 节点。并开始向其他节点发送⼼跳 RPC 请求。确认⾃⼰的 Leader 地位。
- 其他节点接收到 Leader 的⼼跳后,就会乖乖的转为 Follower 状态。 Candidate 也会转为 Follower 。然后等待从 Leader 同步⽇志。直到 Leader 节点⼼跳超时或者服务宕机,再触发下⼀轮选举,进⼊下⼀个 Term任期。
3、Raft协议的基础实现机制拆解
接下来思考下Raft算法要如何实现这些流程呢?这⾥我们主要分析每个节点要保存哪些数据,然后RPC请求要传递哪些数据。
这⾥简单总结Raft算法的基础数据结构:
⾸先是数据:
所有节点都需要的信息:
- currentTerm: 服务器当前的任期
- votedFor:当前任期内投票给了谁。
- log[]:⽇志条⽬Entry。每个 Entry 要包含Command:客户端指令,term:任期,idx:Entry 的偏移量。
- commitIndex:标记为commited的 Entry 的索引。记录消息同步的进度。
- lastApplied:已执⾏完 Command的 Entry 索引。 记录往状态机提交的进度。
- lastApplied<=commitIndex。 这两个主要是提交到状态机需要
Leader 上的特有参数:
- nextIndex[] : 给每个 Follower 同步到了哪⼀条 Entry。记录与follower 的同步进度。
- matchIndex[]:给每个 Follower 已经复制到了哪⼀条 Entry。主要是要记录有哪些 Entry 发给 Follower,正在等待 Follower 确认中。
然后看RPC 请求,最为核心的有三个。第⼀个是 Candidate投票的 RPC 请求。第⼆个是 Leader 发送的⼼跳请求。第三个是Leader 发送的⽇志同步请求。
对于投票请求,主要请求参数
- term : 当前任期
- candidateId: 投票的候选⼈ ID。
- lastLogIndex:候选⼈的最后⽇志Entry 索引。
- last logo term:候选⼈最后⽇志条⽬的任期号。
主要响应参数
- term : 当前任期号
- voteGranted:投票结果。是否⽀持当前 Candidate 当选为 Leader。
4、RocketMQ中的Raft实现
1、每个节点的基础状态
基本跟论⽂中差不多。只不过分在了多个地⽅。
part1:io.openmessaging.storage.dledger.MemberState
核⼼:
- selfId: ⾃⼰的 ID
- role: ⾃⼰的⻆⾊
- leaderId
- currentTerm: 当前 Leader 的任期。
- currentVoteFor: 当前 Term 内,投票给了谁。 ⼀个任期内只能投⼀次票
- ledgerEndIndex:当前节点最后⼀个Entry 的索引
- LedgerEndTerm:当前节点最后⼀个 Entry 的任期
part2、还有⼀个DLedgerEntryPusher,记录了 Leader 的消息同步进度。DLedgerEntryPusher ⾥有⼀个dispatcherMap,⾥⾯记录了每个节点的同步状态。相当于 nextIndex[]。
然后还有⼀个 pendingMap 记录待确认的消息,相当于 matchIndex[]。
// io.openmessaging.storage.dledger.DLedgerEntryPusher 构造⽅法
for (String peer : memberState.getPeerMap().keySet()) {
if (!peer.equals(memberState.getSelfId())) {
dispatcherMap.put(peer, new EntryDispatcher(peer, logger));
}
}
// io.openmessaging.storage.dledger.DLedgerEntryPusher#EntryDispatcher
// doAppendInner⽅法 记录每条消息的等待确认的时间
PushEntryRequest request = buildPushRequest(entry, PushEntryRequest.Type.APPEND);
CompletableFuture<PushEntryResponse> responseFuture = dLedgerRpcService.push(request);
pendingMap.put(index, System.currentTimeMillis());
2、LogEntry的设计
⻅io.openmessaging.storage.dledger.entry.DLedgerEntry 其中的body就是传递的消息。
在RocketMQ中,要传递的消息主要就是CommitLog。实际上是RocketMQ设计的CommitLog下的⼀个⼦类DLedgerCommitLog。
这也就是说RocketMQ的Deldger集群模式下记录的CommitLog⽇志,和主从集群下记录的CommitLog⽇志是不同的。因此,Dledger集群和主从集群,他们的⽇志⽂件是不通⽤的。如果你想要把主从集群升级成Dledger集群,那么⽇志⽂件是无法直接迁移过去的。
3、状态机
Dledger 只保留⼀个接⼝ io.openmessaging.storage.dledger.statemachine.StateMachine 。在状态机中,同样记录了 lastAppliedIndex 和 CommitIndex。其中,lastAppliedIndex 封装在io.openmessaging.storage.dledger.statemachine.StateMachineCaller中。通过这个类来调度触发状态机的对应⽅法。接下来在onCommited⽅法⾥,记录committedIndex,并封装成⼀个 Task,放到队列⾥,排队执⾏。
这个状态机只是定义了⼀些提交Entry的具体操作,具体实现逻辑交由 RocketMQ ⾃⾏实现。
4、RPC 请求
⻅io.openmessaging.storage.dledger.protocol包下的各种 Request 和 Response
⼆、主从节点切换的⾼可⽤集群
从前⾯的分析可以看到, RocketMQ提供的Dledger虽然确实增加了集群的⾼可⽤,但是他是把集群选举和同步⽇志都⼀起完成的。⽽Dledger集群下的⽇志,显然会⽐主从集群⼤很多。这就会增加写⽇志的IO负担。因此,在RocketMQ 5.X的⼤版本中,RocketMQ⼜提供了⼀种Controller机制,即可以使⽤Raft的选举机制带来的⾼可⽤特性,同时⼜可以使⽤RocketMQ原⽣的CommitLog⽇志。
具体部署⽅式,参⻅官⽹ rocketmq.apache.org/zh/docs/dep…
三、RocketMQ的BrokerContainer容器式运⾏机制
在RocketMQ 4.x版本中,⼀个Broker就是⼀个进程。不管是以主从或者Dledger形式部署,⼀个进程中都只有⼀个Broker服务。⽽Broker⼜是分主从的,他们的压⼒式不⼀样的。Broker负责响应客户端的请求,⾮常繁忙。Slave⼀般只承担冷备或热备的作⽤。这种节点之间⻆⾊的不对等会导致RocketMQ的服务器资源没有办法充分利⽤起来。
因此在RocketMQ5.x版本中,提供了⼀种新的模式BrokerContainer。在⼀个BrokerContainer进程中可以加⼊多个Broker。这些Broker可以是Master Broker、Slave Broler或者是DledgerBroker。通过这种⽅式,可以提⾼单个节点的资源利⽤率。并且可以通过各种形式的交叉部署来实现节点之间的对等部署。
BrokeContainer模式的部署⽅式是低啊⽤bin/mqbrokercontainer脚本启动,并通过-c参数指定单独的配置⽂件。
bin/mqbrokercontainer -c broker-container.conf
⾄于配置⽂件具体的配置⽅式,同样可以参⻅RocketMQ给出的配置示例⽂件。conf/container/⽬录下的配置。其中最核⼼的配置就是通过brokerConfigPaths参数,指定多个Broker的配置⽂件,将他们打包成⼀个BrokerContainer执⾏。
#配置端⼝,⽤于接收mqadmin命令
listenPort=10811
#指定namesrv
namesrvAddr=worker1:9876;worker2:9876;worker3:98767
#或指定⾃动获取namesrv
fetchNamesrvAddrByAddressServer=false
#指定要向BrokerContainer内添加的brokerConfig路径,多个config间⽤“:”分隔;
#不指定则只启动BrokerConainer,具体broker可通过mqadmin⼯具添加
brokerConfigPaths=/app/rocketmq/rocketmq-all-5.3.0-bin-release/conf/2m-2s-async/broker-b-s.properties:/app/rocketmq/rocketmq-all-5.3.0-bin-release/conf/2m-2s-async/broker-a.properties