三节点 Raft 挂了两台,为什么连读也超时?别把 status=ok 当成集群还活着
2026/9/15 7:20:01 网站建设 项目流程

上一篇文章里,三节点 Raft 停掉一台以后,剩余两个节点还能选主,基线 key 还能读,新 key 也还能写。

很多人接着会被问一句:

> 那三节点挂掉两台呢?

如果只回答“不能写”,方向是对的,但还不够。面试官真正想看的是,你知道系统为什么不能继续确认成功,以及失败时应该给出什么结果。

这篇文章不背定义,直接做一次实验:

```text
启动三节点
-> 写入基线 key
-> 确认 leaders=2
-> 停掉 node-b 和 node-c
-> 只保留 node-a
-> 再检查 health
-> 再尝试读、尝试写
```

本次结果里最关键的反差不是“节点全挂了”,而是:

```text
status=ok
leaders=0
```

进程和 HTTP 接口还在,但集群已经失去完成一致读写所需的多数派。

## 一、先写一条基线数据

在完整三节点状态下,先写入一个已知 key:

```text
quorum:test:baseline = before-two-nodes-stop
```

读回结果:

```json
{
"found": true,
"key": "quorum:test:baseline",
"shard": "shard-0",
"value": "before-two-nodes-stop"
}
```

故障前健康状态:

```json
{
"cluster": "RaftKV Trial 2026-09-06",
"leaders": 2,
"maintenance": false,
"node_id": 0,
"shards": 2,
"status": "ok",
"uptime_s": 8,
"version": "raft-kv-1.0.2"
}
```

两个分片都有 Leader,三个节点都在线。这条基线数据用于后面判断读取路径是否会直接返回旧值。

## 二、停掉两台后,status 还是 ok

保持 `node-a` 运行,停掉 `node-b` 和 `node-c`。

等待 6 秒后,通过 `node-a` 检查健康状态:

```json
{
"cluster": "RaftKV Trial 2026-09-06",
"leaders": 0,
"maintenance": false,
"node_id": 0,
"shards": 2,
"status": "ok",
"uptime_s": 19,
"version": "raft-kv-1.0.2"
}
```

这里同时出现了两个容易混淆的信号:

```text
status=ok
leaders=0
```

`status=ok` 说明当前节点的进程和 HTTP 服务还活着,它仍能接收请求。

`leaders=0` 说明在这个节点看来,没有分片拥有能够确认多数派的 Leader。

这两个值不矛盾。

节点活着,和集群拥有多数派,是两件事。

## 三、为什么不能把 status=ok 当成集群可用

很多新手监控只看 `/health` 是否返回 200。

如果健康检查只判断“当前进程有没有起来”,那么三节点只剩一个节点时,它仍然会返回成功。这个信号适合判断进程是否需要重启,但不适合回答:

> 集群现在能不能完成一致读写?

至少需要把下面几层分开:

| 检查 | 回答的问题 |
| --- | --- |
| 进程存活 | 当前进程还在不在 |
| HTTP 健康 | 当前节点接口能不能响应 |
| Leader 数量 | 分片有没有可确认的 Leader |
| commit / applied | 日志有没有提交并应用到状态机 |
| 客户端请求结果 | 这次读写有没有真正完成 |

如果把这些信号压成一个 `status=ok`,监控会在最危险的时候给出最安心的结果。

Raft 提交日志需要多数派。

三节点的多数派是 2:

```text
3 节点 - 1 个故障 = 2 个在线
2 / 3 仍然是多数派
```

如果停掉两个:

```text
3 节点 - 2 个故障 = 1 个在线
1 / 3 不是多数派
```

剩余节点继续返回 `status=ok`,不代表它能单独完成日志提交。

## 四、失去多数派后,写请求会怎样

继续通过 `node-a` 写入一个新的 key:

```text
quorum:test:after-two-nodes-stop = should-not-commit
```

本次请求没有返回成功,而是在客户端等待 12 秒后超时:

```text
The request was canceled due to the configured HttpClient.Timeout of 12 seconds elapsing.
```

记录耗时:

```text
12056 ms
```

为什么必须超时,而不是简单返回成功?

Raft 写入只有在多数派确认以后,才能认为日志已经提交。当前只剩一个节点,它没有能力确认多数派,因此不能把这条写入标记为成功。

这里真正危险的做法是:

```text
本地先写入
-> 接口立刻返回成功
-> 等网络恢复后再想办法同步
```

如果客户端已经收到成功,但这次写入后来被丢弃,那它得到的是一个无法兑现的成功确认。

在工程上,宁可让调用方看到超时、失败或明确的不可用错误,也不能伪造一次尚未提交的成功。

## 五、为什么读也会超时

读取同一个基线 key:

```text
quorum:test:baseline
```

当前实现的读取确认路径同样等待了 6 秒,最后超时:

