vault backup: 2026-05-27 11:24:47
This commit is contained in:
@@ -13,27 +13,47 @@ MAC(Media Access Control)地址是数据链路层的物理地址,为全球
|
||||
|
||||
### OUI + NIC Serial
|
||||
|
||||
```
|
||||
┌───────────────┬───────────────┐
|
||||
│ OUI (3B) │ NIC Serial │
|
||||
│ IEEE 分配 │ 厂商自定 │
|
||||
│ │ │
|
||||
│ AA:BB:CC │ DD:EE:FF │
|
||||
└───────────────┴───────────────┘
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph OUI["OUI — 前 3 字节 (IEEE 分配)"]
|
||||
B1["AA"] --- B2["BB"] --- B3["CC"]
|
||||
end
|
||||
subgraph NIC["NIC Serial — 后 3 字节 (厂商自定)"]
|
||||
B4["DD"] --- B5["EE"] --- B6["FF"]
|
||||
end
|
||||
OUI --- NIC
|
||||
```
|
||||
|
||||
- **OUI** (Organizationally Unique Identifier): 前 3 字节由 IEEE 分配给厂商
|
||||
- **NIC Serial**: 后 3 字节由厂商自行分配给具体网卡
|
||||
|
||||
> [!tip] 48 位,全球唯一
|
||||
> MAC 地址共 6 字节 = 48 位,理论上可表示 2^48(约 281 万亿)个地址。日常中你看到的 `aa:bb:cc:dd:ee:ff` 或 `aa-bb-cc-dd-ee-ff` 只是两种书写格式,完全等价。
|
||||
|
||||
### 寻址类型位
|
||||
|
||||
第一个字节的最低比特位(LSB)决定类型:
|
||||
第一个字节的最低两位比特承载了关键的语义信息(它们属于 OUI 的一部分,但具有特殊含义):
|
||||
|
||||
| 位模式 | 类型 | 示例 |
|
||||
| 比特位 | 名称 | 作用 |
|
||||
|--------|------|------|
|
||||
| `xx:xx:xx:xx:xx:xx` — LSB=0 | 单播 (Unicast) | `a0:88:b4:12:34:56` |
|
||||
| `01:00:5e:xx:xx:xx` — LSB=1 | IPv4 组播 | 特定多播组 |
|
||||
| `ff:ff:ff:ff:ff:ff` | 广播 (Broadcast) | 全网段广播 |
|
||||
| Bit 0(最低位) | **I/G** (Individual/Group) | 0 = 单播,1 = 组播/广播 |
|
||||
| Bit 1 | **U/L** (Universal/Local) | 0 = 厂商全局唯一,1 = 本地管理 |
|
||||
|
||||
> [!question] 思考一下
|
||||
> 为什么 IEEE 要在 OUI 里预留两个"控制位",而不是把 48 位全部用来编号?
|
||||
|
||||
完整的类型判定:
|
||||
|
||||
| I/G | U/L | 类型 | 说明 | 示例 |
|
||||
|-----|-----|------|------|------|
|
||||
| 0 | 0 | **厂商单播** | 最常见,网卡出厂烧录 | `a0:88:b4:12:34:56` |
|
||||
| 0 | 1 | **本地单播** | 管理员/软件自定义(见下文) | `02:42:ac:11:00:02` |
|
||||
| 1 | 0 | **标准组播** | IPv4 组播 MAC | `01:00:5e:00:00:fb`(mDNS) |
|
||||
| 1 | 1 | **本地组播** | 较少见 | — |
|
||||
| 1 | 1 | **广播** | 特例:全 1 地址 | `ff:ff:ff:ff:ff:ff` |
|
||||
|
||||
> [!tip] 简化记忆
|
||||
> 看第一个字节的十六进制最低位:**偶数** → 单播,**奇数** → 组播/广播。例如 `a0`(偶)= 单播,`01`(奇)= 组播。
|
||||
|
||||
## 广播域(Broadcast Domain)
|
||||
|
||||
@@ -65,6 +85,18 @@ flowchart LR
|
||||
|
||||
**结论:** Switch 内部不隔离广播域,只有 Router(或三层交换机)的 VLAN 接口才能隔离广播。这就是为什么大型网络需要划分 VLAN。
|
||||
|
||||
> [!question] 冲突域 vs 广播域——你能分清吗?
|
||||
> 这两个概念经常被混淆,它们是两个**不同维度**:
|
||||
>
|
||||
> | | 冲突域 (Collision Domain) | 广播域 (Broadcast Domain) |
|
||||
> |---|---|---|
|
||||
> | **定义** | 同一时刻只能有一台设备发送数据的网络范围 | 广播帧能到达的所有设备集合 |
|
||||
> | **被 Hub 扩大** | ✅ 是 | ✅ 是 |
|
||||
> | **被 Switch 隔离** | ✅ 每个端口一个冲突域 | ❌ 同 VLAN 内不隔离 |
|
||||
> | **被 Router 隔离** | ✅ 是 | ✅ 是 |
|
||||
>
|
||||
> 简记:**Switch 隔离冲突域,Router 隔离广播域**。面试高频考点。
|
||||
|
||||
## 二层转发 vs 三层转发
|
||||
|
||||
### ARP 解析流程(跨网段的情况)
|
||||
@@ -126,6 +158,43 @@ $ ip neigh # 等价于 arp,更现代
|
||||
|
||||
MAC 表条目有超时时间(通常 300 秒),超时后自动删除,等待下一帧到来重新学习。这防止了表中堆积无效条目。
|
||||
|
||||
## 本地管理地址(LAA)与 MAC 欺骗
|
||||
|
||||
### 什么是 LAA?
|
||||
|
||||
当 U/L 位 = 1 时,该地址不再是 IEEE 全局分配的,而是由**管理员或软件自行设定**,称为 Locally Administered Address (LAA)。
|
||||
|
||||
### 谁在用 LAA?
|
||||
|
||||
| 场景 | 说明 |
|
||||
|------|------|
|
||||
| Docker 容器 | 容器的 veth 接口 MAC 通常以 `02:42:` 开头 |
|
||||
| 虚拟机 (KVM/QEMU) | 虚拟网卡 MAC 以 `52:54:` 开头 |
|
||||
| 隐私保护 | iOS/Android 的 Wi-Fi 随机 MAC 地址 |
|
||||
| MAC 欺骗攻击 | 攻击者伪造目标 MAC 以实施中间人攻击 |
|
||||
|
||||
### MAC 欺骗原理
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Attacker as 攻击者
|
||||
participant Switch as Switch
|
||||
participant Victim as 受害者
|
||||
participant GW as 网关
|
||||
|
||||
Note over Switch: MAC表: GW_mac → Port3
|
||||
Attacker->>Switch: 发送伪造帧 (Src MAC = GW_mac)
|
||||
Note over Switch: 更新MAC表: GW_mac → Port1(攻击者)!
|
||||
GW->>Switch: 发往受害者的数据
|
||||
Switch->>Attacker: 转发到 Port1(攻击者端口)
|
||||
Attacker->>Victim: 转发给受害者(无感知)
|
||||
```
|
||||
|
||||
> [!warning] 防御 MAC 欺骗
|
||||
> - **Port Security**:限制每个端口可学习的 MAC 数量
|
||||
> - **Dynamic ARP Inspection (DAI)**:校验 ARP 报文合法性
|
||||
> - **802.1X 认证**:端口级准入控制,未认证设备无法通信
|
||||
|
||||
## 广播风暴与防范
|
||||
|
||||
### 什么是广播风暴?
|
||||
|
||||
+83
-19
@@ -24,19 +24,30 @@ flowchart TD
|
||||
|
||||
### 为什么不用一致性哈希?
|
||||
|
||||
> [!NOTE] 一致性哈希是什么?
|
||||
> 一致性哈希是一种分布式分片算法,它将所有节点和数据映射到一个虚拟的**哈希环**上。数据沿顺时针找到的第一个节点就是它的归属。核心优势是:增减节点时,只需要迁移相邻区间的少量数据。更多细节见 [[hhs/Redis/07-集群方案/一致性哈希|一致性哈希]]。
|
||||
|
||||
| 对比项 | 一致性哈希 | 哈希槽 |
|
||||
|--------|-----------|---------|
|
||||
| 增节点迁移量 | O(N/K × log N) | **O(1/N × total_slots)** = 每次 ~1300 slots |
|
||||
| 客户端透明性 | ✅ 通常透明 | ⚠️ 需要 MOVED/ASK 重定向 |
|
||||
| 数据倾斜 | 依赖虚拟节点 | ❌ slots 分配不均会倾斜 |
|
||||
| 复杂度 | 简单 | 略复杂(Gossip + 槽位管理) |
|
||||
| 增节点迁移量 | **~N/K**(N 为总 key 数,K 为节点数) | **total_slots × 1/N** ≈ 每个新节点分得 ~2730 slots |
|
||||
| 客户端透明性 | ✅ 通常透明,按哈希环顺时针归属 | ⚠️ 需要 MOVED/ASK 重定向 |
|
||||
| 数据倾斜 | 依赖虚拟节点数量平衡 | ❌ slots 分配不均时会倾斜 |
|
||||
| 复杂度 | 简单,纯哈希环 | 略复杂(Gossip + 槽位元数据同步) |
|
||||
|
||||
> [!QUESTION] 思考
|
||||
> Redis 选择了 16384 个槽而不是其他数字,你觉得为什么是这个数?太少了会怎样?太多了又会怎样?
|
||||
|
||||
> [!NOTE] 为什么是 16384(2¹⁴)?
|
||||
> Redis 作者 antirez 在 [GitHub Issue #2576](https://github.com/redis/redis/issues/2576) 中明确解释过:
|
||||
> 1. **Gossip 消息大小限制**:每个节点的槽位信息用 bitmap 表示,16384 slots = 2KB(16384/8 字节),放入心跳包后带宽可控;若用 65536 slots 则需 8KB,心跳报文膨胀明显。
|
||||
> 2. **节点数量上限**:Redis Cluster 官方建议最多 1000 个节点。16384 个槽在 1000 节点时,平均每个节点 16 个槽,粒度已经足够细。
|
||||
> 3. **计算效率**:16384 是 2 的幂,CRC16 取模可直接用位运算 `& 0x3FFF`,无需真正执行除法。
|
||||
>
|
||||
> 因此,16384 是**槽位粒度**与**心跳带宽**之间的工程折中,而非哈希冲突的考量。
|
||||
|
||||
### 槽位计算
|
||||
|
||||
Redis 使用 **CRC16 算法**对 key 取模,结果映射到 0~16383 共 16384 个槽:
|
||||
Redis 使用 **CRC16 算法**(Cyclic Redundancy Check,循环冗余校验——一种广泛用于数据校验的快速哈希算法)对 key 取模,结果映射到 0~16383 共 16384 个槽:
|
||||
|
||||
```bash
|
||||
# CRC16(key) % 16384 = slot 编号
|
||||
@@ -63,6 +74,8 @@ rdb.Set(ctx, "foo", "bar", 0) // 自动路由到对应节点
|
||||
|
||||
## 通信协议与 Gossip 协议
|
||||
|
||||
> Gossip 协议是一种去中心化的信息传播协议,灵感来自"八卦传播"——每个节点定期向随机选择的邻居交换状态信息,几轮之后信息便传遍集群。Redis Cluster 用它同步节点状态和槽位分配,无需中心化协调者。详见 [[hhs/Redis/07-集群方案/Gossip协议|Gossip 协议]]。
|
||||
|
||||
### 节点间通信端口
|
||||
|
||||
```
|
||||
@@ -75,7 +88,7 @@ rdb.Set(ctx, "foo", "bar", 0) // 自动路由到对应节点
|
||||
|
||||
### Gossip 协议运作方式
|
||||
|
||||
每个节点每秒向其他节点发送 PING/PONG,维持集群状态的一致性。选择的节点数量动态调整——集群越小越接近全发,集群越大越倾向于随机子集:
|
||||
每个节点每秒向随机选择的节点发送 PING/PONG,维持集群状态的一致性。选择数量为固定公式:1 个长期未通信节点 + min(5, node_count/10) 个随机节点:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -96,7 +109,7 @@ sequenceDiagram
|
||||
3. **FAIL** — 某节点不可达,广播给其他节点
|
||||
|
||||
> [!QUESTION] 思考
|
||||
> Gossip 节点选择策略会随集群规模动态调整——小集群(<100 节点)向所有节点发 PING,大集群才随机选择子集。为什么不直接广播给所有节点?这样做的带宽开销是怎样的?
|
||||
> Redis 的 Gossip 选择策略(源码 `cluster.c:clusterCron()`):每次选 1 个长时间未通信的随机节点 + 5 个随机节点(上限为集群节点总数的 1/10),而非广播给所有节点。这是固定公式,不因集群规模动态切换。为什么不广播所有节点?1000 节点的集群中,每秒广播意味着 O(N²) 条消息——这正是 Gossip 用随机子集换取带宽的关键设计。
|
||||
|
||||
### PING/PONG 报文详解
|
||||
|
||||
@@ -106,7 +119,7 @@ PING 报文体包含:
|
||||
├── 槽位分配表(哪些节点负责哪些 slot)
|
||||
├── 节点状态(master/slave/fail)
|
||||
├── 最后通信时间戳
|
||||
└── 当前集群 epoch(用于版本控制)
|
||||
└── 当前集群 epoch(配置版本号,每次集群拓扑变更如 failover 或 reshard 时递增,用于解决节点间的状态冲突——epoch 更大的配置被视为最新真相)
|
||||
```
|
||||
|
||||
## 数据迁移流程
|
||||
@@ -120,7 +133,9 @@ CLUSTER SETSLOT <slot_id> MIGRATING <target_node_id>
|
||||
# 在目标节点执行(确认阶段)
|
||||
CLUSTER SETSLOT <slot_id> IMPORTING <source_node_id>
|
||||
|
||||
# 逐 key 迁移(GETKEYSINSLOT 返回指定 slot 中的 key,每次最多 100 个)
|
||||
# 逐 key 迁移
|
||||
# GETKEYSINSLOT: 返回指定 slot 中的 key 列表,每次最多 100 个
|
||||
# MIGRATE: 原子地将 key 从源节点转移到目标节点(同步阻塞,最后一个参数 5000 是超时 ms)
|
||||
for key in $(redis-cli -h $SRC -p $SRC_PORT CLUSTER GETKEYSINSLOT $slot 100); do
|
||||
redis-cli -h $SRC -p $SRC_PORT MIGRATE $DST $DST_PORT $key 0 5000
|
||||
done
|
||||
@@ -129,6 +144,13 @@ done
|
||||
CLUSTER SETSLOT <slot_id> NODE <target_node_id>
|
||||
```
|
||||
|
||||
> [!NOTE] 迁移期间的 ASK 临时定向
|
||||
> 当 slot 处于 MIGRATING/IMPORTING 中间状态时:
|
||||
> - 源节点**拥有该 key** → 正常处理
|
||||
> - 源节点**找不到该 key**(已迁移走) → 返回 `ASK <target-node>`,客户端只需本次请求转发到目标节点,后续请求仍发原节点。
|
||||
>
|
||||
> 这保证了迁移过程对客户端透明,且不会造成路由表的提前混乱。
|
||||
|
||||
### 自动化迁移 —— redis-cli cluster reshard
|
||||
|
||||
```bash
|
||||
@@ -263,6 +285,9 @@ flowchart LR
|
||||
|
||||
### 故障检测三阶段
|
||||
|
||||
> [!NOTE] Raft 式投票是什么?
|
||||
> Raft 是一种分布式共识算法,核心思想之一是**领导者选举**:当 Leader 宕机时,Follower 发起选举,获得多数派投票的节点成为新 Leader。Redis Cluster 的 Failover 选举借鉴了这个机制——Replica 通过递增 epoch 并向所有 Master 请求投票,获得多数票后晋升为新 Master。但 Redis 只使用了 Raft 的选举部分,数据复制仍然走自身协议。详见 [[hhs/Redis/07-集群方案/Raft共识算法|Raft 共识算法]]。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["PFAIL 潜在失效<br/>单个节点判定"] -->|"超过 cluster-node-timeout"| B["FAIL 确认失效<br/>多数 Master 同意"]
|
||||
@@ -288,15 +313,19 @@ flowchart LR
|
||||
|
||||
1. **只有 Replica 能参与选举**,Master 不会主动抢占
|
||||
2. **选举时机**:Replica 发现自己复制的 Master 进入 FAIL 状态
|
||||
3. **Raft 式投票**:每个 Master 一票,先到先得
|
||||
4. **获胜条件**:获得多数 Master 的选票
|
||||
5. **升级步骤**:
|
||||
3. **竞争排序依据**(依次比较):
|
||||
- `replica-priority` 值越小优先级越高(配置为 0 表示永远不参选)
|
||||
- 复制偏移量(`replication offset`)越大越优先——偏移量大意味着数据最新
|
||||
- 若偏移量相同,`node ID` 字典序小的优先(确定性 tiebreaker,避免同时发起投票)
|
||||
4. **Raft 式投票**:每个 Master 一票,先到先得
|
||||
5. **获胜条件**:获得多数 Master 的选票
|
||||
6. **升级步骤**:
|
||||
- 将自己提升为 Master
|
||||
- 接管所有原 Master 管理的 slots
|
||||
- 向所有节点广播新的配置
|
||||
- 向所有节点广播新的配置(configEpoch 递增)
|
||||
|
||||
> [!NOTE] 脑裂保护
|
||||
> 如果网络分区导致两个节点的 Master 都认为对方宕机,各自选举出新的 Master,Redis 会通过 **epoch 版本号**解决冲突——后升级的那个会被回退,这是为了避免「脑裂」写丢失。
|
||||
> 脑裂是指网络分区把集群割裂成多个子集群,各自选举出 Master 同时对外服务。Redis 通过 **epoch 版本号** 解决冲突——epoch 更小的 Master 被强制降级。此外,建议配置 `min-replicas-to-write 1`,当 Master 联系不到足够 Replica 时主动拒绝写入,防止分区恢复后数据丢失。详见 [[hhs/Redis/07-集群方案/脑裂问题|脑裂问题]]。
|
||||
|
||||
### 客户端容错处理
|
||||
|
||||
@@ -348,11 +377,12 @@ rdb := redis.NewClusterClient(&redis.ClusterOptions{
|
||||
val, err := rdb.Get(ctx, "user:100:name").Result()
|
||||
```
|
||||
|
||||
```go
|
||||
```java
|
||||
// lettuce 示例:软客户端,按需刷新拓扑(Java/Spring)
|
||||
let cluster = RedisCluster.connect(url)
|
||||
// 遇到 MOVED 时自动重新发现拓扑
|
||||
let val = await cluster.get("key")
|
||||
RedisClusterClient client = RedisClusterClient.create("redis://node1:7001");
|
||||
StatefulRedisClusterConnection<String, String> conn = client.connect();
|
||||
// 遇到 MOVED 时,ClusterTopologyRefreshScheduler 自动重新发现拓扑
|
||||
String val = conn.sync().get("key");
|
||||
```
|
||||
|
||||
> [!TIP] 推荐配置
|
||||
@@ -374,7 +404,7 @@ let val = await cluster.get("key")
|
||||
| 参数 | 推荐值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `cluster-node-timeout` | 15000 ms | 不要太短,避免网络抖动误判;不要长于业务容忍度 |
|
||||
| `maxmemory-policy` | `allkeys-lru` | 集群场景统一设置 LRU 淘汰,避免部分节点 OOM |
|
||||
| `maxmemory-policy` | 缓存场景 `allkeys-lru`;持久化场景 `noeviction` | 缓存类应用优先 LRU 避免 OOM;需要严格保证写入成功的场景用 `noeviction`,由业务层处理满错误 |
|
||||
| `appendonly` | `yes` | 集群推荐 AOF + 每秒刷盘,兼顾性能与安全 |
|
||||
| `cluster-replica-validity-factor` | 10 | 乘以 timeout 作为复制链路容忍阈值,0 表示不拒绝主从链接 |
|
||||
| `cluster-require-full-coverage` | `yes`(默认) | `yes` 时任意 slot 无主节点则集群整体拒绝写入;生产环境若追求可用性可设 `no`,允许部分 slot 不可用时其余 slot 正常服务 |
|
||||
@@ -409,8 +439,42 @@ flowchart TD
|
||||
> 4. **数据隔离**:不要用 `FLUSHALL`,集群里会影响所有节点
|
||||
> 5. **大 key 慎用**:集群下大 key 无法拆分,会导致单个节点内存/带宽瓶颈
|
||||
|
||||
## 大 Key 的检测与处理
|
||||
|
||||
> [!WARNING] 为什么集群下大 Key 更危险?
|
||||
> 单机 Redis 的大 Key 只影响自己;集群模式下,大 Key 不可拆分——它**固定落在某个 slot、某台机器**。这台机器的 CPU、内存、带宽瞬间成为瓶颈,相当于把集群退化成了单点。
|
||||
|
||||
**检测:**
|
||||
|
||||
```bash
|
||||
# redis-cli 自带扫描(Redis 4.0+)
|
||||
redis-cli --bigkeys # 只统计每种类型中最大的 key
|
||||
redis-cli --memkeys # 按内存占用排序,更精确
|
||||
redis-cli --bigkeys --i 0.01 # 控制扫描间隔(秒),避免线上打满带宽
|
||||
|
||||
# 输出示例
|
||||
# [00.00%] Biggest string found so far 'user:100:feed' with 536870912 bytes
|
||||
# [00.01%] Biggest hash found so far 'order:cache' with 876543 entries
|
||||
```
|
||||
|
||||
**处理策略:**
|
||||
|
||||
| 类型 | 问题 | 拆分方案 |
|
||||
|------|------|---------|
|
||||
| 大 String | `GET` 一次传输全量数据 | 拆成多个子 key:`item:1:chunk1`、`item:1:chunk2` |
|
||||
| 大 Hash | `HGETALL` 阻塞,cursor 遍历慢 | 按字段分桶:`user:100:profile:v1`、`user:100:profile:v2` |
|
||||
| 大 List | `LRANGE` 一次返回全量 | 只保留最近 N 条,历史数据归档到 DB/ES |
|
||||
| 大 Set/ZSet | `SMEMBERS`/`ZREVRANGE` 带宽打满 | 分段查询 + 游标,或业务层拆成多个小集合 |
|
||||
|
||||
> [!TIP] 异步删除(Redis 4.0+)
|
||||
> 对已有的大 Key,优先用 `UNLINK`(异步删除)替代 `DEL`(同步阻塞),防止删除操作拖垮主线程。删除前用 `DEBUG OBJECT key` 查看 `serializedlength` 估算影响范围。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/Redis/07-集群方案/一致性哈希]] — 一致性哈希原理与虚拟节点
|
||||
- [[hhs/Redis/07-集群方案/Gossip协议]] — Gossip 协议的传播原理与收敛特性
|
||||
- [[hhs/Redis/07-集群方案/Raft共识算法]] — Raft 领导者选举与 Redis Failover
|
||||
- [[hhs/Redis/07-集群方案/脑裂问题]] — 脑裂问题的本质与防护策略
|
||||
- [[hhs/Redis/06-主从与哨兵]] — Sentinel 单主高可用方案
|
||||
- [[hhs/Redis/11-运维与性能调优]] — 监控指标与告警
|
||||
- [[hhs/Redis/README]] — 知识索引总览
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
tags: [分布式, Gossip, 最终一致性, 去中心化, Redis Cluster]
|
||||
create time: 2026-05-27 10:00
|
||||
---
|
||||
|
||||
# Gossip 协议
|
||||
|
||||
## 概述
|
||||
|
||||
Gossip 协议(也叫 Epidemic Protocol,流行病协议)是一种**去中心化的信息传播协议**。它的灵感来自社交网络中的"八卦传播"——一个人把消息告诉几个朋友,每个朋友再告诉他们各自的几个朋友,信息很快就会传遍整个社交圈。
|
||||
|
||||
Redis Cluster 使用 Gossip 协议在节点之间同步集群状态(谁是 Master、哪些 slot 归谁、节点是否健康),而不需要一个中心化的协调者。
|
||||
|
||||
## 一、为什么需要 Gossip?
|
||||
|
||||
分布式集群面临一个基本问题:**每个节点如何知道其他节点的状态?**
|
||||
|
||||
两种经典方案:
|
||||
|
||||
| 方案 | 做法 | 优点 | 缺点 |
|
||||
|------|------|------|------|
|
||||
| **中心化注册中心** | 引入 ZooKeeper/etcd 管理节点信息 | 强一致,查询快 | 单点故障风险,增加运维复杂度 |
|
||||
| **去中心化 Gossip** | 节点之间互相"八卦",没有中心 | 无单点故障,天然高可用 | 最终一致性,传播有延迟 |
|
||||
|
||||
Redis Cluster 选择了后者——Redis 本身就是一个高性能的单线程数据库,引入外部依赖(ZK/etcd)会增加架构复杂度,违背了 Redis 轻量、快速的设计哲学。
|
||||
|
||||
## 二、Gossip 的传播原理
|
||||
|
||||
Gossip 协议的核心思想可以归结为三步,每一步都体现了"少而精"的通信策略:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["节点 A<br/>有新消息"] -->|"1. 随机选择几个邻居"| B["节点 B, C, D"]
|
||||
B -->|"2. 收到后继续转发"| E["节点 E, F"]
|
||||
C -->|"2. 收到后继续转发"| G["节点 G, H"]
|
||||
E -->|"3. 持续传播直到收敛"| I["所有节点"]
|
||||
```
|
||||
|
||||
具体到 Redis Cluster,传播方式如下:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Node A
|
||||
participant B as Node B
|
||||
participant C as Node C
|
||||
participant D as Node D
|
||||
|
||||
Note over A: 新状态: slot 0~5460 归我管
|
||||
A->>B: PING (携带 A 的状态摘要 + 随机挑 3 个节点状态)
|
||||
B->>A: PONG (携带 B 的状态摘要)
|
||||
A->>C: PING (携带更新后的状态)
|
||||
C->>A: PONG
|
||||
Note over A,B,C,D: 几轮之后<br/>所有节点都知道 slot 0~5460 归 A 管
|
||||
```
|
||||
|
||||
### 收敛速度
|
||||
|
||||
Gossip 的收敛速度与集群规模的关系:
|
||||
|
||||
- **传播轮数**:O(log N) 轮后信息传遍所有节点
|
||||
- **每轮通信量**:每个节点选 min(3, sqrt(N)) 个目标
|
||||
|
||||
| 集群规模 | 每节点每轮通信目标数 | 大约收敛轮数 | 总消息量 |
|
||||
|---------|-------------------|------------|---------|
|
||||
| 6 节点 | 3 | ~3 轮 | ~18 条 |
|
||||
| 100 节点 | 10 | ~7 轮 | ~700 条 |
|
||||
| 1000 节点 | 32 | ~10 轮 | ~10000 条 |
|
||||
|
||||
> [!NOTE] Redis Cluster 的 Gossip 通信频率
|
||||
> 每秒执行 10 次(即每 100ms 一次),每次从集群中随机选取 5 个节点,从中选出最久没通信的一个节点发送 PING。这个频率在 `cluster-node-timeout` 的窗口内足以让状态收敛。
|
||||
|
||||
> [!QUESTION] 思考
|
||||
> 为什么不直接让每个节点把消息广播给所有其他节点?对比一下"全广播"和"Gossip"的带宽开销。
|
||||
|
||||
## 三、Gossip 的四种消息类型
|
||||
|
||||
Redis Cluster 的 Gossip 通信使用以下四种消息(实际协议中有更多,这四种是核心):
|
||||
|
||||
| 消息类型 | 发送条件 | 作用 |
|
||||
|---------|---------|------|
|
||||
| **PING** | 每秒定时发送 | 心跳探活 + 携带发送者自身状态和随机选中的节点状态 |
|
||||
| **PONG** | 收到 PING 后回复 | 确认存活 + 携带接收者自身状态 |
|
||||
| **MEET** | 新节点加入集群 | 强制目标节点加入集群(区别于 PING 的"可选") |
|
||||
| **FAIL** | 检测到某节点不可达 | 广播故障通知,触发 Failover |
|
||||
|
||||
> [!NOTE] PING 报文不只是心跳
|
||||
> PING 报文中携带的信息远不止"我还活着"。它包含:
|
||||
> - 发送节点的 nodeId、IP、端口、角色(Master/Slave)
|
||||
> - 集群当前的 **clusterEpoch**(配置版本号,用于冲突解决)
|
||||
> - 发送节点负责的**槽位 bitmap**(一个 16384 位的位图)
|
||||
> - 发送节点的 **configEpoch**(本节点见过的最大纪元)
|
||||
> - 随机选择的其他节点的状态(间接传播,每个节点附带 2~3 字节 flags 表示 PFAIL 等状态)
|
||||
>
|
||||
> 这意味着,即使节点 A 从未和节点 D 直接通信,A 也能通过 B 的 PING 报文了解到 D 的状态——这就是"八卦"的力量。同时,epoch 机制保证了:当两个节点对 slot 归属有分歧时,epoch 更大的配置胜出,解决了最终一致性的冲突问题。
|
||||
|
||||
## 四、故障检测:PFAIL → FAIL 双阶段确认
|
||||
|
||||
Gossip 的一个关键实战应用是故障检测。Redis Cluster 不会因为一次超时就判定节点下线,而是采用**两阶段确认**,避免误判:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Node A
|
||||
participant B as Node B
|
||||
participant C as Node C
|
||||
participant D as Node D
|
||||
|
||||
A->>A: 与 Node D 通信超时
|
||||
Note over A: 标记 D 为 PFAIL<br/>(可能下线, 暂不处理)
|
||||
A->>B: PING (携带: D is PFAIL)
|
||||
A->>C: PING (携带: D is PFAIL)
|
||||
B->>A: PONG (我这里 D 也超时了, PFAIL)
|
||||
C->>A: PONG (D 对我正常, 无 PFAIL)
|
||||
Note over A: 收集到集群半数以上<br/>Master 对 D 的 PFAIL 票
|
||||
Note over A: D 从 PFAIL 升级为 FAIL<br/>广播 FAIL 消息
|
||||
A->>B: FAIL(D)
|
||||
A->>C: FAIL(D)
|
||||
Note over B,C: 收到 FAIL 后<br/>触发 D 的 Slave 故障转移
|
||||
```
|
||||
|
||||
> [!IMPORTANT] 为什么要两阶段?
|
||||
> **PFAIL(Possibly Fail)** 是本地判断:节点 A 发现 D 通信超时,可能只是 A 和 D 之间网络抖动。
|
||||
> **FAIL** 是集群共识:只有当集群中**过半 Master 节点**都在 `cluster-node-timeout` 窗口内标记 D 为 PFAIL,才会升级为 FAIL。
|
||||
>
|
||||
> 这个设计本质上是一个轻量级的"多数派投票"——和 Raft 中的选举逻辑异曲同工,但通过 Gossip 而非显式投票完成。
|
||||
|
||||
## 五、Gossip 的局限性
|
||||
|
||||
Gossip 并非完美方案,它有一个核心 trade-off:
|
||||
|
||||
> [!IMPORTANT] 最终一致性,而非强一致性
|
||||
> Gossip 保证信息**最终**会传遍所有节点,但不保证**立刻**。在传播过程中,不同节点对集群状态的认知可能是不一致的。
|
||||
>
|
||||
> 这就是为什么 Redis Cluster 需要 **epoch(集群纪元)** 来解决冲突——当两个节点对 slot 归属有分歧时,epoch 更大的配置胜出。
|
||||
|
||||
其他局限:
|
||||
|
||||
| 局限 | 说明 | Redis 的应对 |
|
||||
|------|------|------------|
|
||||
| 消息冗余 | 同一条信息可能被多次传播 | 报文中携带 epoch,节点收到旧 epoch 的消息直接丢弃 |
|
||||
| 不适合实时场景 | 收敛需要数秒 | 故障检测用 PFAIL/FAIL 双阶段确认,不依赖 Gossip 收敛 |
|
||||
| 带宽开销随节点增长 | O(N log N) 级,每节点每轮 min(3, √N) 个目标 | 大集群通过调低通信频率、使用 proxy 分层缓解 |
|
||||
|
||||
> [!QUESTION] 对比思考
|
||||
> Gossip 和 Raft 都能解决分布式一致性问题,但适用场景不同。前者是"最终一致",后者是"强一致"。结合 [[hhs/Redis/07-集群方案/Raft共识算法]],思考:Redis Cluster 为什么同时用了这两种协议——Gossip 负责什么?Raft 负责什么?
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/Redis/07-集群方案]] — Redis Cluster 集群方案总览
|
||||
- [[hhs/Redis/07-集群方案/Raft共识算法]] — 对比理解:Gossip 是最终一致,Raft 是强一致;故障转移中 Slave 选举使用类 Raft 投票
|
||||
- [[hhs/Redis/06-主从复制/哨兵模式]] — 对比理解:哨兵模式也做故障检测,但由哨兵节点集中负责,而 Cluster 用 Gossip 去中心化完成
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
tags: [分布式, Raft, 共识算法, 选举, 故障转移]
|
||||
create time: 2026-05-27 10:00
|
||||
---
|
||||
|
||||
# Raft 共识算法
|
||||
|
||||
## 概述
|
||||
|
||||
Raft 是一种分布式**共识算法**,用于让多个节点就某个值达成一致。它的设计目标是比 Paxos 更容易理解和实现。Redis Cluster 的故障转移(Failover)借鉴了 Raft 的**领导者选举**机制来选出新的 Master。
|
||||
|
||||
> [!NOTE] "Raft 式投票"不等于完整 Raft
|
||||
> Redis Cluster 只使用了 Raft 的**选举(Leader Election)** 部分,并没有使用完整的 Raft 日志复制机制。Redis 的数据复制走的是自身的异步复制协议。所以文档中说的是"Raft **式**投票"。
|
||||
|
||||
## 一、为什么需要共识?
|
||||
|
||||
在分布式系统中,当一个节点宕机时,其他节点需要达成一致:"这个节点确实挂了",并且"由谁来接替它"。如果各节点自行判断、自行行动,就会出现混乱——比如两个节点同时认为自己应该接管服务。
|
||||
|
||||
> [!QUESTION] 直觉思考
|
||||
> 一个 3 人小组的组长突然失踪了。剩下的两个人可以投票选出新组长。但如果 6 人小组刚好分成两个 3 人小组,两组各自选出了一个新组长——怎么办?这就是共识算法要解决的核心问题:**在有故障和网络分区的情况下,如何让大多数节点达成一致决定?**
|
||||
|
||||
## 二、Raft 选举核心机制
|
||||
|
||||
Raft 将节点分为三种角色:
|
||||
|
||||
- **Leader(领导者)**:负责处理所有写请求,向 Follower 复制日志
|
||||
- **Follower(追随者)**:被动接收 Leader 的心跳和日志
|
||||
- **Candidate(候选人)**:Follower 超时未收到心跳后,临时切换的角色,负责发起选举请求。如果赢得多数票则晋升为 Leader,失败则退回 Follower
|
||||
|
||||
> [!NOTE] 角色转换
|
||||
> 所有节点初始都是 Follower。正常运行时只有 Follower → Candidate → Leader 这条路径。集群稳定后 Leader 持续发送心跳,Follower 保持被动,不会发起选举。
|
||||
|
||||
当 Follower 在一段时间内没有收到 Leader 的心跳,就会发起选举:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant F1 as Follower 1
|
||||
participant F2 as Follower 2
|
||||
participant F3 as Follower 3
|
||||
|
||||
Note over F1,F3: Leader 宕机,Follower 超时
|
||||
F1->>F1: 超时,升级为 Candidate
|
||||
F1->>F2: RequestVote (我的 term=2, 日志最新)
|
||||
F1->>F3: RequestVote (我的 term=2, 日志最新)
|
||||
F2->>F1: VoteGranted (同意)
|
||||
F3->>F1: VoteGranted (同意)
|
||||
Note over F1: 获得多数票<br/>成为新 Leader
|
||||
F1->>F2: AppendEntries (心跳, 我是 Leader)
|
||||
F1->>F3: AppendEntries (心跳, 我是 Leader)
|
||||
```
|
||||
|
||||
### 关键概念
|
||||
|
||||
> [!QUESTION] 如果两个 Follower 同时超时会怎样?
|
||||
> 它们会同时变成 Candidate 并各自向对方拉票——谁都拿不到多数票,选举失败。Raft 的解决办法是**随机化选举超时**:每个节点的超时时间在 `[T, 2T]` 区间内随机选取(T 通常为 150~300ms)。这样第一个超时的节点大概率会率先完成选举,避免了"选票被瓜分"的僵局。
|
||||
|
||||
| 概念 | 含义 | Redis Cluster 中的对应 |
|
||||
|------|------|---------------------|
|
||||
| **Term(任期)** | 逻辑时钟,每次选举递增 | **Epoch(集群纪元)** |
|
||||
| **Election Timeout(选举超时)** | Follower 等待心跳的超时时间,超时则发起选举 | `cluster-node-timeout`(默认 15s) |
|
||||
| **Heartbeat(心跳)** | Leader 定期向 Follower 发送 AppendEntries RPC 维持地位 | Master → Replica 的 PING/PONG |
|
||||
| **RequestVote** | Candidate 请求投票的 RPC | Replica 向 Master 发起投票请求 |
|
||||
| **多数派(Quorum)** | 超过半数节点同意 | 多数 Master 节点投票 |
|
||||
| **先到先得** | 每个节点在一个 term 内只能投一票 | 同一个 epoch 内,每个 Master 只投一票 |
|
||||
| **随机化超时** | 每个节点的选举超时设置为一个随机值,避免同时发起选举 | Replica 选举延迟与复制偏移量挂钩 |
|
||||
|
||||
> [!TIP] 为什么需要"多数派"?
|
||||
> 在 N 个节点中,任何两个"多数派"一定有交集。这意味着不可能同时存在两个都获得了多数票的 Candidate——**保证了最多只有一个 Leader**。这是 Raft 正确性的基石。
|
||||
>
|
||||
> 数学上:如果 N=3,多数派是 2;如果 N=5,多数派是 3。这也解释了为什么 Redis Cluster 建议使用**奇数个 Master**——4 个 Master 的多数派是 3,只能容忍 1 个故障(和 3 个 Master 的容错能力完全相同),但多了一个节点的开销;而 5 个 Master 能容忍 2 个故障,容错能力真正提升了。
|
||||
|
||||
## 三、Redis Cluster 的 Failover 选举
|
||||
|
||||
Redis Cluster 将 Raft 选举简化应用到故障转移场景:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Master 宕机"] --> B["Replica 超时未收到心跳"]
|
||||
B --> C["Replica 将 epoch + 1"]
|
||||
C --> D["向所有 Master 请求投票"]
|
||||
D --> E{"获得多数票?"}
|
||||
E -->|是| F["提升为新 Master<br/>接管 slot 并广播"]
|
||||
E -->|否| G["等待随机超时后重试"]
|
||||
|
||||
style A fill:#fce4ec,stroke:#e91e63
|
||||
style F fill:#e8f5e9,stroke:#4caf50
|
||||
style G fill:#fff3e0,stroke:#ff9800
|
||||
```
|
||||
|
||||
**详细步骤:**
|
||||
|
||||
1. **超时检测**:Replica 发现 Master 超过 `cluster-node-timeout` 未响应 PONG
|
||||
2. **纪元递增**:Replica 将当前 epoch 加 1(类似 Raft 的 term 递增)
|
||||
3. **发起投票**:向集群中所有 Master 节点发送投票请求
|
||||
4. **投票规则**:每个 Master 在同一个 epoch 内**只投一票**,先到先得
|
||||
5. **赢得选举**:获得多数 Master 投票的 Replica 升级为 Master
|
||||
6. **广播新配置**:新 Master 接管旧 Master 的所有 slot,通知集群所有节点
|
||||
|
||||
> [!WARNING] 选举失败怎么办?
|
||||
> 如果多个 Replica 同时发起选举且票数被瓜分,谁都拿不到多数票——选举失败。Redis Cluster 的解决方案是**随机退避**:每个 Replica 等待一个随机时间后再发起下一轮选举,减少再次冲突的概率。
|
||||
>
|
||||
> **Redis 的巧妙设计**:这个随机延迟并非完全随机,而是与 Replica 的**复制偏移量(replication offset)** 挂钩——数据越新的 Replica,等待时间越短(`DELAY_FACTOR * 固定延迟 + 随机抖动`)。这保证了**数据最新的 Replica 大概率优先发起选举**,从而最大程度减少数据丢失。
|
||||
|
||||
## 四、与 Paxos 的简要对比
|
||||
|
||||
| 特性 | Raft | Paxos |
|
||||
|------|------|-------|
|
||||
| 可理解性 | 相对简单,角色清晰 | 极其晦涩,号称"只有一个人理解 Paxos" |
|
||||
| 工程实现 | etcd、Consul、Redis(部分) | Google Chubby、Spanner |
|
||||
| 适用场景 | Leader 选举 + 日志复制 | 任意值的共识 |
|
||||
| 活跃节点要求 | 需要多数派存活 | 需要多数派存活 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/Redis/07-集群方案]] — Redis Cluster 集群方案
|
||||
- [[hhs/Redis/07-集群方案/脑裂问题]] — 当出现两个 Leader 时怎么办
|
||||
- [[hhs/Redis/07-集群方案/Gossip协议]] — 节点间如何互相发现和交换状态
|
||||
- [[hhs/Redis/07-集群方案/一致性哈希]] — 数据分片的基础理论
|
||||
@@ -0,0 +1,181 @@
|
||||
---
|
||||
tags: [分布式, 一致性哈希, 哈希环, 数据分片]
|
||||
create time: 2026-05-27 10:00
|
||||
---
|
||||
|
||||
# 一致性哈希
|
||||
|
||||
## 概述
|
||||
|
||||
一致性哈希(Consistent Hashing)是分布式系统中解决**数据如何均匀分配到多个节点**的经典算法。它在 1997 年由 Karger 等人提出,核心目标是:**当节点增减时,尽量少地迁移数据**。
|
||||
|
||||
Redis Cluster 最终没有采用一致性哈希,而是选择了更简单的哈希槽方案,但理解一致性哈希是理解分布式分片的基础。
|
||||
|
||||
## 正文
|
||||
|
||||
## 一、传统取模方案的问题
|
||||
|
||||
最直观的分片方式是对 key 取模:`hash(key) % N`(N 是节点数)。
|
||||
|
||||
> [!QUESTION] 这个方案有什么问题?
|
||||
> 假设有 3 个节点,突然要加到 4 个节点——N 从 3 变成 4,几乎所有 key 的取模结果都变了,意味着**几乎所有数据都需要重新分配**。对于百万级数据来说,这是一场灾难。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
K["key: hash=7"] --> M["7 % 3 = 1<br/>Node 1"]
|
||||
K2["key: hash=7"] --> M2["7 % 4 = 3<br/>Node 3"]
|
||||
|
||||
style M fill:#e8f5e9,stroke:#4caf50
|
||||
style M2 fill:#fce4ec,stroke:#e91e63
|
||||
```
|
||||
|
||||
同一个 key,只因为节点数变了,就被分配到了完全不同的节点。一致性哈希正是为了解决这个问题。
|
||||
|
||||
## 二、哈希环——一致性哈希的核心结构
|
||||
|
||||
一致性哈希将整个哈希值空间组织成一个**虚拟的环**,范围是 `[0, 2^32)`。
|
||||
|
||||
**步骤:**
|
||||
1. **节点映射**:对每个节点的标识(如 IP:Port)做 hash,映射到环上的某个位置
|
||||
2. **数据映射**:对每个 key 做 hash,同样映射到环上
|
||||
3. **顺时针查找**:从 key 在环上的位置出发,沿顺时针方向找到的**第一个节点**,就是这个 key 应该存储的位置
|
||||
|
||||
> [!TIP] 哈希函数的选择
|
||||
> 一致性哈希要求 hash 函数的输出尽可能均匀分布,实践中常用 **MurmurHash** 或 **xxHash** 这类非加密哈希函数。它们速度快、分布性好,远优于简单的取模或 CRC32。Java 中的 `TreeMap`、Go 中的 `sort.Search` 都可以用来高效实现顺时针查找。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
N1["Node A: hash=100"] --> K1["Key 1: hash=200"]
|
||||
K1 -->|"顺时针归属"| N2["Node B: hash=300"]
|
||||
N2 --> K2["Key 2: hash=350"]
|
||||
K2 -->|"顺时针归属"| N3["Node C: hash=500"]
|
||||
N3 --> K3["Key 3: hash=80"]
|
||||
K3 -->|"顺时针归属"| N1
|
||||
|
||||
style N1 fill:#e8eaf6,stroke:#3f51b5
|
||||
style N2 fill:#e8eaf6,stroke:#3f51b5
|
||||
style N3 fill:#e8eaf6,stroke:#3f51b5
|
||||
style K1 fill:#fff3e0,stroke:#ff9800
|
||||
style K2 fill:#fff3e0,stroke:#ff9800
|
||||
style K3 fill:#fff3e0,stroke:#ff9800
|
||||
```
|
||||
|
||||
> 上图将环形结构展开为线性示意。在实际的哈希环上,Key 3(hash=80)从 80 位置沿顺时针方向,经过 100 处的 Node A,所以它归属于 Node A。
|
||||
|
||||
**核心查找逻辑**:用一个排序数组维护环上所有节点位置,然后二分查找"第一个 >= key hash 值的位置",取模回到环头即为顺时针下一节点:
|
||||
|
||||
```go
|
||||
// ring 是所有节点 hash 值的升序排列
|
||||
func getTargetNode(ring []uint32, keyHash uint32) uint32 {
|
||||
// 二分查找:第一个 >= keyHash 的位置
|
||||
idx := sort.Search(len(ring), func(i int) bool {
|
||||
return ring[i] >= keyHash
|
||||
})
|
||||
// 如果没找到,说明 key 落在环的尾部,回到环头(顺时针绕回)
|
||||
if idx == len(ring) {
|
||||
idx = 0
|
||||
}
|
||||
return ring[idx]
|
||||
}
|
||||
```
|
||||
|
||||
> [!TIP] 关键优势
|
||||
> 当新增或删除一个节点时,**只有该节点到逆时针方向的前一个节点之间的 key 需要迁移**,其他 key 的归属完全不变。节点变动时,迁移数据量从 O(N) 降到 O(K/N)(K 是总 key 数,N 是节点数)。
|
||||
|
||||
## 三、虚拟节点——解决数据倾斜
|
||||
|
||||
基本的一致性哈希有一个致命问题:如果节点在环上分布不均匀,某些节点会承担远超平均值的数据量。
|
||||
|
||||
> [!QUESTION] 极端情况
|
||||
> 如果 3 个节点的 hash 值恰好都挤在环的某 1/4 区域,那剩余 3/4 的数据全部落到一个节点上——这个节点瞬间成为热点。
|
||||
|
||||
**解决方案:虚拟节点(Virtual Nodes)**。每个物理节点对应环上的多个虚拟节点:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Ring["哈希环上的虚拟节点分布"]
|
||||
VA1["Node A-1"]
|
||||
VB1["Node B-1"]
|
||||
VA2["Node A-2"]
|
||||
VC1["Node C-1"]
|
||||
VB2["Node B-2"]
|
||||
VA3["Node A-3"]
|
||||
VC2["Node C-2"]
|
||||
VB3["Node B-3"]
|
||||
VC3["Node C-3"]
|
||||
end
|
||||
|
||||
VA1 -.->|映射| PA["物理 Node A"]
|
||||
VA2 -.->|映射| PA
|
||||
VA3 -.->|映射| PA
|
||||
VB1 -.->|映射| PB["物理 Node B"]
|
||||
VB2 -.->|映射| PB
|
||||
VB3 -.->|映射| PB
|
||||
VC1 -.->|映射| PC["物理 Node C"]
|
||||
VC2 -.->|映射| PC
|
||||
VC3 -.->|映射| PC
|
||||
```
|
||||
|
||||
每个物理节点生成 100~200 个虚拟节点,分布在环的不同位置。这样即使物理节点数量很少,数据也能近似均匀分布。
|
||||
|
||||
```go
|
||||
type ConsistentHash struct {
|
||||
replicas int // 每个物理节点的虚拟节点数
|
||||
ring []uint32 // 虚拟节点 hash 排序数组
|
||||
vnodeMap map[uint32]string // 虚拟节点 hash → 物理节点名
|
||||
}
|
||||
|
||||
func (ch *ConsistentHash) Add(node string) {
|
||||
for i := 0; i < ch.replicas; i++ {
|
||||
// 虚拟节点 key = 物理节点名 + 编号
|
||||
vkey := fmt.Sprintf("%s#%d", node, i)
|
||||
h := hash(vkey)
|
||||
ch.ring = append(ch.ring, h)
|
||||
ch.vnodeMap[h] = node
|
||||
}
|
||||
sort.Slice(ch.ring, func(i, j int) bool { return ch.ring[i] < ch.ring[j] })
|
||||
}
|
||||
|
||||
func (ch *ConsistentHash) Get(key string) string {
|
||||
h := hash(key)
|
||||
idx := sort.Search(len(ch.ring), func(i int) bool { return ch.ring[i] >= h })
|
||||
if idx == len(ch.ring) { idx = 0 }
|
||||
return ch.vnodeMap[ch.ring[idx]] // 虚拟节点 → 物理节点
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE] 虚拟节点数量的权衡
|
||||
> 虚拟节点越多,数据分布越均匀,但也会增加内存开销和查找时间。经验值 100~200 个是工程上的平衡点——在 3~10 个物理节点的场景下已经足够均匀。当节点数增多时,虚拟节点数可以适当减少。
|
||||
|
||||
## 四、一致性哈希 vs 哈希槽
|
||||
|
||||
Redis Cluster 最终选择了**哈希槽**而非一致性哈希,两者的核心区别在于:
|
||||
|
||||
| 对比维度 | 一致性哈希 | 哈希槽(Redis Cluster) |
|
||||
|---------|-----------|----------------------|
|
||||
| 分片单位 | 连续的 hash 区间 | 固定的 16384 个离散槽 |
|
||||
| 节点增减 | 邻居区间数据迁移 | 指定槽号迁移,粒度更可控 |
|
||||
| 数据分布 | 依赖虚拟节点平衡 | 天然均匀(槽均分即可) |
|
||||
| 元数据 | 只需记录节点位置 | 需要维护 slot → node 映射表 |
|
||||
| 实现复杂度 | 较低 | 略高(需要槽位管理) |
|
||||
|
||||
> [!NOTE] 为什么 Redis 选择了哈希槽?
|
||||
> 哈希槽方案下,数据迁移的粒度是**整个槽**而非连续区间,运维可以精确控制"迁哪些 slot",配合 `CLUSTER SETSLOT` 命令可以做到在线平滑迁移。而一致性哈希的迁移区间是隐式的,运维不够直观。
|
||||
|
||||
### 一致性哈希的实际应用
|
||||
|
||||
虽然 Redis 没有采用,但一致性哈希在分布式系统中被广泛使用:
|
||||
|
||||
| 系统 | 应用方式 |
|
||||
|------|---------|
|
||||
| **Amazon DynamoDB** | 使用一致性哈希(带虚拟节点)管理数据分区 |
|
||||
| **Apache Cassandra** | 早期版本使用一致性哈希,后来改为更可控的分区策略 |
|
||||
| **Memcached 客户端** | 客户端侧使用一致性哈希决定缓存路由 |
|
||||
| **Nginx + upstream** | `hash` 负载均衡策略基于一致性哈希 |
|
||||
|
||||
> [!QUESTION] 思考:一致性哈希适合什么场景?
|
||||
> 当节点数量**动态变化频繁**、且对迁移的数据量有严格要求时(如缓存集群、CDN 节点路由),一致性哈希是更好的选择。而当节点数量相对稳定、运维需要精确控制分片时(如 Redis Cluster),哈希槽方案更为实用。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/Redis/07-集群方案]] — Redis Cluster 集群方案(哈希槽机制)
|
||||
@@ -0,0 +1,214 @@
|
||||
---
|
||||
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 模式下同样存在脑裂问题
|
||||
Reference in New Issue
Block a user