215 lines
9.6 KiB
Markdown
215 lines
9.6 KiB
Markdown
---
|
||
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 模式下同样存在脑裂问题
|