tongyifan的博客

后端开发 / 云原生 / 分布式系统 · 记录学习与思考

← 返回归档

从 Raft 到 Etcd:一致性协议的工程视角

2026-06-30 · 15 分钟 · 分布式Etcd

Raft 论文的好处是它把一致性讲得非常干净:leader 选举、日志复制、安全性,三块拼起来就是一个可证明正确的协议。但当你打开 Etcd 的源码,会发现论文里那些"显然"的部分,在工程实现中占据了不到三成的代码量。剩下的七成,全在处理论文里一句话带过的异常路径。

raft 库的边界:它不做 IO

这是理解 go.etcd.io/raft 的第一个关键点,也是最容易困惑的地方。raft 库是一个纯状态机:你通过 Node.Propose() 喂进去提案,通过 Node.Ready() 拿到一个 Ready 结构体,里面装着"你现在应该持久化哪些日志、应该给谁发什么消息、哪些日志已经 commit 可以 apply"。

所有的磁盘写入、网络发送、apply 到业务状态机,都由调用方(etcdserver)自己完成,完成后再调 Advance() 告诉 raft 库"我处理完了,可以给我下一个 Ready"。

// etcdserver/raft.go 的主循环骨架(简化)
for {
    select {
    case rd := <-r.Ready():
        // 1. 先持久化 HardState 和 Entries(顺序不能反)
        r.raftLog.Save(rd.HardState, rd.Entries)
        // 2. 发送消息
        r.send(rd.Messages)
        // 3. apply 已提交的日志
        r.applyAll(rd.CommittedEntries)
        // 4. 通知 raft 可以继续
        r.Advance()
    }
}

这里第 1 步和第 3 步的顺序是正确性要求而不是性能优化:如果在持久化之前就 apply,进程崩溃重启后会出现"状态机已经变了但日志丢了"的不一致。

选举:论文里的随机超时,工程里的三重保险

论文说"每个 follower 在 150–300ms 之间随机选一个超时时间"。Etcd 的实现除了这个随机化,还加了几个东西:

PreVote 和 CheckQuorum 必须一起开。只开 CheckQuorum 会让被分区的 leader 反复降级又反复参选,反而加剧抖动。

日志复制:一个被误解的优化

很多人以为 leader 会给每个 follower 维护一个发送窗口做流控。Etcd 确实有 MaxInflightMsgs,但它控制的是未确认的 AppendEntries 数量,不是字节数。这意味着如果单条 entry 很大(比如一次大的事务),inflight 限制就形同虚设。

所以 etcd 在更上层做了 --max-request-bytes(默认 1.5MB)的硬限制。这也是为什么"etcd 不适合存大 value"不是一句经验之谈,而是协议实现层面的直接约束。

线性一致读:ReadIndex 与 LeaseRead

Raft 论文第 6.4 节提到读操作也要走日志才能保证线性一致,但那样代价太高。Etcd 用了两个优化:

  1. ReadIndex:leader 记录下当前的 commit index,然后向多数派发一轮心跳确认自己还是 leader,等到 apply index 追上 commit index 后再返回读结果。省掉了写日志。
  2. LeaseRead:更进一步,leader 认为自己在一个租约期内必然是 leader(因为 follower 的选举超时更长),连心跳都省了。

LeaseRead 快,但它依赖时钟假设。如果 leader 发生长时间 STW GC 或者虚拟机被暂停,租约可能实际已过期而 leader 不自知,此时会读到陈旧数据。Etcd 默认开启 LeaseRead,这是一个用正确性换性能的显式取舍——生产环境如果对线性一致读有强要求,值得重新评估这个默认值。

读源码的建议路径

不要从 etcdserver/server.go 开始,那里全是胶水代码。推荐顺序:

  1. raft/raft.go 里的 stepFollower / stepCandidate / stepLeader 三个函数——协议的全部状态转移都在这里,且和论文一一对应。
  2. raft/log_unstable.goraft/tracker.go——理解日志管理和 per-follower 进度。
  3. raft/node.gorun 循环——理解 Ready 机制。
  4. 最后才回到 etcdserver,看它怎么驱动这个状态机。

配合 raft/testutil 里的网络模拟器写几个测试,比单纯读代码有效得多。你可以人为制造分区、丢包、延迟,然后观察 term 和 commit index 的变化——这比任何博客讲得都清楚。