这是一个使用 Go 语言实现的分布式 KV 存储项目。系统通过 Raft 协议在多个节点之间复制日志,保证多数派节点提交后的数据一致。
本项目已经实现以下能力:
- Raft 三种角色:
Follower、Candidate、Leader - Leader 选举、随机选举超时、投票机制、心跳维持
- 日志复制和
commitIndex推进 - 基于
prevLogIndex和prevLogTerm的日志一致性校验 - Leader 通过
nextIndex回退机制修复 Follower 落后或冲突日志 - KV 状态机接口:
Get、Put、Delete - 写请求先进入 Raft 日志,多数派提交后再应用到 KV 状态机
- 基于
clientId + requestId的客户端请求去重,避免重试导致重复写入 - 使用 bbolt 做持久化,保存 term、vote、log、snapshot 等状态
- Snapshot 快照和日志截断,降低日志膨胀
- gRPC 服务接口
- Protobuf 接口定义
- Prometheus 指标暴露
- 内置节点状态页面,可在浏览器查看角色、Leader、term、commitIndex 和 Snapshot 状态
- 客户端命令行工具
raftkvctl - YAML 配置文件启动
- Docker Compose 一键启动三节点集群和 Prometheus
- Follower 自动转发读写请求到 Leader,避免读旧副本
- Leader 读请求先确认多数派心跳,再读取状态机,提供更强的读一致性
- 写请求必须多数派提交后才返回成功,少数派不可提交
- 单元测试覆盖选主、复制、删除、冲突修复、重复请求、重启恢复、Leader 故障、网络分区和 Snapshot
- 真实 TCP gRPC 集成测试覆盖客户端读写和 Follower 转发
- 端到端 demo 脚本覆盖启动集群、读写、幂等请求和 Leader 故障恢复
- GitHub Actions 自动运行测试和构建
- 简单压测脚本和 Grafana Dashboard 示例
- Go
- gRPC
- Protobuf
- bbolt
- Prometheus
api/raftkv.proto
Protobuf 接口定义,描述 Raft 节点之间通信和客户端 KV 操作的 RPC 接口。
cmd/raftkv/
节点启动入口。运行这个目录下的程序可以启动一个 RaftKV 节点。
cmd/raftkvctl/
客户端命令行工具,可以执行 put、get、delete。
configs/
本地和 Docker 环境下的节点配置文件。
deploy/
Prometheus 配置文件。
deploy/grafana-dashboard.json
Grafana Dashboard 示例,可以导入后观察提交日志、应用日志、角色变化和复制错误。
scripts/
本地一键启动三节点集群脚本。
internal/raft/
Raft 核心实现,包括选举、日志复制、状态机、持久化、快照和测试。
internal/server/
gRPC 服务层,把外部 RPC 请求转成 Raft 节点内部调用。
internal/pb/
由 api/raftkv.proto 生成的标准 Protobuf 和 gRPC Go 代码。
在项目根目录执行:
go test ./...如果测试通过,会看到类似输出:
ok github.com/juanjuandog/mini-raftkv/internal/raft
ok github.com/juanjuandog/mini-raftkv/internal/server
其中:
internal/raft测试 Raft 核心算法internal/server测试真实 TCP gRPC 集群和 Follower 读写请求转发
项目使用 buf 调用 protoc-gen-go 和 protoc-gen-go-grpc 生成代码:
make proto生成目标:
internal/pb/raftkv.pb.go
internal/pb/raftkv_grpc.pb.go
make build编译后会生成:
bin/raftkv
bin/raftkvctl
分别打开三个终端:
make run-n1make run-n2make run-n3make run-cluster这个命令会同时启动 n1、n2、n3,并把日志写到:
logs/n1.log
logs/n2.log
logs/n3.log
也可以打开三个终端,分别执行下面三个命令。
启动节点 n1:
go run ./cmd/raftkv \
-config configs/n1.yaml启动节点 n2:
go run ./cmd/raftkv \
-config configs/n2.yaml启动节点 n3:
go run ./cmd/raftkv \
-config configs/n3.yaml三个节点启动后,会自动进行 Leader 选举。
启动集群后,可以用 raftkvctl 写入数据:
go run ./cmd/raftkvctl -addr 127.0.0.1:7001 put name raftkv读取数据:
go run ./cmd/raftkvctl -addr 127.0.0.1:7002 get name删除数据:
go run ./cmd/raftkvctl -addr 127.0.0.1:7003 delete name即使请求打到 Follower,读写请求也会根据已知 Leader 自动转发。
如果要验证幂等去重,可以重复使用同一个 client 和 request:
go run ./cmd/raftkvctl -addr 127.0.0.1:7001 -client c1 -request 100 put idempotent first
go run ./cmd/raftkvctl -addr 127.0.0.1:7001 -client c1 -request 100 put idempotent second
go run ./cmd/raftkvctl -addr 127.0.0.1:7001 get idempotent结果仍然应该是:
first
除了浏览器状态页面,也可以直接用客户端查看节点状态:
go run ./cmd/raftkvctl status -http 127.0.0.1:9001查看三节点集群:
go run ./cmd/raftkvctl cluster -http-addrs 127.0.0.1:9001,127.0.0.1:9002,127.0.0.1:9003持续刷新,适合观察 Leader 切换:
go run ./cmd/raftkvctl watch -http-addrs 127.0.0.1:9001,127.0.0.1:9002,127.0.0.1:9003输出会包含节点地址、节点 ID、角色、Leader、term、commitIndex、lastApplied 和 snapshotIndex。
make demo这个脚本会自动完成:
- 编译
raftkv和raftkvctl - 启动本地三节点集群
- 执行
put/get/delete - 验证重复请求不会覆盖第一次写入
- 杀掉当前 Leader
- 等待重新选主
- 在新 Leader 上继续写入
运行期间可以打开浏览器查看节点状态:
http://127.0.0.1:9001/debug/ui
http://127.0.0.1:9002/debug/ui
http://127.0.0.1:9003/debug/ui
也可以直接查看 JSON 状态接口:
http://127.0.0.1:9001/debug/status
日志会输出到:
logs/n1.log
logs/n2.log
logs/n3.log
如果只想重点演示故障恢复,可以运行:
make failover-demo这个脚本会自动完成:
- 启动三节点集群
- 发现当前 Leader
- 写入一条数据
- 杀掉 Leader
- 等待新 Leader 选出
- 继续写入新数据
- 验证故障前后的数据仍然可以读取
运行时可以同时打开节点状态页面,观察 Leader 和 term 的变化。
集群启动后可以执行:
make bench默认向 127.0.0.1:7001 写入 100 个 key。可以通过环境变量调整:
ADDR=127.0.0.1:7002 N=1000 make bench输出示例:
addr=127.0.0.1:7001 requests=100 ok=100 fail=0 seconds=1 qps=100
make docker-up这会启动:
- n1
- n2
- n3
- Prometheus
停止并清理:
make docker-downPrometheus 页面:
http://127.0.0.1:9090
启动节点后,可以在浏览器访问:
http://127.0.0.1:9001/metrics
http://127.0.0.1:9002/metrics
http://127.0.0.1:9003/metrics
目前暴露的指标包括:
- Raft 角色切换次数
- 已提交日志数量
- 已应用日志数量
- Snapshot 生成次数
- 日志复制失败次数
项目内置了一个轻量的节点状态页面,不需要额外前端服务:
http://127.0.0.1:9001/debug/ui
页面会自动刷新并展示:
- 当前节点 ID
- 当前角色:Follower、Candidate 或 Leader
- 当前 Leader
- currentTerm
- commitIndex
- lastApplied
- lastLogIndex / lastLogTerm
- snapshotIndex / snapshotTerm
- peer 的 nextIndex / matchIndex
如果只想看 JSON,可以访问:
http://127.0.0.1:9001/debug/status
写请求不会直接修改本地 KV,而是先追加到 Leader 的 Raft 日志。只有当日志被多数派复制并提交后,才会按顺序 apply 到状态机并返回成功。
读请求也不会从任意 Follower 直接读取。Follower 收到读请求会转发给 Leader;Leader 在读取本地状态机前,会先向多数派发送心跳确认自己仍然拥有领导权。如果 Leader 被网络隔离到少数派,读写都会返回 quorum unavailable,避免读到旧 Leader 的过期数据。
对应测试位于:
internal/raft/cluster_test.go
重点测试包括:
TestLeaderRequiresQuorumToCommitWritesTestLinearizableReadRequiresQuorumTestLinearizableReadSucceedsWithMajority
Raft 相关逻辑集中在:
internal/raft/node.go
internal/raft/types.go
节点在 Follower、Candidate、Leader 三种状态之间切换。选举过程使用随机超时、投票请求和日志新旧比较;Leader 定期发送心跳,并通过 commitIndex 推进已提交日志。
Leader 复制日志时会带上 prevLogIndex 和 prevLogTerm。Follower 如果发现前置日志不匹配,会拒绝本次追加;Leader 根据响应回退对应节点的 nextIndex,直到找到双方一致的位置,再继续复制后续日志。
相关代码:
internal/raft/node.go
KV 状态机位于:
internal/raft/state_machine.go
Put 和 Delete 请求会先进入 Raft 日志,只有日志被多数派提交后才会应用到状态机。Get 请求由 Leader 在确认多数派后读取本地状态机;Follower 收到客户端请求时会转发给已知 Leader。
写请求携带 clientId 和 requestId。状态机会记录每个客户端已经处理过的请求,重复请求会直接返回之前的处理结果,避免客户端重试造成重复写入。
持久化实现位于:
internal/raft/storage.go
节点会保存 term、vote、日志和 Snapshot 状态。Snapshot 生成后会截断已经包含在快照中的旧日志,重启时可以从持久化状态恢复已提交数据。
核心测试位于:
internal/raft/cluster_test.go
internal/server/grpc_integration_test.go
覆盖的场景包括选主、日志复制、日志冲突修复、客户端请求去重、Leader 故障恢复、Follower 重启追赶、Snapshot 安装、少数派不可提交,以及真实 gRPC 读写转发。
api/raftkv.proto 是项目的 RPC 契约。当前仓库已经使用 buf 和 Go Protobuf 插件生成了标准代码:
internal/pb/raftkv.pb.go
internal/pb/raftkv_grpc.pb.go
重新生成命令:
make proto