From 329185507dd71bb2dc8048b862a71fe9c4f90ff2 Mon Sep 17 00:00:00 2001 From: hhs <386998068@qq.com> Date: Wed, 27 May 2026 11:24:47 +0800 Subject: [PATCH] vault backup: 2026-05-27 11:24:47 --- hhs/NETWORK/02-链路层/03-MAC地址与广播域.md | 93 +++++++-- hhs/Redis/07-集群方案.md | 102 ++++++++-- hhs/Redis/07-集群方案/Gossip协议.md | 150 ++++++++++++++ hhs/Redis/07-集群方案/Raft共识算法.md | 118 +++++++++++ hhs/Redis/07-集群方案/一致性哈希.md | 181 +++++++++++++++++ hhs/Redis/07-集群方案/脑裂问题.md | 214 ++++++++++++++++++++ 6 files changed, 827 insertions(+), 31 deletions(-) create mode 100644 hhs/Redis/07-集群方案/Gossip协议.md create mode 100644 hhs/Redis/07-集群方案/Raft共识算法.md create mode 100644 hhs/Redis/07-集群方案/一致性哈希.md create mode 100644 hhs/Redis/07-集群方案/脑裂问题.md diff --git a/hhs/NETWORK/02-链路层/03-MAC地址与广播域.md b/hhs/NETWORK/02-链路层/03-MAC地址与广播域.md index 262f6c6..be74c69 100644 --- a/hhs/NETWORK/02-链路层/03-MAC地址与广播域.md +++ b/hhs/NETWORK/02-链路层/03-MAC地址与广播域.md @@ -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 认证**:端口级准入控制,未认证设备无法通信 + ## 广播风暴与防范 ### 什么是广播风暴? diff --git a/hhs/Redis/07-集群方案.md b/hhs/Redis/07-集群方案.md index 374717b..5ade73c 100644 --- a/hhs/Redis/07-集群方案.md +++ b/hhs/Redis/07-集群方案.md @@ -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 MIGRATING # 在目标节点执行(确认阶段) CLUSTER SETSLOT IMPORTING -# 逐 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 NODE ``` +> [!NOTE] 迁移期间的 ASK 临时定向 +> 当 slot 处于 MIGRATING/IMPORTING 中间状态时: +> - 源节点**拥有该 key** → 正常处理 +> - 源节点**找不到该 key**(已迁移走) → 返回 `ASK `,客户端只需本次请求转发到目标节点,后续请求仍发原节点。 +> +> 这保证了迁移过程对客户端透明,且不会造成路由表的提前混乱。 + ### 自动化迁移 —— 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 潜在失效
单个节点判定"] -->|"超过 cluster-node-timeout"| B["FAIL 确认失效
多数 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 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]] — 知识索引总览 diff --git a/hhs/Redis/07-集群方案/Gossip协议.md b/hhs/Redis/07-集群方案/Gossip协议.md new file mode 100644 index 0000000..bec7a3e --- /dev/null +++ b/hhs/Redis/07-集群方案/Gossip协议.md @@ -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
有新消息"] -->|"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: 几轮之后
所有节点都知道 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
(可能下线, 暂不处理) + 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: 收集到集群半数以上
Master 对 D 的 PFAIL 票 + Note over A: D 从 PFAIL 升级为 FAIL
广播 FAIL 消息 + A->>B: FAIL(D) + A->>C: FAIL(D) + Note over B,C: 收到 FAIL 后
触发 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 去中心化完成 diff --git a/hhs/Redis/07-集群方案/Raft共识算法.md b/hhs/Redis/07-集群方案/Raft共识算法.md new file mode 100644 index 0000000..60b62aa --- /dev/null +++ b/hhs/Redis/07-集群方案/Raft共识算法.md @@ -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: 获得多数票
成为新 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
接管 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-集群方案/一致性哈希]] — 数据分片的基础理论 diff --git a/hhs/Redis/07-集群方案/一致性哈希.md b/hhs/Redis/07-集群方案/一致性哈希.md new file mode 100644 index 0000000..358cc8f --- /dev/null +++ b/hhs/Redis/07-集群方案/一致性哈希.md @@ -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
Node 1"] + K2["key: hash=7"] --> M2["7 % 4 = 3
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 集群方案(哈希槽机制) diff --git a/hhs/Redis/07-集群方案/脑裂问题.md b/hhs/Redis/07-集群方案/脑裂问题.md new file mode 100644 index 0000000..9d4b691 --- /dev/null +++ b/hhs/Redis/07-集群方案/脑裂问题.md @@ -0,0 +1,214 @@ +--- +tags: [分布式, 脑裂, 网络分区, CAP, 故障转移] +create time: 2026-05-27 10:00 +--- + +# 脑裂问题 + +## 概述 + +脑裂(Split Brain)是分布式系统中最危险的故障之一:**集群因为网络分区被割裂成多个独立的子集群,每个子集群都以为自己是"正统",各自选举出 Leader,同时对外提供服务**。如果不能妥善处理,就会出现两个 Master 同时接受写入,导致数据丢失或不一致。 + +## 一、什么是脑裂? + +用一个简单的场景来说明: + +```mermaid +flowchart TD + subgraph 原始集群 + M["Master
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
仍认为自己是 Master"] + end + + subgraph 分区B["网络分区 B (多数派)"] + R1B["Replica 1
被选举为新 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
epoch=5"] + M_New["新 Master
epoch=6"] + end + + After["epoch=6 胜出
原 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 数
>= min-replicas-to-write?"} + C -->|是| A["接受写入"] + C -->|否| R["拒绝写入
返回错误"] + + 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 模式下同样存在脑裂问题