摘要:本文重点梳理 Dledger 文件一致性协议(含 Raft 原理与 RocketMQ 实现)、主从节点切换的高可用集群、BrokerContainer 容器式运行机制。
一、Dledger 文件一致性协议
1. Dledger 高可用集群下的消息一致性问题
RocketMQ 提供两种集群机制:固定角色的主从集群和自行选主的 Dledger 集群。Dledger 集群的主节点由自主选举产生,文件同步比主从同步复杂得多——消息数据不能简单从主节点向从节点同步,而需要在整个集群内达成一致。
分布式数据一致性问题的核心挑战包括:
- 服务稳定性:各个 Server 状态不稳定,随时可能宕机。
- 网络抖动:Server 之间网络抖动会导致请求丢失。
- 网速差异:传输速度不一致,难以保证数据顺序,需要引入操作日志集并保证日志顺序。
- 快速响应:写入响应时间不能简单依赖集群中最慢的节点。
一致性算法分类:
- 弱一致性算法:DNS 系统、Gossip 协议(用于 Fabric 区块链、Cassandra、RedisCluster、Consul)。
- 强一致性算法:Basic-Paxos、Multi-Paxos(含 Raft 系列:Nacos 的 JRaft、Kafka 的 Kraft、RocketMQ 的 Dledger)、ZAB(Zookeeper)。
RocketMQ 的 Dledger 基于 Raft 协议,源自 OpenMessage 开源组织,是一个保证分布式日志一致性的小框架,被用于日志文件同步场景。
2. 理解 Raft 协议的基本流程
Raft 协议分为两个阶段:Election 选举和Log Replication 日志同步。核心是保证所有节点上的 Entry 顺序一致(注意:不是保证 Entry 不丢失)。
基本工作流程:
- 多个 Server 共同选举产生一个 Leader,负责响应客户端请求。
- Leader 通过一致性协议,将客户端指令转发到集群所有节点。
- 每个节点将指令以 Entry 形式保存到自己的 Log 日志中,此时 Entry 为 uncommitted 状态。
- 当多数节点共同保存了 Entry 后,执行 Entry 中的指令并提交到 State Machine 状态机,Entry 更新为 committed 状态。
节点三种状态:
- Leader:选举产生(多数派决定);向 Follower 发送心跳;响应客户端请求;向 Follower 同步操作日志。
- Follower:参与选举投票;同步 Leader 数据;接收 Leader 心跳;长期未收到心跳则转为 Candidate。
- Candidate:没有 Leader 时发起投票,竞选 Leader。
为防止脑裂,Raft 引入Term 任期概念:时间被划分为多个任期,每个任期以选举开始,选举成功后由一名 Leader 管理集群直到任期结束。
从 CAP 理论看,Raft 优先保证 CP,放弃 A;与之对比,Eureka 保证 AP。
说明:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)
状态变化过程:
- 所有节点启动时从 Follower 状态开始。
- 每个 Follower 设定选举过期时间(Election Timeout,通常 150ms-300ms 随机值),超过则转为 Candidate。
- Candidate 开始新任期选举,投自己一票,向其他节点发起投票 RPC 请求。
- 每个节点每个任期有一次投票资格,按规则响应投票请求。
- Candidate 收到超过半数节点同意后转为 Leader,开始发送心跳确认地位。
- 其他节点收到心跳后转为 Follower,等待同步日志;Leader 心跳超时或宕机后触发下一轮选举。
3. Raft 协议的基础实现机制拆解
每个节点需要保存的数据:
- currentTerm:服务器当前任期。
- votedFor:当前任期投票给了谁。
- log[]:日志条目 Entry,包含 Command(客户端指令)、term(任期)、idx(偏移量)。
- commitIndex:已提交 Entry 的索引,记录消息同步进度。
- lastApplied:已执行完 Command 的 Entry 索引,记录往状态机提交的进度(lastApplied ≤ commitIndex)。
Leader 特有参数:
- nextIndex[]:给每个 Follower 同步到了哪一条 Entry。
- matchIndex[]:每个 Follower 已复制到了哪一条 Entry,记录待确认消息。
核心 RPC 请求:
- 投票请求:参数含 term、candidateId、lastLogIndex、lastLogTerm;响应含 term、voteGranted。
- 心跳请求:Leader 发送,不带日志条目。
- 日志同步请求:Leader 发送,带日志条目,支持批量同步;参数含 term、leaderId、entries[]、leaderCommit。
4. RocketMQ 中的 Raft 实现
RocketMQ 的 Dledger 实现要点:
- 节点基础状态:io.openmessaging.storage.dledger.MemberState 记录
selfId:自己的 ID
role: 自己的角色
leaderId
currentTerm:当前 Leader 的任期
currentVoteFor:当前 Term 内,投票给了谁。 一个任期内只能投一次票
ledgerEndIndex:当前节点最后一个Entry 的索引
LedgerEndTerm:当前节点最后一个 Entry 的任期。 - 同步进度:DLedgerEntryPusher 中的 dispatcherMap 相当于 nextIndex[],pendingMap 相当于 matchIndex[]。
- LogEntry 设计:io.openmessaging.storage.dledger.entry.DLedgerEntry,body 传递消息(主要是 CommitLog 的子类 DLedgerCommitLog)。Dledger 集群与主从集群的日志文件不通用,无法直接迁移。
- 状态机:io.openmessaging.storage.dledger.statemachine.StateMachine 接口,记录 lastAppliedIndex 和 CommitIndex,具体实现由 RocketMQ 自行完成。
- RPC 请求:位于 io.openmessaging.storage.dledger.protocol 包下。
二、主从节点切换的高可用集群
Dledger 集群把选举和同步日志一起完成,日志比主从集群大很多,增加写日志的 IO 负担。因此 RocketMQ 5.x 提供Controller 机制:既使用 Raft 选举机制带来的高可用特性,又使用 RocketMQ 原生的 CommitLog 日志。
部署方式参见官网:https://rocketmq.apache.org/zh/docs/deploymentOperations/03autofailover
理解重点:Controller 组件不仅是一个工具,更体现了 RocketMQ 思考问题的方式——在保证高可用的同时,尽量复用原有组件、降低额外开销。
三、RocketMQ 的 BrokerContainer 容器式运行机制
在 RocketMQ 4.x 中,一个 Broker 就是一个进程,主从角色不对等导致服务器资源无法充分利用。RocketMQ 5.x 提供BrokerContainer模式:一个进程中可加入多个 Broker(Master、Slave 或 DledgerBroker),提高单节点资源利用率,通过交叉部署实现节点对等。
启动方式:
bin/mqbrokercontainer -c broker-container.conf核心配置示例:
# 配置端口,用于接收 mqadmin 命令 listenPort=10811 # 指定 namesrv namesrvAddr=worker1:9876;worker2:9876;worker3:9876 # 指定要向 BrokerContainer 内添加的 brokerConfig 路径,多个 config 间用 ":" 分隔 brokerConfigPaths=/app/rocketmq/conf/2m-2s-async/broker-b-s.properties:/app/rocketmq/conf/2m-2s-async/broker-a.properties四、RocketMQ 的集群架构总结
RocketMQ 5.x 相比 4.x 向云原生迈进了一大步,新增三大特性:
- Controller 主从切换集群:Raft 选举 + 原生 CommitLog。
- Container 容器化运行机制:单进程多 Broker,提高资源利用率。
- Proxy 组件:兼容多语言客户端,可与 Broker 组合部署或单独部署;Java 客户端无需启动 Proxy。
Proxy 部署参见官网:https://rocketmq.apache.org/zh/docs/quickStart/01quickstart
五、面试常见提问与解答
Q1:RocketMQ 主从集群和 Dledger 集群有什么区别?
答:主从集群角色固定,由主节点向从节点同步消息,实现简单但主节点故障时需人工或额外机制切换。Dledger 集群基于 Raft 协议,主节点由选举产生,消息需在整个集群内达成一致,具备自动故障转移能力,但日志文件更大,写 IO 负担更重。
Q2:Raft 协议如何保证数据一致性?
答:Raft 通过 Leader 选举、日志复制和提交机制保证一致性。所有写请求由 Leader 处理,Leader 将指令以 Entry 形式复制到多数节点后才提交到状态机。通过 Term 任期和多数派投票防止脑裂,保证同一时刻最多只有一个 Leader。
Q3:Raft 中 Leader 选举的触发条件是什么?
答:Follower 在 Election Timeout(通常 150ms-300ms 随机值)内未收到 Leader 心跳,就会转为 Candidate 并发起新一轮选举。随机超时时间可防止多个节点同时发起选举导致投票分裂。
Q4:Raft 中 committed 和 uncommitted 状态有什么区别?
答:Entry 被保存到节点日志时是 uncommitted 状态,此时尚未执行。当多数节点都保存了该 Entry 后,Leader 将其标记为 committed 并提交到状态机执行。只有 committed 的 Entry 才会真正影响系统状态。
Q5:RocketMQ 5.x 的 Controller 机制解决了什么问题?
答:Dledger 集群日志文件大、写 IO 负担重。Controller 机制将 Raft 选举与日志同步分离,既获得自动故障转移的高可用能力,又保留 RocketMQ 原生 CommitLog 日志格式,降低额外开销。
Q6:BrokerContainer 相比传统部署模式有什么优势?
答:传统模式一个进程只有一个 Broker,主从角色不对等导致资源利用率低。BrokerContainer 允许一个进程运行多个 Broker,支持 Master、Slave、DledgerBroker 混合部署,提高单节点资源利用率,并可通过交叉部署实现节点对等。
Q7:Raft 协议中 nextIndex[] 和 matchIndex[] 的作用是什么?
答:nextIndex[] 记录 Leader 给每个 Follower 同步到了哪条 Entry,用于决定下次发送的起始位置;matchIndex[] 记录每个 Follower 已复制到哪条 Entry,用于判断哪些 Entry 已被多数节点复制,从而推进 commitIndex。
Q8:为什么说 Raft 保证的是 Entry 顺序一致而不是不丢失?
答:Raft 的核心目标是让所有节点以相同顺序应用日志条目,保证状态机最终一致。但网络分区或节点宕机可能导致部分 Entry 未复制到所有节点,这些 Entry 在 Leader 切换时可能被覆盖或丢弃,因此 Raft 不保证 Entry 绝对不丢失。