写在前面
终于可以写这篇收官了。
说实话,Raft 日志复制那篇(第三篇)发完之后,我拖了很久才敢写第四篇。不是懒,是真的觉得难。commitIndex、lastApplied、matchIndex 这几个索引的关系,我反复看别人的博客、翻 JRaft 的源码、自己写代码调试,才慢慢搞明白。
说实话,Raft 论文我没有逐字逐句啃过,那个写法太学术了。我主要靠参考Nacos,手写代码,遇到问题再回去谷歌和AI。Figure 8 那个场景,就是调试的时候才真正理解的——漏了一个检查,数据就乱了,然后倒回去查,才明白为什么要那么设计。
中间有好几次想放弃,想着"算了,写到日志复制就行了,后面太难了"。
但最后还是硬着头皮写完了。现在回头看,挺庆幸当时没放弃。
这篇是整个系列的收官。不写新代码了,聊聊这个"玩具"离生产环境还有多远,也聊聊我自己在这个过程中学到了什么。
先回顾一下我们干了啥
四篇文章,从零搭了一个能跑的 Raft:
graph LR
A[第1篇<br/>选举] --> B[第2篇<br/>心跳]
B --> C[第3篇<br/>日志复制]
C --> D[第4篇<br/>提交与持久化]
A1[能选出Leader] --> A
B1[Leader不会被踢] --> B
C1[日志能同步] --> C
D1[数据不丢失] --> D
代码开源在 Gitee:gitee.com/sh_wangwanb…
现在这个 Simple Raft 能干什么呢?
- 三个节点组成集群,能自动选举出 Leader
- Leader 挂了,能重新选举
- 写入的数据能复制到多数节点
- 重启不丢数据
听起来还不错?但说实话,离生产环境还差得远。
和生产级实现的差距
我后来花了些时间看 JRaft(Nacos 用的那个)和 etcd 的 raft 实现,发现差距主要在这几个地方。
功能上的差距
成员变更
Simple Raft 的集群节点是写死的,要加节点或者减节点,得停机改配置重启。
生产环境不可能这么搞。JRaft 支持动态成员变更,在线加减节点,业务无感知。这块涉及到 Joint Consensus 或者单步变更,论文里有讲,但我没实现。主要是这个场景在学习阶段用不上,投入产出比不高。
日志压缩
我们的 WAL 是单文件,只增不减。跑久了文件会越来越大,启动要全扫描,越来越慢。
生产环境会定期做快照,然后把快照之前的日志删掉。JRaft 还支持增量快照和并行传输,这块优化很深。
读优化
Simple Raft 所有读写都走 Leader。但其实很多场景读请求量远大于写请求量,全压在 Leader 上扛不住。
生产环境有 ReadIndex 和 LeaseRead 两种优化方案,可以让 Follower 也能处理读请求。这块我还没研究透,后面有时间可能会单独写一篇。
graph TB
subgraph "Simple Raft"
C1[客户端] --> L1[Leader]
L1 --> F1[Follower]
L1 --> F2[Follower]
end
subgraph "生产级实现"
C2[客户端读] --> L2[Leader]
C3[客户端读] --> F3[Follower]
C4[客户端读] --> F4[Follower]
C5[客户端写] --> L2
L2 --> F3
L2 --> F4
end
性能上的差距
这块差距更大。
我们是一条一条复制,生产环境是 batch + pipeline。
Simple Raft 每写一条日志,就发一次 AppendEntries。生产环境会攒一批一起发,而且不等上一批确认就发下一批(pipeline),吞吐量差了一个数量级。
我们每次 append 都 fsync,生产环境用 group commit。
fsync 是很慢的操作,我们每写一条就 fsync 一次。生产环境会把多个写请求合并,一起 fsync,摊薄开销。
我们用 HashMap 存日志,生产环境用 RocksDB。
内存里存日志,重启要全量加载。RocksDB 支持随机读,启动快,而且内存占用可控。
JRaft 官方有 benchmark 数据,三节点集群能跑到几万 QPS。我们这个 Simple Raft,估计几百 QPS 就顶天了。
但这不重要,我们的目的是学习,不是造生产级轮子。
为什么还是值得写
有人可能会问:既然和生产环境差这么多,写这个有什么意义?
我觉得意义在于把抽象的概念变成能跑的代码。
网上讲 Raft 的文章很多,但看文章和写代码是两回事。很多细节文章里一笔带过,真写起来才发现到处是坑。
比如 matchIndex 的初始化。看别人的代码都是初始化成 0,我当时没细想为什么,随手初始化成了 lastLogIndex,结果 Leader 刚当选就以为所有日志都复制了,直接提交。Follower 懵了:我啥也没收到啊。
这种坑,不写代码根本不会遇到。
还有 Figure 8 那个场景,网上很多文章都提到,我当时看了好几篇,觉得自己懂了。但真到写代码的时候,才发现那个 entry.getTerm() == currentTerm 的检查有多重要。漏了这个检查,数据一致性就没了。
sequenceDiagram
participant A as 节点A
participant B as 节点B
participant C as 节点C
Note over A,C: 第1届:A当Leader
A->>A: 写日志[1] term=1
A->>B: 复制日志[1]
B->>B: 收到,存下
Note over A: A挂了,还没提交
Note over A,C: 第2届:C当Leader
C->>C: 写日志[1] term=2
Note over C: C也挂了
Note over A,C: 第3届:A恢复,重新当Leader
Note over A: A能直接提交term=1的日志吗?
Note over A: 不能!必须先写一条term=3的日志
Note over A: 等term=3的日志提交后
Note over A: term=1的日志才算"顺带"提交
写完这个系列之后,我再去看 Nacos 的选举日志,就能看懂了。以前看到 "term changed to 360743" 这种日志,完全不知道什么意思。现在一看就知道:哦,发生了选举,term 涨了。
这就是"拆机式学习"的价值。不是为了造轮子,是为了理解轮子。
Raft 在分布式系统中的地位
写完这个系列,我对 Raft 的认识也更深了一层。
Raft 解决的是分布式共识问题:怎么让多个节点对一系列操作达成一致。这个问题是分布式系统的基石。
你看现在主流的分布式系统,底层几乎都有 Raft 的影子:
graph TB
R[Raft协议] --> N[Nacos]
R --> E[etcd]
R --> T[TiKV]
R --> C[CockroachDB]
R --> K[Kafka KRaft]
N --> N1[服务注册发现]
E --> E1[K8s配置存储]
T --> T1[分布式数据库]
C --> C1[分布式数据库]
K --> K1[消息队列元数据]
- Nacos 用 JRaft 做配置中心的数据一致性
- etcd 用自己的 raft 实现,是 K8s 的底座
- TiKV 用 Raft 做分布式存储引擎
- CockroachDB 用 Raft 做分布式事务
- Kafka 新版本用 KRaft 替代 ZooKeeper
可以说,不理解 Raft,就很难真正理解这些系统的设计。
当然,Raft 不是银弹。它保证的是强一致性,代价是性能和可用性。有些场景不需要强一致,用最终一致性方案(比如 Gossip)反而更合适。
但作为分布式系统的基础知识,Raft 是绑定要学的。
后续计划
这个系列虽然收官了,但 Raft 相关的内容还会继续写。
近期会出一篇 Raft 面试相关的内容。 面试分布式系统岗位,Raft 几乎是必考的。选举流程、脑裂怎么处理、日志复制的一致性保证,这些问题怎么答才能让面试官满意,我整理一下。
还想写一篇 Raft 在 Nacos 中的应用。 Nacos 用的是 JRaft,它在 Nacos 里具体怎么用的,配置数据怎么同步的,选举怎么触发的。结合源码分析一下,应该会很有意思。
Simple Raft 的代码会继续留在 Gitee,有兴趣的可以 fork 去玩。如果后面有时间,可能会加 ReadIndex 优化,但不保证。
写在最后
回顾这个系列,最大的感受是:难的东西,慢慢啃,总能啃下来。
Raft 我最早是 2022 年接触的,当时看了几篇博客觉得自己懂了。后来想写代码实现,发现根本写不出来,又放下了。今年重新捡起来,一篇一篇写,一个坑一个坑踩,终于算是搞明白了。
中间有好几次想放弃。特别是第四篇,commitIndex 那块逻辑,我调了整整两天才跑通。当时真的很崩溃,觉得自己是不是不适合搞这个。
但现在回头看,那些崩溃的时刻,恰恰是成长最快的时刻。
如果你也在学 Raft,遇到困难很正常。建议多画图,多跑测试,看着日志输出去理解。代码已经开源了,clone 下来跑一跑,比光看论文有用得多。
这个系列就到这里了。感谢一路陪伴的读者,你们的点赞和评论是我继续写下去的动力。
最后,想问问大家几个问题:
- 你们生产环境用的是什么共识协议?Raft、Paxos、还是 ZAB?遇到过什么坑?
- 对于 Raft 面试题,有没有什么问题是你觉得特别难答的?
- Nacos 那篇你们感兴趣吗?想看源码分析还是想看使用踩坑?
评论区聊聊,我看到都会回复。
如果觉得这个系列对你有帮助,点个 Star 吧,这对我很重要。
下个系列见。