Raft 共识算法 · 演示系统(多终端)
精简版 Raft 实现,用于高级操作系统课程汇报现场演示。
每个节点作为独立进程运行在各自的终端窗口中,通过控制台统一管理。
实验介绍
为什么选择 Go
本实验参考 MIT 6.5840(原 6.824)Lab 3 的设计思路,使用 Go 语言 实现。选择 Go 的原因:
- 天然并发模型:Go 的 goroutine + channel 是实现分布式节点间并行通信的最简洁方式------每个 RPC 调用、每次投票收集、每轮心跳发送都是一个 goroutine,代码直接映射论文描述,无需手动管理线程池
- 标准库即够用 :
net/rpc提供开箱即用的 RPC 框架,net/http提供管理 API,sync.Mutex提供互斥锁------零外部依赖,go.mod中没有任何第三方库 - 编译为单二进制 :
go build直接生成可执行文件,无需运行时环境,适合现场演示时快速部署
架构设计
采用 多进程 + 双通道通信 架构,刻意模拟真实分布式系统的部署方式:
┌──────────┐ RPC(TCP) ┌──────────┐ RPC(TCP) ┌──────────┐
│ Node 0 │◄──────────►│ Node 1 │◄──────────►│ Node 2 │
│ :9001 │ │ :9002 │ │ :9003 │
│ :8001 │ │ :8002 │ │ :8003 │
└────▲─────┘ └────▲─────┘ └────▲─────┘
│ HTTP │ HTTP │ HTTP
└───────────────┬───────┴───────────────────────┘
│
┌──────┴───────┐
│ Controller │
│ (控制台) │
└──────────────┘
- 节点间通信 :Go
net/rpcover TCP------这是 Raft 论文中 RPC 的直接实现(RequestVote、AppendEntries) - 管理通道:每个节点额外暴露 HTTP API,供控制台查询状态、提交命令、触发快照、模拟宕机
- 进程隔离:每个节点是独立进程,拥有独立的内存空间、独立的计时器、独立的终端输出------这不是模拟,而是真正的进程间通信
核心设计思想
- 论文驱动:每个函数、每个字段都能在 Raft 论文 Figure 2 / §5 / §7 中找到对应。代码中的中文注释标注了论文原文出处,便于对照阅读
- 可观测性优先:颜色按节点固定(而非按角色)、选举计时器可见、候选者自投票显式输出、心跳计数------所有设计决策都服务于"让观众看懂发生了什么"
- 最小完备:仅实现 Leader Election + Log Replication + 简化 Snapshot 三个核心子问题,总代码约 830 行,每个文件职责单一,不超过 370 行
快速启动
powershell
cd raft-demo-v2
# 方式一:一键启动(自动编译 + 打开3个节点窗口 + 控制台)
.\demo\start_cluster.ps1
# 方式二:手动启动
go build -o node.exe ./node
go build -o controller.exe ./controller
# 终端1: Node0 (红色)
.\node.exe -id 0 -rpc-port 9001 -http-port 8001 -peers 1:9002,2:9003
# 终端2: Node1 (绿色)
.\node.exe -id 1 -rpc-port 9002 -http-port 8002 -peers 0:9001,2:9003
# 终端3: Node2 (蓝色)
.\node.exe -id 2 -rpc-port 9003 -http-port 8003 -peers 0:9001,1:9002
# 终端4: 控制台
.\controller.exe
控制台命令
| 命令 | 说明 |
|---|---|
status |
查看所有节点的 Term、角色、在线状态 |
put x 5 |
向 Leader 提交 SET x 5 命令 |
get x |
查询各节点状态机中 x 的值 |
kill 0 |
模拟 Node0 宕机(关闭进程) |
log |
查看各节点日志列表 |
snapshot 0 |
触发 Node0 日志压缩 |
snapshot-info 0 |
查看 Node0 快照信息 |
demo1 |
预设演示:Leader 宕机 → 重新选举 |
demo2 |
预设演示:日志复制流程 |
demo3 |
预设演示:多数节点宕机 → 集群停服 |
演示流程建议(15分钟)
场景1:正常选举(3min)
- 启动集群,观察各节点终端:选举计时器 → 超时 → 成为 Candidate → 投票给自己 → 获得多数票 → 当选 Leader
- 在控制台输入
status确认角色分配
场景2:Leader 宕机重选(3min)
- 输入
demo1或手动kill <leader_id> - 观察其他节点终端:心跳停止 → 计时器到期 → 发起新选举 → 新 Leader 当选
- 输入
status查看新 Leader(Term +1)
场景3:日志复制(4min)
- 输入
demo2或手动put x 1、put y 2 - 观察各节点终端:Leader 写入日志 → 发送 AppendEntries → Follower 确认 → 多数确认提交
- 输入
log查看各节点日志一致,get x查看状态机一致
场景4:日志压缩(2min)
- 输入
snapshot 0触发 Node0 日志压缩 - 输入
log对比:Node0 日志已截断,其他节点保留完整日志 - 输入
snapshot-info 0查看快照包含的状态
场景5:多数宕机停服(3min)
- 输入
demo3或手动kill两个节点 - 观察:剩余节点无法成为 Leader(无法获得多数投票)
- 说明 CAP 定理:一致性优先于可用性
v2 相比 v1 的改进
| 改进点 | v1 | v2 |
|---|---|---|
| 颜色方案 | 按角色着色(混乱) | 按节点固定:Node0红, Node1绿, Node2蓝 |
| 终端架构 | 单进程混合输出 | 多终端:每个节点独立窗口 + 控制台窗口 |
| 选举可视化 | 无计时器信息 | 显示计时器超时时间和到期触发 |
| 候选者自投票 | 无显示 | 明确输出"投票给自己 (得票: 1/3, 需要: 2)" |
| 日志压缩 | 未实现 | snapshot 命令:截断已提交日志 + 保留快照状态 |
| 心跳日志 | 每次都输出 | 每10次输出一次,减少刷屏 |
代码结构
raft-demo-v2/
├── raft/
│ ├── raft.go # 核心结构体、状态转换、选举/心跳循环 (~370行)
│ ├── election.go # RequestVote RPC(对应论文 Figure 2)(~140行)
│ ├── replication.go # AppendEntries RPC + 日志提交(对应论文 Figure 2)(~240行)
│ ├── snapshot.go # 简化版日志压缩(对应论文 §7)(~60行)
│ └── log.go # LogEntry + Snapshot 结构体 (~24行)
├── node/
│ └── main.go # 节点独立进程(Raft + HTTP管理API)(~170行)
├── controller/
│ └── main.go # 控制台进程(交互式CLI)(~370行)
├── demo/
│ ├── start_cluster.ps1 # 一键启动集群
│ └── stop_all.ps1 # 停止所有节点
├── go.mod
└── README.md
关键实现对应论文
- Figure 2 State :
raft.go中的Raft结构体字段 - Figure 2 RequestVote :
election.go中的RequestVote()和startElection() - Figure 2 AppendEntries :
replication.go中的AppendEntries()和sendAppendEntries() - Figure 4 状态转换 :
raft.go中的becomeFollower/becomeCandidate/becomeLeader - §5.4.2 提交规则 :
replication.go中updateCommitIndex()只提交当前 Term 的日志 - §7 日志压缩 :
snapshot.go中TakeSnapshot()截断已提交日志
截图:
