简介:这是一份基于Raft共识算法实现的分布式可靠KV存储系统完整项目资料,主要面向计算机相关专业学生、开发者以及需要完成课程设计、毕业设计或系统实验的人群。资源整合了项目全部源码、测试文件、配置文件和详细文档,覆盖客户端、服务端、Raft核心模块、网络通信、命令行工具等完整链路,便于理解分布式一致性协议从原理到工程落地的全过程。压缩包共35个文件,以Go源码为主体(25个go),辅以YAML配置文件、Dockerfile、依赖管理及README说明等,整体仅42KB,轻量易部署。已有65人学习下载。项目代码经过实际运行验证,功能稳定,并获导师指导认可,适合直接使用或在其基础上二次开发,也可作为分布式系统进阶学习的参考样例。
1. Raft 共识算法驱动的 KV 存储:这套资料包到底想让你掌握什么
很多人第一次看到“基于 Raft 共识算法的分布式可靠的 KV 存储系统”这个标题,第一反应是:这不就是把 Raft 论文翻译成代码吗?真上手才知道,难的不是共识算法本身,而是把选主、日志复制、状态机、持久化、成员变更全部揉进一个系统里闭环跑通,还经得住故障注入。这份资料包的价值,正是补齐从论文到可运行集群之间的工程空白。它适合两类人:一类在准备面试或做课程设计,想从零搭一个带共识的 KV;另一类在做分布式存储,想对照完整实现检查自己的方案边界。
2. 先看懂 Raft 再动手:选举、日志复制与 KV 状态机的映射
拿到资料包先别急着解压跑 demo。Raft 这个算法最容易被误解的地方是:它不是一个“选举协议”,而是一套以日志复制为核心的共识协议。选举只是为日志复制服务的前置机制。如果你只记住“选主”而忽略“日志”,写出来的系统会在故障切换时丢数据。这一章把三件事讲透:多数派选主、日志提交、状态机应用,以及它们和 KV 接口的对应关系。
2.1 为什么 KV 存储必须用“多数派”而不是“主备”:Term、心跳与随机超时
很多初做分布式的人第一反应是主备切换:主节点负责读写,备节点同步数据,主挂了就把备拉起来。这个方案看似可靠,但有一个致命场景:网络分区。假设三节点集群里节点 A 是 leader,节点 B 和 C 与 A 之间网络中断。隔离区的 A 可能还在接受写入,另一侧的 B 和 C 组成小集群选出了新 leader,两边都认为自己有权写入。等网络恢复,数据已经分叉,而双方都“有理”:A 说自己是旧 leader,B 说自己是多数派刚选出来的。
Raft 的做法是引入 Term(任期编号)和一条简单规则:每个节点在一个任期内只能投一票,候选人必须拿到超过半数(N/2+1)选票才能当选;leader 通过周期性心跳维持权威。网络分区时,多数派仲裁保证同一时间最多只有一个节点能拿到提交所需的确认。注意,“最多一个 leader”不等于“一定有一个 leader”:节点数为偶数且对半分区时,系统会拒绝写入来保证安全。这是 Raft 与其他方案的核心差异——优先保证正确性,可用性让位给多数派。
参数设置上,常见做法是心跳间隔取 50~100ms,选举超时取 150~300ms 且随机化。随机化的目的是防止所有 follower 同时超时并发起选举,导致选票被瓜分。下面是一套常见的初始参数,实际压测后再按集群规模调整:
| 参数 | 典型取值 | 作用与注意点 |
|---|---|---|
| heartbeat_interval | 50~100ms | leader 向 follower 确认存在;太短浪费网络,太长延迟选主 |
| election_timeout_min | 150ms | follower 等待心跳的最短时间 |
| election_timeout_max | 300ms | 实际超时在 min 到 max 之间随机取 |
| 随机化范围 | 心跳的 2~3 倍以上 | 避免多个 follower 同时超时发起选举 |
心跳间隔和选举超时的比值不是玄学,它直接决定网络抖动时集群是“快速恢复”还是“频繁换主”。比值小于 2,一次普通的 GC 停顿就能触发选举;比值大于 8,leader 故障后客户端要等很久才能感知。我一般先按 1:3 起跑,再根据压测结果微调。
2.2 Leader 把一次 put 变成一条日志:从客户端请求到多数派提交的全链路
把 KV 写请求放进来。假设客户端执行put("last_login", 1700000000),一次完整提交的路径是:
- 客户端把请求发给任意节点。如果它不是 leader,会返回重定向响应并附上当前 leader 地址;客户端更新缓存后重新发送。
- leader 把操作包装成一条日志条目,内容至少包括操作类型(PUT)、key、value、当前 term、日志索引(index)。条目先追加到 leader 本地日志,此时处于“未提交”状态。
- leader 并行向所有 follower 发送 AppendEntries RPC,携带上次确认位置之后的新日志。
- follower 收到条目后先做一致性检查——前一条的 index 和 term 是否匹配——匹配则写入本地日志并返回成功。
- leader 收到超过半数节点(包括自己)的成功响应后,把这条日志标记为 committed,应用到状态机,并向客户端返回成功。
这里有两个词必须分清:replicated(已复制)和 committed(已提交)。一条日志被复制到多数派,代表它已经“安全提交”,因为即使当前 leader 立刻崩溃,后续被选为 leader 的节点也一定包含这条日志;这就是 Raft 的安全性。反过来,只被少数节点复制的日志,在 leader 切换后可能被新 leader 覆盖删除,所以它从未真正提交。
还有一个容易错过的细节:follower 对 AppendEntries 的响应会带上自己的 term。如果 follower 的 term 比 leader 大,leader 必须立刻意识到自己过期,主动退位成 follower。这是 Raft 防止“双主”的最后一道防线。我第一次实现时只做了“收到更高 term 的请求才退位”,漏了“收到更高 term 的响应也退位”,结果网络分区恢复后两个节点轮流自认是 leader,查了两天才发现是这里少了判断。
这一节的实操意义:拿到资料包里的源码,先去找 AppendEntries 的处理函数,确认第 4 步的一致性检查用的是“前一条的 index 和 term 都匹配”,第 5 步的多数派计数用的是“节点收到成功的数量”而不是“收到响应的数量”。后者会把节点宕机也算进去,提交条件就错了。
2.3 从日志到数据:Apply 状态机与读请求的一致性边界
日志是“操作序列”,KV 的状态机是“操作结果”。Raft 本身不关心状态机是什么,它只保证所有存活节点把同一条日志按相同顺序应用到状态机。资料包里的实现通常用内存 map 或类 LSM 结构承载状态机,apply 时串行执行,逐条把 PUT/DELETE 落到存储引擎。
读请求的一致性问题最容易被忽略。直觉上从 leader 读就够了——但 Raft 只保证“已提交的日志最终会被 apply”,不保证 apply 一定发生在响应读请求之前。如果 leader 刚完成提交还没来得及 apply,此时读旧值,就是一次线性一致性违例。常见解法有三种:
- ReadIndex:leader 在响应读前确认自己仍是 leader,并且已经 apply 完所有 committed 日志,再执行本地读。
- Lease Read(心跳租约):leader 在租约期内假设没有其他节点能成为 leader,直接本地读,省掉一次多数派确认,但依赖时钟和网络假设,出问题最难排查。
- 从 follower 读:能分担流量,但必须额外校验,否则会读到明显落后的数据。
以 ReadIndex 为例的最小流程:客户端读请求到达 leader → leader 记录当前 commitIndex → 向多数派发一次心跳确认自己仍是 leader → 等待本地 applyIndex 追上 commitIndex → 执行本地读并返回。代价是每次读多一轮心跳 RTT,换来严格的线性一致读。对 KV 场景,我默认先上 ReadIndex,等性能测试确认瓶颈在确认开销时再考虑 Lease Read。
提示:资料包里如果有“详细文档”,优先看它怎么写读一致性。如果文档没提,代码只做了“读 leader”,这个 KV 只适合最终一致场景,放到严格场景会出问题。
3. 把资料包变成可运行的最小集群:三节点部署与客户端读写
第 2 章的理论部分消化后,这一章就能落得很快。标题里的“全部资料+详细文档”说明包里至少有源码、部署文档和某种接口说明。我不会假定你拿到的包长什么样,但这类包的结构高度相似:代码目录、文档目录、配置目录,外加一组配套脚本。按下面的顺序拆包、验证、起三节点集群,通常一个小时内能让put/get跑通。
3.1 拿到资料包后先别跑:四类文件分开,三步最少化验证
很多人的习惯是解压后直接构建,遇到缺依赖再回头找。我的做法是先把文件归档成四类,避免文档被埋进源码:
mkdir -p raft-kv-workspace/{src,docs,conf,scripts} unzip raft-kv-storage.zip -d raft-kv-workspace/tmp find raft-kv-workspace/tmp -maxdepth 2 -type f | head -50 # 把 *.md / *.pdf / *.txt 归入 docs # 把 *.go / *.java / *.cc 归入 src # 把 *.yml / *.toml / *.conf 归入 conf,*.sh 归入 scripts归档之后先做三步最少化验证,每步只回答一个问题:
- 编译通过:执行项目文档里指定的构建命令,确认源码依赖完整。
- 单机模式跑通:很多实现提供单节点模式,或者把
cluster.peers里的节点数临时改成 1。能启动、能 put/get,说明存储引擎和网络链路基本是通的。 - 三节点冷启动:Raft 集群启动时必须先选出 leader,三个节点同时启动时谁也不认识谁,正常现象是 1~2 秒内选出 leader。这里不需要容器,本机起三个进程、用不同端口,就能模拟三节点集群。
为什么强调这个顺序?因为单机失败大概率是配置或代码问题,三节点启动失败大概率是 Raft 层问题——peer 地址配错、心跳端口被防火墙挡、election_timeout太短导致选举完不成。把变量拆开,定位快很多。
3.2 三节点最小配置:心跳、选举超时、数据目录与监听地址
下面是一份最小配置,node1 的完整配置文件长这样,node2 和 node3 只替换node_id、监听端口和数据目录:
# conf/node1.toml node_id = 1 raft_listen = "127.0.0.1:17001" kv_listen = "127.0.0.1:18001" data_dir = "./data/node1" [raft] heartbeat_interval_ms = 100 election_timeout_min_ms = 300 election_timeout_max_ms = 600 sync_on_each_append = true [[peers]] node_id = 1 raft_addr = "127.0.0.1:17001" [[peers]] node_id = 2 raft_addr = "127.0.0.1:17002" [[peers]] node_id = 3 raft_addr = "127.0.0.1:17003"参数说明:heartbeat_interval_ms = 100是 leader 发心跳的周期;election_timeout_min_ms = 300、election_timeout_max_ms = 600表示 follower 等待心跳的超时会在 300~600ms 间随机取值。这里我把选举超时调到了心跳的 3~6 倍,而不是第 2 章表格里的 1.5~3 倍,原因是本机三进程共用 CPU,GC 和日志落盘都会造成心跳抖动,太激进会在压测里频繁换主。真实机器部署可以回到 150~300ms 这一档,但本地验证建议放宽。
sync_on_each_append = true是最保守的配置:每条 AppendEntries 都要写盘并 fsync 后才返回成功。调试阶段必须开,否则你会看到“节点恢复后日志对不上”的诡异问题。raft_listen是 Raft 节点之间的通信端口,kv_listen是暴露给客户端的 KV 服务端口,两者必须分开,否则客户端流量会干扰心跳延迟。
对应的启动脚本:
#!/usr/bin/env bash # scripts/start_cluster.sh set -euo pipefail BASE_DIR="$(cd "$(dirname "$0")/.." && pwd)" cd "$BASE_DIR" for id in 1 2 3; do nohup ./bin/raft-kv-server \ --config "conf/node${id}.toml" \ --log "logs/node${id}.log" \ >/dev/null 2>&1 & done for id in 1 2 3; do for _ in $(seq 1 20); do if curl -s "http://127.0.0.1:$((18000 + id))/health" >/dev/null 2>&1; then echo "node${id} is up" break fi sleep 0.5 done done脚本思路是启动三个进程后轮询各自的/health接口,全部就绪才退出。curl探测端口用18000 + id计算,正好对上 node1 的 18001、node2 的 18002。某个节点一直没起来,去对应日志查监听失败或配置解析报错,这两个是启动阶段最常遇到的问题。
提示:
sync_on_each_append在调试期保持 true,性能测试阶段再改成 false 配合批量刷盘,否则单条写的延迟会明显偏高。
3.3 客户端读写的最小实现:重定向、超时与重试
三节点起来后,第一个客户端只做一件事:写一个 key,读一个 key。下面用 Python 描述通用流程,具体方法名以资料包里的客户端 SDK 为准:
#!/usr/bin/env python3 # scripts/demo_client.py import time import raftkv # 资料包提供的客户端库 endpoints = [ "127.0.0.1:18001", "127.0.0.1:18002", "127.0.0.1:18003", ] client = raftkv.Client(endpoints, timeout=2.0) # 第一次写入前客户端不知道谁是 leader,先探测角色 leader = None for ep in endpoints: info = raftkv.inspect(ep) # 返回 {"role": "leader"/"follower", "term": n} if info["role"] == "leader": leader = ep break if leader is None: raise RuntimeError("no leader elected, check raft logs") print("leader is", leader) for i in range(10): key = f"user:{i}" ok = client.put(key, f"value-{i}") assert ok is True val = client.get(key) assert val == f"value-{i}" time.sleep(0.2) print("10 rounds of put/get passed")这段代码想演示的不是 SDK 用法,而是三个工程要点。
第一,写入前先选 leader。大多数客户端库内部有 leader 缓存,但第一次连接时缓存为空,需要探测。探测要用“角色”而不是“能不能连上”判断,因为 follower 也能连上。第二,写请求重试要有边界。timeout=2.0如果太小,leader 在大量刷盘时响应慢,客户端误判超时后重试,可能把同一条日志提交两次,幂等问题在第 5 章展开。第三,读请求同样要重定向。从 follower 读虽然也能拿到数据,但 follower 落后时会返回旧值,线上客户端需要区分“一致性读”和“允许旧读”两种模式。
到这里,你应该已经能看到三节点集群对外提供put/get服务。但这只是“能跑”,离“可靠”还远。第 4 章直接进入可靠性设计,这是标题里“分布式可靠”四个字的分量所在。
4. 分布式可靠不是靠运气:持久化、快照与成员变更的三个设计点
“能跑”和“可靠”之间隔着一整段工程。三节点集群表面上正常,但只要做一次kill -9再重启,就可能暴露第一个问题:日志写盘了吗?元数据写盘了吗?本章拆开三个工程点:持久化、快照、成员变更。这三个点如果照着“看起来能用”的写法做,等故障现场就知道错了。
4.1 持久化:WAL、fsync 与“半同步”的性能取舍
Raft 日志是分布式 KV 唯一的持久化机制,这一点必须刻在脑子里。每个节点在返回 AppendEntries 成功前,必须把日志条目写入存储并确保进程崩溃后不丢失。这意味着不能只写内存,也不能只靠操作系统 page cache——进程崩溃后 page cache 还在,机器断电就是另一回事。
需要持久化的数据有三类:日志条目、currentTerm、votedFor。后两项经常被忽略,问题也最隐蔽:votedFor丢失后,节点重启可能在同一任期投两次票,破坏选举安全。常见做法是把三者写进同一个 WAL 文件,每追加一条日志或发生一次投票都追加一条记录,避免多文件崩溃后状态不一致。
对应到配置层,就是sync_on_each_append参数。它为 true 时每次追加日志都调一次 fsync,延迟在毫秒到十几毫秒;为 false 时可以攒一批再刷盘,吞吐能涨数倍,但代价是崩溃时丢失“已写入但未刷盘”的日志。Raft 论文要求的语义是每条目在响应前落盘,所以“可靠”实现默认都是 true。
| 持久化对象 | 必要性 | 丢失后的后果 |
|---|---|---|
| 日志条目 | 必须 | 已提交日志丢失等于永久数据丢失 |
| currentTerm | 必须 | 选举机制错乱,可能选出同一任期两个 leader |
| votedFor | 必须 | 同一任期重复投票,破坏选举安全 |
| appliedIndex | 强烈建议 | 重启后重新 apply 已提交日志,要求状态机幂等 |
appliedIndex是只有自己实现状态机才体会得到的坑。有些参考实现重启后从日志开头重新 apply,数据量小时没问题,日志一长就会把恢复时间拖到不可接受。我的习惯是:状态机里记录“当前已应用到哪条日志”的指针,和日志一起持久化。
那么“半同步”值不值得做?如果业务允许少数节点短暂落后或丢失最近几秒数据,可以牺牲每次 fsync,采用“批量刷盘 + 多数派确认”的组合。但一旦这么做,“分布式可靠”就从严格降级成了最终可靠。这个取舍留给业务决定,但你要清楚自己在哪一档。
4.2 快照与日志压缩:节点重启后如何快速追上进度
日志无限增长会让两个场景变慢:新节点加入时要从头同步日志,追平可能需要几小时;老节点重启后 replay 一遍也很久。Raft 的答案是快照加日志截断:对某个 commitIndex 时刻的状态机做全量快照,之后删除该 index 之前的日志。
快照安装有个工程上很敏感的细节:leader 发快照的前提是,它意识到 follower 需要的日志已经被自己截断了,AppendEntries 发不了历史日志。此时 leader 改发 InstallSnapshot RPC,把快照数据传给落后的 follower。follower 的日志虽然落后,但只要快照包含的 index 是它已提交过的,替换就是安全的。
这块最常见的实现问题是快照安装期间阻塞。新手实现通常把“接收快照”和“处理读写请求”放在同一个线程,大快照传几秒钟,节点僵住几秒。对运行中的集群,这意味着该节点读写全部超时,客户端把流量转移后,三个节点可能同时进入“僵住、恢复、再僵住”的循环。改进做法是分块传输加流式写入:leader 按固定大小分块,follower 每收一块写临时文件,全部收完再原子替换当前状态机。收到快照不代表立刻可用,follower 还要追平快照之后的日志,这个窗口内它不应参与一致性读。
4.3 成员变更:加节点和踢节点为什么最容易出幺蛾子
成员变更是比选主更容易踩坑的地方,核心原因是:Raft 的安全性证明建立在“任一时刻全局只能有一个多数派”上,而成员变更是要改变多数派的定义。如果变更中间出现两个不同定义的多数派,就可能同时提交不同日志,造成数据分叉。
最稳妥的方案是单节点变更:一次只增或只减一个节点。比如三节点加一个节点,先把新节点以“非投票成员”身份接入,让它同步日志追到接近 leader 的进度,追平后再提交一条包含新配置的日志。配置日志一旦提交,多数派从 2/3 变成 3/4。注意,新配置提交前,新节点没有投票权,也不能参加选举,否则总票数变成 4 而多数派仍是 2,可用性会出现“两个多数派”的窗口。
踢节点同理。从四节点集群移除一个节点,目标配置变成三节点。如果一次性改成两节点,多数派阈值从 3 变成 2,变更窗口内新旧配置的多数派同时存在,脑裂风险随之而来。不要图省事把 peer 地址一次性改掉。
如果资料包里提供了成员变更接口,先看它是否实现“节点追日志后再加入”的步骤,而不是简单改配置重启。只改配置不追赶日志,新节点一加入就把集群拖慢——它要花大量时间补历史日志,期间写请求的提交延迟会被拉长到不可接受。
5. Raft KV 避坑实录:最容易翻车的 5 个问题与参数修正
前面把理论和部署链路讲完了,这一章集中暴露“我以为没问题,它偏给你翻车”的地方。以下 5 条来自我实现和排查同类系统的血泪经验,每一条按“现象 → 原因 → 解决”的顺序写,你可以直接对照自己的代码和配置查。
5.1 现象:压测一上来就频繁换主,term 值一直在涨
现象:对集群做读写压测,Raft 状态输出里 term 反复增大,leader 频繁变更,客户端大量写超时。原因:压测拉高 CPU 和磁盘占用后,心跳线程被调度延迟,心跳间隔加处理延迟超过选举超时,follower 误判 leader 故障发起选举。多个 follower 超时集中时,选票分散导致重复选举。解决:把选举超时放到心跳间隔的 3~6 倍,随机范围放宽(比如 300~600ms)。同时检查心跳发送和 apply 是否共用同一把锁或线程,如果是,把心跳从业务线程独立出来。这一步通常能直接消掉“压测必换主”的现象。
5.2 现象:网络抖动后,两个节点都自认为是 leader
现象:网络分区恢复后,节点 A 和 B 同时对外响应写请求,两边日志里都有自己提交的记录。原因:最常见的实现错误是旧 leader 收到更高 term 的请求后没有主动退位。Raft 要求节点在任何时候发现自己任期落后——无论从请求还是响应里发现——都要立即转为 follower。很多实现只处理了“收到更高 term 的请求”,漏了“收到更高 term 的响应”。解决:在 AppendEntries 和 RequestVote 的响应处理里都加任期校验:response.term > current_term时无条件更新本地 term 并转为 follower。这个判断只需几行,但就是防双主的关键防线。
5.3 现象:客户端重试之后,同一个 key 的值被覆盖成旧值
现象:一次 put 请求超时,客户端重发,最终结果变成旧值覆盖新值,或者计数器字段被加了两次。原因:请求虽然超时,但前一条请求可能在服务端已经提交成功,只是响应丢了。客户端重试带相同的操作,服务端无法区分“同一次请求”和“新请求”,同一操作被提交两次;两条操作日志顺序交错时,旧值就覆盖新值。解决:客户端为每次写操作生成全局唯一 requestId,服务端在日志条目里带上它,apply 时对最近一段 requestId 去重。更通用的做法是让操作本身幂等,例如put附带版本号。客户端侧至少限制:同一请求重试次数有上限,且重试使用相同 requestId。
5.4 现象:快照安装期间,落后节点还在对外返回“半旧半新”的数据
现象:节点重启后落后很多,leader 正在给它传快照。此时客户端读这个节点,部分 key 是新值,部分 key 是旧值,甚至读不到。原因:快照替换状态机不是原子的。接收节点先把快照写入临时文件,替换完成前,旧状态机和快照文件不一致。解决:接收节点在快照安装完成前,把 KV 服务标记为 unavailable,不响应一致性读。这表面是牺牲可用性,实际是对的:数据分叉的读结果比超时更可怕。如果不能接受这种不可用,就做读写分离,让这部分流量只走已同步节点。
5.5 现象:节点重启后日志还在,但重新选举导致已提交数据丢失
现象:三节点中一个节点重启后 leader 重新选举,随后部分已提交的 key 突然丢失或回滚。原因:日志写盘了,但currentTerm或votedFor没写盘。重启后节点忘记投过谁、当前任期是多少,在旧任期里重复投票干扰选举。另一种可能是appliedIndex没持久化,重启后重新 apply,而状态机操作不是幂等的。解决:把currentTerm、votedFor和日志写进同一个 WAL 文件,追加式写入,不要分两个文件各存一部分。处理 RequestVote 或发现更高任期时,先持久化再更新内存。同时让 apply 幂等,最简单就是记录并持久化 appliedIndex,重启后从该位置继续。
这 5 个现象覆盖选举、日志、客户端、快照、持久化五个维度,是最常出现的五种翻车方式。如果都不中,下一步去读集群状态输出,结合实际故障场景继续定位。第 6 章给你一套验证手段,让这些问题在真正上线前暴露。
6. 用故障注入验证“分布式可靠”:三个实验和一组检查习惯
再充分的代码 review 也替代不了故障注入。这一章给出三个最低成本的验证实验,配合第 5 章的坑,基本能把一套 Raft KV 的底裤翻出来。
6.1 三个最低成本的故障注入实验:kill、网络分区、并发写入
实验一:kill 掉当前 leader,记录从 kill 到新 leader 开始服务的时间,以及期间失败的请求数。
# 先记录集群状态 curl -s http://127.0.0.1:18001/debug/raft | jq '.term, .commit_index, .leader_id' # 找到 leader 后 kill 对应进程,保留日志文件 kill -9 $(pgrep -f raft-kv-server) # 观察另一个节点日志里出现 "become leader" 的时间点 tail -f logs/node2.log实验二比 kill 更有价值。进程崩溃是最简单的故障,网络分区才是制造“伪双主”的温床。用 iptables 把 leader 的出方向包丢弃 10 秒,观察期间集群是否完成选举、恢复后旧 leader 是否主动退位。这个实验直接检验第 5.2 条说的“响应里带更高 term 也要退位”是否实现。
实验三:并发写入与一致性核对。用一批线程持续写入带序号的 key,同时按 30 秒一次的节奏 kill 一个非 leader 节点。恢复后把三个节点的存储数据导出做 diff,所有最终成功的写入都应该出现在每个节点上,且序号不缺失。这个检查不需要读 Raft 内部状态,只需要对比最终 KV 数据。
6.2 把验证固化成回归脚本:不只是上线前跑一次
这三个实验跑完,你已经有了基本验证手段。但更重要的习惯是固化:把每个实验写成脚本,放进项目根目录的scripts/fault_test/下,每次改动 Raft 相关代码后重跑一遍。我见过太多项目只在初次调试时手动做一次故障注入,之后改代码凭感觉认为“应该没影响”,上线后问题爆发。故障注入脚本应该像单元测试一样随代码库演进。
我自己的习惯是:任何涉及选举、日志、状态机 apply 的改动,必须过一遍实验三的并发写入一致性检查才能合入。这套方法不能说帮我避免了所有问题,但至少让大部分问题在合入前就暴露。参数别照抄——不同网络延迟、磁盘性能下,心跳和超时要重新压测再定。希望帮到你。
本文还有配套的精品资源,点击获取