Raft协议实现数据的分布式存储

📅 2026/7/24 5:43:40
Raft协议实现数据的分布式存储
Raft 是一种分布式共识协议。它本身不直接负责把数据存入磁盘而是保证多个节点按照相同顺序执行相同的操作从而让多个节点拥有一致的数据副本。可以把它理解为客户端命令 ↓ Raft 复制日志 ↓ 多个节点按相同顺序执行日志 ↓ 每个节点得到相同的 KV 数据例如PUT user:1001 {name:张三}Raft 要保证所有副本最终都按同样的顺序执行这条命令。一、Raft 实现分布式存储的总体结构一个基于 Raft 的 KV 存储系统通常分为四层客户端 │ ▼ 请求路由层 │ ▼ Raft 共识层 │ ▼ 状态机层 │ ▼ 本地存储引擎具体来说1. 客户端层客户端发送PUT(user:1001, 张三) GET(user:1001)2. Raft 层Raft 负责选举 Leader复制操作日志确认多数节点已经保存日志保证日志顺序一致处理节点故障和 Leader 故障3. 状态机层状态机负责真正执行命令PUT user:1001 张三执行后变成内存状态 user:1001 → 张三4. 本地存储层每个节点可以将 Raft 日志和状态机数据保存到本地磁盘例如WAL 日志SSTableB 树RocksDBLevelDB自定义文件Raft 保证的是“大家执行的命令相同”本地存储引擎负责“如何保存这些命令产生的数据”。二、Raft 集群中的三种角色Raft 节点有三种角色Follower 跟随者 Candidate 候选者 Leader 领导者1. FollowerFollower 不主动处理普通写请求主要负责接收 Leader 的心跳接收 Leader 的日志投票选举保存日志执行已经提交的日志2. Candidate当 Follower 长时间没有收到 Leader 的消息时会认为 Leader 可能失效转换为 Candidate开始发起选举。3. LeaderLeader 负责接收客户端请求追加日志向 Followers复制日志判断日志是否提交通知 Followers 执行日志正常情况下客户端只需要和 Leader 通信。三、写入数据的完整流程假设客户端执行PUT user:1001 {name:张三,age:25}集群结构客户端 │ ▼ Node A Leader / \ ▼ ▼ Node B Node C Follower Follower第一步客户端找到 Leader客户端可能首先连接到 Node B但 B 是 Follower。B 可以返回 Leader 地址将请求转发给 Leader直接拒绝并提示客户端重试最终请求到达 Node A。PUT user:1001 ... │ ▼ Node A Leader第二步Leader 将命令写入日志Leader 不会马上直接修改最终 KV 状态而是先将命令追加到本地日志日志 index term command ----------------------------------------------- 1 3 SET config:x 1 2 4 PUT user:1001 张三 3 5 PUT user:1001 {name:张三,age:25}日志中的几个重要字段Index日志条目的位置1、2、3、4……Term写入这条日志时 Leader 所处的任期。Command真正要执行的操作PUT user:1001 ... DELETE user:1001 INCR stock:1001第三步Leader 向 Followers 复制日志Leader 向 B、C 发送日志A ──日志 index3── B A ──日志 index3── CFollower 收到后会先写入自己的本地日志。Node B保存 index3 Node C保存 index3Follower 此时通常还不能立即执行这条命令因为这条日志还没有被 Leader 确认提交。第四步等待多数节点确认假设 B 成功保存C 暂时宕机A保存成功 B保存成功 C没有响应三个节点中有两个节点已经保存A B 2达到多数派因此该日志可以提交。commitIndex 3第五步Leader 执行状态机Leader 将已经提交的日志交给状态机执行PUT user:1001 {name:张三,age:25}状态机执行后KV 数据 user:1001 → {name:张三,age:25}然后 Leader 向客户端返回成功OK第六步通知 Followers 执行Leader 会在后续心跳或日志同步消息中告诉 FollowersleaderCommit 3B 看到leaderCommit3后也执行 index3Node B user:1001 → {name:张三,age:25}C 恢复后Leader 会先把缺少的日志补给 CC 再执行这些已经提交的命令。四、Raft 中的“提交”和“应用”不是一回事这是一个很重要的概念。日志复制成功表示日志已经保存在足够多的节点上。A、B 已保存 index10日志提交表示 Leader 确认它已经不会丢失commitIndex 10状态机应用表示节点真正执行了命令user:1001 → 张三流程是日志写入 ↓ 达到多数派 ↓ 日志提交 ↓ 状态机应用 ↓ KV 数据发生变化通常每个节点维护两个位置commitIndex已经提交到哪里 lastApplied已经执行到哪里要求lastApplied commitIndex节点会持续将lastApplied 1到commitIndex之间的日志交给状态机执行。五、KV 数据如何与 Raft 状态机结合Raft 只处理命令不直接理解 KV 业务。例如客户端发来PUT user:1 张三系统可以把它编码成Command { type: PUT, key: user:1, value: 张三 }Leader 将这个 Command 写入 Raft 日志LogEntry { index: 10, term: 7, command: PUT user:1 张三 }当日志提交后每个节点都调用相同的状态机stateMachine.apply(command)伪代码可以表示为function apply(command): if command.type PUT: kv[command.key] command.value if command.type DELETE: delete kv[command.key] if command.type INCR: kv[command.key] command.amount由于所有节点拥有相同的日志所有节点按照相同顺序执行状态机逻辑确定性一致所以最终得到的 KV 数据也一致Node Auser:1 → 张三 Node Buser:1 → 张三 Node Cuser:1 → 张三这叫做状态机复制 Replicated State Machine六、读取数据如何处理写请求通常必须发送给 Leader但读请求有多种处理方式。1. 从 Leader 读取最简单的方式GET → Leader这样可以保证读取到最新提交的数据。但 Leader 需要确认自己仍然是当前 Leader否则可能出现旧 Leader 读取旧数据的问题。2. ReadIndexLeader 通过一次心跳确认自己仍然获得多数派支持然后执行读取。适合需要线性一致性的读取。3. Leader LeaseLeader 在一个租约时间内认为自己仍然有效可以直接读取。优点是延迟低缺点是依赖时钟和网络延迟假设使用时需要谨慎。4. 从 Follower 读取可以直接从 Follower 读取但可能读到旧数据客户端写入成功 立即从 Follower 读取 Follower 还没同步完成 返回旧值这种方式称为Stale Read陈旧读取适合对实时一致性要求不高的场景。七、Raft 日志不能无限增长如果所有历史操作永久保存在日志中日志会越来越大PUT a 1 PUT a 2 PUT a 3 PUT b 4 DELETE c ...因此需要快照机制。1. 创建快照当日志达到一定大小时节点将当前状态机状态保存成快照Snapshot a → 3 b → 4然后删除快照之前的旧日志旧日志1 2 3 4 5 6 7 8 9 快照包含1 到 7 保留日志8 92. 新节点加入如果新节点落后太多Leader 不必发送几百万条日志而是直接发送快照Leader ──InstallSnapshot── 新节点新节点恢复快照后再同步快照之后的少量日志。3. 快照和日志的关系可以理解为快照 某个时间点的完整状态 日志 从这个时间点之后的增量操作恢复数据时加载快照 ↓ 重放快照之后的日志 ↓ 得到最新状态八、Raft 如何实现水平扩展一个 Raft 集群通常不应该把全部数据放进一个无限增长的 Raft 日志组否则所有写入都要经过同一个 Leader吞吐量会受限制。更常见的方式是多个 Raft Group例如按照 Key 分片Raft Group 1user:0 ~ user:999 Raft Group 2user:1000 ~ user:1999 Raft Group 3order:0 ~ order:999每个 Raft Group 有自己的 Leader 和副本Group 1A、B、C Group 2D、E、F Group 3G、H、I请求路由层根据 Key 找到对应的 Groupuser:1001 → Group 2 → Group 2 的 Leader这样多个 Group 可以并行处理请求要注意Raft 负责副本一致性 分片负责容量和吞吐扩展 路由层负责把请求送到正确的 Raft GroupRaft 本身并不自动解决数据分片九、一个完整流程图客户端 │ │ PUT user:1001 张三 ▼ 路由层 │ │ 根据 Key 找到 Raft Group ▼ Group Leader │ ├── 追加日志到本地 WAL │ ├── AppendEntries → Follower 1 │ ├── AppendEntries → Follower 2 │ ├── 获得多数派确认 │ ├── 更新 commitIndex │ ├── 应用到本地 KV 状态机 │ ├── 通知 Followers 提交 │ └── 返回客户端成功