```text
The request was canceled due to the configured HttpClient.Timeout of 6 seconds elapsing.
```

记录耗时:

```text
6102 ms
```

读请求为什么不能直接从 `node-a` 返回本地那条旧数据?

因为“本地还有这条数据”和“这条数据现在仍然有资格对外返回”是两件事。

如果系统承诺的是当前 Leader 确认后的读,那么在失去多数派时,它不能确认自己仍然代表当前集群状态。

这时有两个选择:

1. 拒绝读,返回错误或等待超时;
2. 明确提供 stale read,并让调用方知道自己接受旧数据。

最容易出错的是第三种:

> 接口看起来像正常读,实际却悄悄返回旧值。

对缓存或明确标注的弱一致场景,旧数据可能可以接受。但只要接口没有把语义说清楚,客户端就会误以为这是当前值。

## 六、为什么三节点最多只能容忍一台故障

多数派公式可以写成:

```text
多数派 = floor(N / 2) + 1
```

当 `N=3`:

```text
多数派 = 2
```

所以:

| 在线节点 | 是否有多数派 | 能不能提交新日志 |
| --- | --- | --- |
| 3 | 是 | 可以 |
| 2 | 是 | 可以 |
| 1 | 否 | 不可以 |
| 0 | 否 | 不可以 |

这也解释了为什么上一篇文章里,停掉一台仍能读写。

不是因为某个节点“天生更可靠”,而是因为剩下两个节点仍然能组成多数派。

如果业务需要容忍两台节点同时故障,三节点不够。

通常需要考虑五节点:

```text
5 节点的多数派 = 3
可以容忍 2 台故障
```

但节点数增加不等于永远更安全。节点越多,请求需要等待的副本更多,网络和运维复杂度也会增加。选几节点,要结合故障模型、延迟目标和成本一起判断。

## 七、服务层应该怎样处理

对于这类失去多数派的场景,服务层至少应该做好四件事。

### 1. 区分存活和可用

把进程健康、Leader 状态和写入可用性拆成不同指标,不要只用一个 `/health` 判断全部。

### 2. 给请求设置明确超时

失去多数派后,请求不能无限等待。调用方需要知道这次操作最终是成功、失败,还是超时。

### 3. 返回可识别的不可用结果

如果能够确认没有多数派,应尽快返回明确的错误,例如 `503` 或对应的集群不可用状态,而不是一直挂住连接。

### 4. 不要把超时自动改写成成功

重试可以交给上层,但成功确认必须来自真实的提交结果。

## 八、面试时可以直接这样回答

如果面试官问“三节点 Raft 挂两台会怎样”,可以回答:

> 三节点的多数派是 2。挂掉一台时还剩 2 个节点,可以继续选举和提交日志。挂掉两台后只剩 1 个节点,它虽然可能仍能启动进程并返回 status=ok,但无法形成多数派,所以不能完成新的日志提交。我的实现里,剩余节点的 leaders 会变成 0,写请求最终超时,读取确认路径也不会直接返回旧值。这里的工程边界是:进程存活不等于集群可用,也不能把未提交写入伪装成成功。

这段话比单独回答“不能写”多出了三点:

```text
为什么不能写
系统实际观察到什么
错误结果应该怎样暴露
```

## 九、作品集应该保留哪些证据

如果要把这次测试写进简历或项目说明,至少保留:

1. 故障前的 `leaders`、基线 key 和读回结果;
2. 停掉两台节点后的 `status` 与 `leaders`;
3. 写请求的失败或超时结果和耗时;
4. 读请求的失败或超时结果和耗时;
5. 明确说明本次测试没有覆盖网络分区、磁盘故障和生产级容灾。

不要只截一张“还剩一个节点在线”的图。

单节点还能启动,不代表集群还能提交写入。

## 十、结论

三节点 Raft 的关键边界不是“挂了几台”,而是“剩下多少节点还能组成多数派”。

挂一台:

```text
2 / 3 在线
leaders=2
可以继续读写
```

挂两台:

```text
1 / 3 在线
leaders=0
无法完成新的多数派提交
```

面试里最值得强调的一句话是:

> `status=ok` 只能说明当前节点还活着,不能证明整个集群仍然可用。

系列文章:

- 《Raft 旧 Leader 恢复后还能读旧数据吗?一个分区测试讲清楚》
https://blog.csdn.net/qq_53554649/article/details/164864176
- 《Raft 节点重启后怎么恢复?别把“进程起来了”当成日志追平》
https://blog.csdn.net/qq_53554649/article/details/165001827
- 《三节点 Raft 挂了一台,先别背选举:照着这 5 步查完再回答》
https://blog.csdn.net/qq_53554649/article/details/165121566

公开测试记录和复现入口:

https://github.com/yuan1521913/raft-kv

教学级 / 作品集级项目,不替代 Redis、etcd 或 TiKV。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询