Raft需用etcd/raft而非手写,它只管共识;须自实现Storage、网络层和状态机Apply,生产环境推荐RawNode以精确控制同步点与崩溃恢复。

Raft 不是开箱即用的库,Go 标准库不提供 Raft 实现;直接手写完整 Raft 协议极易出错,不建议在生产环境这么做。
用 etcd/raft 而不是自己实现 Raft 核心逻辑
etcd 官方维护的 go.etcd.io/etcd/v3/raft 是最成熟、经过高强度验证的 Go Raft 实现。它只负责「共识」部分(选主、日志复制、提交判断),不包含网络、存储、应用状态机——这正是你该控制的部分。
常见错误现象:raft.Node.Step 返回 nil 但日志没推进、raft.Ready 中 CommittedEntries 为空却期待已应用、忽略 Ready.MustSync 导致数据丢失。
- 必须自己实现
raft.Storage接口:至少支持InitialState()和Entries(),持久化靠你——比如用boltdb或badger存HardState和日志条目 - 网络层需将
Ready.Messages序列化(推荐protobuf)并发送给其他节点;收到远程消息后调用node.Step(ctx, msg) - 每次从
node.Ready()取出的Entries必须按顺序 Append 到本地日志(磁盘 or WAL),再把CommittedEntries逐条 Apply 到你的状态机
raft.RawNode vs raft.Node:选哪个?
raft.Node 是高层封装,自带 goroutine 调度和 channel 消费逻辑,适合快速原型;但隐藏了同步点,容易误以为“调用了 Propose() 就等于已提交”。生产系统更推荐 raft.RawNode——它不启动任何 goroutine,所有流程(tick、propose、step、advance)全由你显式驱动,可控性高、无隐藏并发风险。
使用场景:需要精确控制 WAL fsync 时机、与现有 event loop 集成、或做 determinism testing 时,RawNode 是唯一选择。
-
RawNode.Tick()必须定期调用(如每 100ms),否则选举超时不会触发 -
RawNode.Propose(ctx, data)的data是任意[]byte,由你定义序列化格式(如 JSON 或 protobuf) -
RawNode.Advance(rd)是关键:必须在完成rd.Entries持久化 +rd.CommittedEntries应用后才调用,否则会重复应用
状态机 Apply 的三个硬性约束
Apply 不是简单地执行命令;它必须满足线性一致性、幂等性和顺序性。Raft 本身只保证日志顺序提交,不保证你 Apply 的方式安全。
常见错误现象:并发 Apply 同一个已提交日志、Apply 失败后未回滚导致状态不一致、对空 CommittedEntries 跳过但遗漏了 HardState 更新。
- Apply 必须单协程串行:用一个
chan raftpb.Entry+ for-select 循环,避免并发修改状态 - 每条
Entry的Index必须严格递增且连续;若发现跳变(如上一次 Apply 到 100,这次是 102),说明中间条目丢失或乱序,应 panic 或重建状态机 - Apply 前先检查
entry.Type == raftpb.EntryNormal;跳过EntryConfChange(需单独调用ApplyConfChange)
真正难的不是跑通 Raft,而是让日志落盘、网络重传、状态机恢复这三者在崩溃边界下仍保持一致。比如 WAL.Write() 成功但 fsync() 失败,或者 Apply 成功但进程 crash 在写快照前——这些路径都得有明确的 recovery 逻辑。别指望 Raft 库替你扛住这些。

















