面试官问“Leader 挂了怎么办”,很多人会立刻开始背:
心跳超时、Candidate、RequestVote、多数派、新 Leader。
这些词没有错,但只背这些,面试官无法判断你到底有没有真的做过分区或节点故障。
更稳的做法不是先解释,而是先复现。
这篇文章给你一条可以直接照着走的 Windows 操作路径:
```text
启动 3 个节点
-> 写入并读回 key
-> 查看 Leader 和 commit/applied
-> 停掉 node-a
-> 从 node-b、node-c 继续读写
-> 最后再解释为什么
```
下面所有输出都来自 RaftKV 在 Windows 上的真实验收,不是只画一张 Raft 示意图。
## 一、先确认你测的是什么,不是什么
这次测的场景是:
- 三节点集群;
- 每个分片有三个副本;
- 停掉一个节点 `node-a`;
- 剩余两个节点仍然在线;
- 原 key 可以继续读;
- 新 key 可以继续写。
这次不证明:
- 两台节点同时挂掉还能继续服务;
- Redis、etcd、TiKV 的生产级替代能力;
- 跨机房容灾、自动运维或者零请求失败。
把边界先说清楚,后面的结果才不会被夸大。
## 二、第 1 步:把三节点启动起来
解压 RaftKV 15 分钟试跑包,在根目录打开 PowerShell:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\start-trial.ps1
```
脚本会启动三个独立进程:
```text
node-a http://127.0.0.1:19090
node-b http://127.0.0.1:19091
node-c http://127.0.0.1:19092
```
预期看到:
```text
status ok
cluster RaftKV Trial 2026-09-06
shards 2
RaftKV trial healthy.
```
`status=ok` 说明进程和接口已经起来,但不代表选举已经全部完成。
再检查一次 Leader 数量:
```powershell
Invoke-RestMethod `
-Uri 'http://127.0.0.1:19090/api/v1/health' |
ConvertTo-Json
```
选举完成后应看到:
```text
leaders 2
shards 2
status ok
```
如果 `leaders` 还是 0,等待几秒再查一次。选举没有完成时,不要急着判断写入失败。
如果 60 秒还没有 healthy,先不要继续下一步,直接看日志:
```powershell
Get-Content .\logs\node-0.err.log -Tail 50
Get-Content .\logs\node-1.err.log -Tail 50
Get-Content .\logs\node-2.err.log -Tail 50
```
常见原因只有几个:端口被占用、三个进程没有全部启动、旧数据和新配置不一致。
## 三、第 2 步:先写入一个 key,再从接口读回来
打开另一个 PowerShell,先准备地址和密钥:
```powershell
$base = 'http://127.0.0.1:19090'
$admin = @{ 'X-API-Key' = 'admin-trial-20260906' }
$read = @{ 'X-API-Key' = 'read-trial-20260906' }
```
写入一个 key:
```powershell
$body = @{
key = 'trial:user:1001'
value = 'RaftKV-ok'
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri "$base/api/v1/put" `
-Headers $admin `
-ContentType 'application/json' `
-Body $body
```
读取同一个 key:
```powershell
Invoke-RestMethod `
-Uri "$base/api/v1/get?key=trial%3Auser%3A1001" `
-Headers $read |
ConvertTo-Json
```
本次验收返回:
```json
{
"found": true,
"key": "trial:user:1001",
"shard": "shard-1",
"value": "RaftKV-ok"
}
```
先写入、再读回,不是多余步骤。
后面停掉节点以后,你会用同一个 key 再读一次。只有这样,才能证明“故障前确实有一份已提交数据”。
## 四、第 3 步:先看清 Leader、commit 和 applied
故障前的集群状态:
```powershell
Invoke-RestMethod `
-Uri "$base/api/v1/cluster/status" `
-Headers $read |
ConvertTo-Json -Depth 8
```
本次验收中,三个节点都有两个分片副本。
`shard-0`:
```text
node-b state=leader term=5 commit=4 applied=4
node-a state=follower term=5 commit=4 applied=4
node-c state=follower term=5 commit=4 applied=4
```
`shard-1`:
```text
node-a state=leader term=3 commit=3 applied=3
node-b state=follower term=3 commit=3 applied=3
node-c state=follower term=3 commit=3 applied=3
```
这里先不用背 Raft 论文,只要记住三个检查位置:
- `leader`:这个分片当前由谁负责。
- `commit`:日志已经提交到哪里。
- `applied`:状态机已经应用到哪里。
正常服务时,`commit` 和 `applied` 不能长期脱节。
## 五、第 4 步:真的停掉 node-a,不要改页面状态
先读取启动脚本写下的 PID:
```powershell
$pids = Get-Content .\data\trial\cluster-pids.json | ConvertFrom-Json
```
停掉第一个进程,也就是 `node-a`:
```powershell
Stop-Process -Id $pids[0] -Force
Start-Sleep -Seconds 12
```
为什么要等?
因为 Leader 停止以后,其余节点需要经过选举超时、投票和日志追赶,状态不会在进程退出的那一瞬间就完成切换。
检查端口:
```powershell
Get-NetTCPConnection -State Listen |
Where-Object { $_.LocalPort -in 19090,19091,19092 }
```
此时只应剩:
```text
127.0.0.1:19091
127.0.0.1:19092
```
## 六、第 5 步:从剩余两个节点继续读、继续写
先通过 `node-b` 检查健康状态:
```powershell
$baseB = 'http://127.0.0.1:19091'
Invoke-RestMethod `
-Uri "$baseB/api/v1/health" |
ConvertTo-Json
```
本次返回:
```json
{
"cluster": "RaftKV Trial 2026-09-06",
"leaders": 2,
"status": "ok",
"shards": 2,
"version": "raft-kv-1.0.2"
}
```
这时 `node-a` 已经停止,但两个分片依然都有 Leader。
再读取故障前写入的 key:
```powershell
Invoke-RestMethod `
-Uri "$baseB/api/v1/get?key=trial%3Auser%3A1001" `
-Headers $read |
ConvertTo-Json
```
返回仍然是:
```json
{
"found": true,
"key": "trial:user:1001",
"shard": "shard-1",
"value": "RaftKV-ok"
}
```
接着写入一个新 key:
```powershell
$body2 = @{
key = 'failover:after:node0'
value = 'still-writable'
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri "$baseB/api/v1/put" `
-Headers $admin `
-ContentType 'application/json' `
-Body $body2
```
再从 `node-c` 读取新 key:
```powershell
$baseC = 'http://127.0.0.1:19092'
Invoke-RestMethod `
-Uri "$baseC/api/v1/get?key=failover%3Aafter%3Anode0" `
-Headers $read |
ConvertTo-Json
```
返回:
```json
{
"found": true,
"key": "failover:after:node0",
"shard": "shard-0",
"value": "still-writable"
}
```
到这里,验收结果已经足够明确:
```text
故障前:写入成功
停 node-a:节点进程真实退出
故障后:原 key 可读
故障后:新 key 可写、可从其他节点读回
```
最后再看一次状态:
```powershell
Invoke-RestMethod `
-Uri "$baseB/api/v1/cluster/status" `
-Headers $read |
ConvertTo-Json -Depth 8
```
本次结果中:
- `shard-1` 的新 Leader 变成 `node-c`,term 提升到 4;
- `node-b` 和 `node-c` 的 `commit`、`applied` 都追到 4;
- 两个分片仍然各有 Leader,集群保持可读写。
## 七、做到这里,再补最少量的原理
前面的操作已经完成。现在才需要解释三个问题。
### 1. 为什么少一个节点还能工作
三节点集群里,三个节点中的任意两个可以组成多数派。
停掉一个节点以后,还有两个节点,所以可以继续选举和提交日志。
如果三台里停掉两台,就只剩一个节点,不再有多数派,这时不能继续承诺写入。
### 2. 为什么原数据还能读
已经提交的数据,在返回成功前已经复制到多数派。
`node-a` 停止以后,剩余节点中仍然保存着已提交日志和对应状态,所以可以继续提供原数据。
### 3. 为什么可能出现短暂失败
“多数派还在”不等于“故障瞬间零失败”。
旧 Leader 停止后,客户端可能遇到连接失败、选举窗口内的重试,或者暂时还要等待本地 `applied` 追上 `commit`。
所以面试里更准确的表达是:
> 在三节点失去一个节点的场景下,多数派仍然存在,集群可以完成重新选举并继续推进会提交日志;但客户端仍应处理切换期间的短暂失败和重试。
## 八、面试时可以直接这样答
如果不想背长篇概念,按下面这段说:
> 我做过一次三节点 Raft 分片 KV 的故障验证。先写入一个 key,再从其他节点读回,确认它是已提交数据。然后我真实停掉 node-a,等待选举完成,再从 node-b 和 node-c 检查健康状态。两个分片仍然都有 Leader,原 key 还能读,新 key 也还能写。状态里能继续看到 `commit` 和 `applied` 推进,新 Leader 的 term 也发生了提升。这个测试证明的是教学级三节点故障恢复,不替代 Redis、etcd 或 TiKV 的生产能力。
这段话里没有堆概念,但它包含:
```text
测试环境
-> 操作步骤
-> 现场证据
-> 结果判断
-> 工程边界
```
面试官继续追问时,再展开心跳、选举、日志复制和读取条件。
## 九、最后记住这 5 个检查点
遇到“Leader 挂了怎么办”,先按顺序检查:
1. 三个节点是否都在运行。
2. 故障前是否真的写入并读回过一个 key。
3. 停节点后,端口和进程是否真的发生变化。
4. 剩余节点是否还能形成多数派。
5. `commit`、`applied` 是否继续推进,原数据是否能读、新数据是否能写。
如果这五项没有证据,先不要急着背 Raft。
先把故障跑出来,再把日志和状态保留下来。
系列延伸:
- 《Raft 旧 Leader 恢复后还能读旧数据吗?一个分区测试讲清楚》
https://blog.csdn.net/qq_53554649/article/details/164864176
- 《Raft 节点重启后怎么恢复?别把“进程起来了”当成日志追平》
https://blog.csdn.net/qq_53554649/article/details/165001827
公开测试记录和复现入口:
https://github.com/yuan1521913/raft-kv
配套视频:
https://www.bilibili.com/video/BV1VKYE6oEmm/
教学级 / 作品集级项目,不替代 Redis、etcd 或 TiKV。