Files
cs-note/hhs/Redis/07-集群方案/脑裂问题.md
T
2026-05-27 11:24:47 +08:00

215 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [分布式, 脑裂, 网络分区, CAP, 故障转移]
create time: 2026-05-27 10:00
---
# 脑裂问题
## 概述
脑裂(Split Brain)是分布式系统中最危险的故障之一:**集群因为网络分区被割裂成多个独立的子集群,每个子集群都以为自己是"正统",各自选举出 Leader,同时对外提供服务**。如果不能妥善处理,就会出现两个 Master 同时接受写入,导致数据丢失或不一致。
## 一、什么是脑裂?
用一个简单的场景来说明:
```mermaid
flowchart TD
subgraph 原始集群
M["Master<br/>slot 0~5460"]
R1["Replica 1"]
R2["Replica 2"]
end
M -->|"正常复制"| R1
M -->|"正常复制"| R2
style M fill:#e8eaf6,stroke:#3f51b5
```
网络分区发生后:
```mermaid
flowchart TD
subgraph 分区A["网络分区 A (少数派)"]
M2["原 Master<br/>仍认为自己是 Master"]
end
subgraph 分区B["网络分区 B (多数派)"]
R1B["Replica 1<br/>被选举为新 Master"]
R2B["Replica 2"]
end
C1["部分客户端"] --> M2
C2["部分客户端"] --> R1B
style M2 fill:#fff3e0,stroke:#ff9800
style R1B fill:#e8f5e9,stroke:#4caf50
```
> [!DANGER] 脑裂的后果
> 1. **两个 Master 同时接受写入**——客户端 A 写入原 Master,客户端 B 写入新 Master
> 2. **网络恢复后,原 Master 降级为 Replica**——它在分区期间接受的写入数据**全部丢失**
> 3. 因为降级后要从新 Master 同步数据,覆盖掉分区期间的增量写入
> [!QUESTION] 思考
> 在上面的场景中,原 Master 在分区 A 中仍然是"合法"的 Master(它不知道自己已被替换)。那么问题来了——**谁来告诉它"你已经不是 Master 了"?** 如果没有人告诉它,它就会一直接受写入,这就是脑裂的本质。
## 二、Redis Cluster 的脑裂防护
Redis Cluster 提供了两层防护机制,我们先看**没有任何防护**时会发生什么,再逐层叠加:
### 场景对比:有无防护的差异
| 防护等级 | 分区期间少数派 Master | 分区恢复后 | 数据丢失? |
|---------|---------------------|-----------|-----------|
| **无防护** | 继续接受写入 | 降级为 Replica,增量数据被全量同步覆盖 | ✅ 丢失 |
| **仅 epoch 冲突解决** | 继续接受写入 | 同上(epoch 只解决"谁是 Master",不防丢数据) | ✅ 丢失 |
| **epoch + min-replicas-to-write** | 主动拒绝写入 | 无增量数据需要丢弃 | ❌ 不丢失 |
### 防护层 1:Epoch 版本号冲突解决
Redis 使用 **epoch(集群纪元)** 来解决配置冲突。当网络分区愈合后:
```mermaid
flowchart LR
subgraph Before["分区愈合前"]
M_Old["Master<br/>epoch=5"]
M_New["新 Master<br/>epoch=6"]
end
After["epoch=6 胜出<br/>原 Master 降级为 Replica"]
Before --> After
style M_Old fill:#fff3e0,stroke:#ff9800
style M_New fill:#e8f5e9,stroke:#4caf50
```
- epoch 更大的配置被视为"更新的真相"
- epoch 较小的 Master 被强制降级为 Replica
- **这解决了"谁是 Master"的分歧,但不解决"数据丢失"的问题**
### 防护层 2:写入拒绝(min-replicas-to-write)
这是防止数据丢失的关键配置。在 `redis.conf` 中设置:
```conf
# 至少需要 1 个在线的 Replica 才允许写入
min-replicas-to-write 1
# Replica 的最大延迟(秒),超过则不算"在线"
min-replicas-max-lag 10
```
**工作原理:**
```mermaid
flowchart TD
W["客户端写入请求"] --> C{"在线 Replica 数<br/>>= min-replicas-to-write?"}
C -->|是| A["接受写入"]
C -->|否| R["拒绝写入<br/>返回错误"]
style A fill:#e8f5e9,stroke:#4caf50
style R fill:#fce4ec,stroke:#e91e63
```
当脑裂发生时,被隔离的少数派 Master 发现自己无法联系到足够的 Replica,于是**主动拒绝写入**——虽然损失了可用性,但保证了数据不会在分区恢复后丢失。
> [!TIP] 贝叶斯视角
> `min-replicas-to-write 1` 的含义是:"如果连一个 Replica 都联系不上,那我很可能已经不是集群的多数派了,还是别写了吧。"这是一种基于概率的保守策略。
> [!WARNING] 这个配置的代价
> `min-replicas-to-write` 不仅仅在脑裂时生效——**任何原因导致 Replica 不够数时,Master 都会拒绝写入**,包括:
> - Replica 短暂网络抖动或重启
> - Replica 复制延迟超过 `min-replicas-max-lag`
> - 只部署了 1 个 Replica 且恰好宕机
>
> 这意味着配置了这个参数后,可用性会降低。生产环境中需要在**一致性**和**可用性**之间做出明确的取舍。
## 三、分区恢复后的处理流程
当网络分区愈合后,Redis Cluster 会自动完成"善后"工作。整个过程无需人工干预:
```mermaid
sequenceDiagram
participant Old as "原 Master (分区 A)"
participant New as "新 Master (分区 B)"
participant R2 as "Replica 2"
Note over Old,New: 网络分区愈合,节点重新连通
Old->>New: Gossip 交换状态
New->>Old: PONG (epoch=6)
Note over Old: 比较 epoch: 5 < 6
Old->>Old: 降级为 Replica
Old->>New: 全量/增量同步数据
Note over Old: 分区期间的写入被覆盖
```
**具体步骤:**
1. **Gossip 状态交换**:分区愈合后,节点之间通过 PING/PONG 交换集群状态
2. **Epoch 比较**:原 Master 发现新 Master 的 epoch 更大(6 > 5)
3. **自动降级**:原 Master 将自己降级为 Replica,停止接受写入
4. **数据同步**:从新 Master 进行全量(`FULLRESYNC`)或增量(`PSYNC`)同步
5. **数据覆盖**:分区期间原 Master 接受的写入被新 Master 的数据覆盖,**永久丢失**
> [!QUESTION] 思考
> 如果在分区期间,原 Master 上写入了 key A=1,新 Master 上也写入了 key A=2。分区恢复后,key A 的值是什么?为什么 Redis 选择这种"简单粗暴"的覆盖策略,而不是尝试合并两边的写入?
## 四、CAP 角度理解脑裂
脑裂问题本质上是 CAP 定理在网络分区(P)发生时的体现——你必须在一致性(C)和可用性(A)之间做出选择:
| 选择 | 牺牲 | 脑裂时的行为 | Redis 配置 |
|------|------|-------------|-----------|
| **CP(一致性优先)** | 可用性 | 少数派 Master 拒绝写入 | `min-replicas-to-write 1` |
| **AP(可用性优先)** | 一致性 | 两边都接受写入,事后丢弃少数派数据 | `min-replicas-to-write 0`(默认) |
> [!IMPORTANT] 两个易混淆的配置
> - **`min-replicas-to-write`**:控制**脑裂场景**下的 CP/AP 行为。设为 1 时,少数派 Master 主动拒绝写入(CP);默认为 0,不拒绝(AP)。
> - **`cluster-require-full-coverage`**:控制**slot 无主场景**下的行为。设为 `yes`(默认)时,任何一个 slot 没有 Master 则整个集群拒绝服务;设为 `no` 时,只有访问无主 slot 的请求失败,其余正常。
>
> 两者解决的是**不同的故障场景**,不要混淆。
> [!NOTE] Redis 不是强一致系统
> 即使配置了 `min-replicas-to-write 1`,Redis Cluster 的复制仍然是**异步**的。Master 写入成功后不会等待 Replica 确认,所以在 Master 宕机的瞬间,最后几条写入可能还没同步到 Replica——这是 Redis 为了性能做出的 trade-off。
>
> 如果业务需要强一致(如金融交易),应该使用支持同步复制的系统(如 etcd、ZooKeeper),而非 Redis。
## 五、Sentinel 模式下的脑裂
上面讨论的都是 Redis Cluster 模式。但 **Sentinel(哨兵)模式同样存在脑裂问题**,防护思路类似但机制不同:
| 对比项 | Redis Cluster | Sentinel |
|--------|--------------|----------|
| 故障检测 | Gossip + PFAIL/FAIL 多数确认 | Sentinel 节点投票(quorum) |
| 选举机制 | Replica 向 Master 投票 | Sentinel 向其他 Sentinel 投票 |
| 脑裂防护 | `min-replicas-to-write` | `min-replicas-to-write`(同样生效) |
| 冲突解决 | Epoch(集群纪元) | Epoch(配置纪元,由 Sentinel 递增) |
> [!NOTE] Sentinel 的额外防护
> Sentinel 模式中,需要**多数 Sentinel 节点同意**才会执行故障转移(`quorum` 参数)。如果 Sentinel 本身也被分区隔离,少数派的 Sentinel 无法凑够 quorum,也就无法选出新 Master——这本身就是一种脑裂防护。
>
> 但原 Master 仍然可能在分区期间接受写入,所以 `min-replicas-to-write` 在 Sentinel 模式下**同样推荐配置**。
## 六、生产环境建议
| 配置 | 推荐值 | 说明 |
|------|--------|------|
| `min-replicas-to-write` | 1 | 至少 1 个 Replica 在线才写入(**脑裂防护的核心**) |
| `min-replicas-max-lag` | 10 | Replica 延迟超过 10 秒视为不在线 |
| `cluster-require-full-coverage` | 按业务选择 | `yes`:slot 无主时集群整体拒绝服务;`no`:仅无主 slot 不可用 |
| `cluster-node-timeout` | 15000 | 故障检测超时,太短易误判,太长切换慢 |
| Master 数量 | 奇数 | 3/5/7,避免偶数导致投票平局 |
> [!IMPORTANT] 脑裂不是 Redis 特有的问题
> 任何分布式系统在网络分区时都可能面临脑裂:ZooKeeper(ZAB 协议)、Kafka(ISR + epoch)、etcd(Raft 共识)等都有各自的防护机制。理解脑裂的本质有助于在任何分布式场景中做出正确的架构决策。
## 关联笔记
- [[hhs/Redis/07-集群方案]] — Redis Cluster 集群方案
- [[hhs/Redis/07-集群方案/Raft共识算法]] — 故障转移的投票选举机制
- [[hhs/Redis/07-集群方案/Gossip协议]] — 节点状态传播与分区检测
- [[hhs/Redis/06-主从与哨兵]] — Sentinel 模式下同样存在脑裂问题