vault backup: 2026-05-27 11:24:47

This commit is contained in:
hhs
2026-05-27 11:24:47 +08:00
parent 91b4fe3f36
commit 329185507d
6 changed files with 827 additions and 31 deletions
+83 -19
View File
@@ -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]] — 知识索引总览