vault backup: 2026-06-08 23:08:57

This commit is contained in:
hhs
2026-06-08 23:08:57 +08:00
parent d67831afe0
commit 51de196713
136 changed files with 26069 additions and 344 deletions
+115 -10
View File
@@ -29,8 +29,8 @@ flowchart TD
| 对比项 | 一致性哈希 | 哈希槽 |
|--------|-----------|---------|
| 增节点迁移量 | **~N/K**(N 为总 key 数,K 为节点数) | **total_slots × 1/N** ≈ 每个新节点分得 ~2730 slots |
| 客户端透明性 | ✅ 通常透明,按哈希环顺时针归属 | ⚠️ 需要 MOVED/ASK 重定向 |
| 增节点迁移量 | **~N/K**(N 为总 key 数,K 为节点数) | 新节点分得 **16384/(K+1)** slots,约 2730 个(6 Master 时) |
| 客户端透明性 | ✅ 通常透明,按哈希环顺时针归属 | ✅ 主流客户端(go-redis/jedis)内置处理 MOVED/ASK 重定向,对业务透明 |
| 数据倾斜 | 依赖虚拟节点数量平衡 | ❌ slots 分配不均时会倾斜 |
| 复杂度 | 简单,纯哈希环 | 略复杂(Gossip + 槽位元数据同步) |
@@ -100,7 +100,7 @@ sequenceDiagram
B->>A: PONG (含自身 + 选中节点的状态)
end
Note over A: Gossip 收敛原理:<br/>集群越大 fanout 越大,约 O(sqrt(N)),<br/>O(log(N)) 轮即可全同步
Note over A: Gossip 收敛原理:<br/>每节点每轮向 min 6 台对等体发消息,<br/>O(log N) 轮后信息传遍集群
```
**Gossip 传播的三类信息:**
@@ -192,15 +192,52 @@ version: "3.9"
services:
redis-1:
image: redis:7-alpine
command: redis-server --cluster-enabled yes --appendonly yes
ports: ["7001:7001"]
volumes: ["redis-data-1:/data"]
# ... 重复 redis-2 ~ redis-6
command: >
redis-server
--cluster-enabled yes
--cluster-config-file nodes.conf
--cluster-node-timeout 5000
--appendonly yes
ports:
- "7001:7001"
- "17001:17001" # cluster bus 端口,必须暴露
volumes:
- redis-data-1:/data
networks:
- redis-cluster
# redis-2 ~ redis-6 同理,修改端口即可
# 注意每个实例的 port 也需要通过 --port 指定
redis-2:
image: redis:7-alpine
command: >
redis-server
--port 7002
--cluster-enabled yes
--cluster-config-file nodes.conf
--cluster-node-timeout 5000
--appendonly yes
ports:
- "7002:7002"
- "17002:17002"
volumes:
- redis-data-2:/data
networks:
- redis-cluster
# ... redis-3 ~ redis-6 类推
networks:
redis-cluster:
driver: bridge
volumes:
redis-data-1:
redis-data-2:
# ...
```
> [!WARNING] Docker 网络注意
> 集群组建时各节点需要通过 IP 互相访问,如果在不同机器部署,`--net=host` 模式或自定义网络(指定 `cluster-announce-ip`)是必须的。本地开发用 `127.0.0.1` 即可,但生产环境切勿依赖 localhost。
### 连接集群客户端
```go
@@ -248,6 +285,16 @@ val, _ := rdb.Get(ctx, "key").Result()
# 集群状态
redis-cli --cluster check 127.0.0.1:7001
# 查看集群整体信息(快速判断集群是否健康)
redis-cli -c -p 7001 CLUSTER INFO
# 关键字段:
# cluster_state:ok ← ok 表示正常,fail 表示有 slot 不可用
# cluster_slots_assigned:16384 ← 应为 16384,否则有 slot 未分配
# cluster_slots_ok:16384
# cluster_known_nodes:6 ← 集群已知节点总数
# cluster_size:3 ← Master 节点数量
# cluster_current_epoch:6 ← 当前集群纪元,每次 failover/reshard 递增
# 查看节点信息
redis-cli -p 7001 CLUSTER NODES
# 输出格式: nodeId ip:port@busPort flags master/slot-range self
@@ -278,6 +325,25 @@ flowchart LR
> 2. 再给每个 Master 配 Replica
> 3. 分批迁移(每次 1~2 台),避免流量剧变
### 数据倾斜检测与 Rebalance
扩容或运行一段时间后,各节点的 key 数量可能不均——有些节点"撑"了很多 key,有些却"空"着,这就是**数据倾斜**。
```bash
# 查看各节点 key 数量,快速判断是否倾斜
redis-cli -c -p 7001 CLUSTER COUNTKEYSINSLOT 0 # 查某个 slot 有多少 key
redis-cli --cluster rebalance 127.0.0.1:7001 # 自动均衡 slot 分布
```
> [!NOTE] reshard vs rebalance
> - `reshard`:手动指定从 A 搬多少 slot 到 B,精确控制
> - `rebalance`:自动计算各节点应有 slot 数,自动迁移,省心但不可控
>
> 生产环境建议先用 `rebalance --dry-run` 预览迁移计划,确认无误后再执行。
> [!WARNING] Rebalance 不能解决 key 分布不均
> `rebalance` 均衡的是 **slot 数量**,而非 key 数量。如果某些 slot 内 key 特别多(比如大量 key 哈希到同一 slot),rebalance 无法解决。这种情况需要检查是否缺少 hash tag 设计,或考虑在应用层做 key 拆分。
## 故障转移机制(Failover)
> [!QUESTION] 思考
@@ -327,6 +393,21 @@ flowchart LR
> [!NOTE] 脑裂保护
> 脑裂是指网络分区把集群割裂成多个子集群,各自选举出 Master 同时对外服务。Redis 通过 **epoch 版本号** 解决冲突——epoch 更小的 Master 被强制降级。此外,建议配置 `min-replicas-to-write 1`,当 Master 联系不到足够 Replica 时主动拒绝写入,防止分区恢复后数据丢失。详见 [[hhs/Redis/07-集群方案/脑裂问题|脑裂问题]]。
### 旧 Master 恢复后的行为
> [!QUESTION] 思考
> Master A 宕机,Replica A' 接管。几分钟后 A 重新上线,会发生什么?
旧 Master 恢复后**不会抢回 Master 身份**,而是自动降级为 Replica,同步新 Master 的数据。流程如下:
1. 旧 Master 重新加入集群,通过 Gossip 收到最新配置(发现自己的 slots 已被别人接管,且 configEpoch 更大)
2. 旧 Master 主动将自己**降级为 Replica**,指向新 Master
3. 发起**全量同步**(PSYNC FULL),用新 Master 的数据覆盖自己落后或分歧的数据
4. 同步完成后,作为 Replica 继续提供读服务(如果客户端配置了读副本)
> [!WARNING] 旧 Master 有写入风险
> 如果旧 Master 在宕机期间被客户端写入了数据(未同步到 Replica),降级同步后这些数据会**被覆盖丢失**。这就是为什么 `min-replicas-to-write` 很重要——它能在分区时提前阻止孤立 Master 的写入。
### 客户端容错处理
集群模式下客户端会收到两种重定向响应:
@@ -359,7 +440,10 @@ ASK 3999 192.168.1.20:7002
| 拓扑更新 | 遇到 MOVED/ASK 时按需刷新 | 后台定时拉取,提前感知变化 |
| 扩容时体验 | 迁移过渡期频繁 MOVED,请求偶发延迟 | 平滑无感,提前知道新 slot 在哪 |
| 内存开销 | 仅存储已知节点 | 存储全部节点 + slot→node 映射表 |
| 代表库 | lettuce(默认 lazy) | go-redis, jedis(内置拓扑缓存) |
| 代表库 | 无(纯 lazy 已少见) | go-redis, jedis, lettuce |
> [!NOTE] 关于 lettuce
> lettuce 早期版本默认 lazy,但现代版本(6.x+)已支持 `ClusterTopologyRefreshOptions` 主动定时刷新拓扑,本质上已经是"硬客户端"模式。这里分类仅为说明两种策略的**思想差异**,实际选型建议直接看各库的官方文档。
```go
// go-redis 示例:硬客户端,自动维护拓扑
@@ -378,10 +462,16 @@ val, err := rdb.Get(ctx, "user:100:name").Result()
```
```java
// lettuce 示例:软客户端,按需刷新拓扑(Java/Spring)
// lettuce 示例:默认按需刷新,可配置主动定时刷新(Java/Spring)
RedisClusterClient client = RedisClusterClient.create("redis://node1:7001");
// 可选:开启定时拓扑刷新(推荐生产环境启用)
ClusterTopologyRefreshOptions refreshOptions = ClusterTopologyRefreshOptions.builder()
.enablePeriodicRefresh(Duration.ofSeconds(30))
.enableAllAdaptiveRefreshTriggers()
.build();
client.setOptions(ClusterClientOptions.builder()
.topologyRefreshOptions(refreshOptions).build());
StatefulRedisClusterConnection<String, String> conn = client.connect();
// 遇到 MOVED 时,ClusterTopologyRefreshScheduler 自动重新发现拓扑
String val = conn.sync().get("key");
```
@@ -415,6 +505,21 @@ String val = conn.sync().get("key");
> - **`yes`(默认)**:集群整体拒绝读写,宁可停机也不允许数据不一致——适合对一致性要求极高的金融场景。
> - **`no`**:集群只拒绝访问故障 slot 的请求,其他 slot 继续服务——适合追求可用性的互联网业务,牺牲部分可用 slot 换取整体不宕机。
### 连接池配置
集群模式下,客户端为**每个节点维护独立连接池**(而非共享一个池)。合理配置连接池能避免热点节点耗尽连接、冷节点浪费资源。
| 参数 | 建议值 | 说明 |
|------|--------|------|
| `PoolSize`(go-redis)/ `MaxTotal`(jedis) | 每节点 10~20 | 并发量大的业务可适当调高,但不宜超过 50 |
| `MinIdleConns` | 每节点 2~5 | 预留空闲连接,避免冷启动延迟 |
| `ReadTimeout` | 1~3s | 根据业务 P99 延迟设置,别太宽松 |
| `WriteTimeout` | 1~3s | 同上 |
| `IdleTimeout` | 3~5min | 空闲连接回收时间,太短会频繁建连 |
> [!TIP] 如何估算 PoolSize?
> 经验公式:`PoolSize ≈ QPS × 平均耗时(s)`。例如单节点 QPS 为 1000,平均耗时 2ms,则 PoolSize = 1000 × 0.002 = 2,实际上再乘以 3~5 的安全系数,设 10 左右即可。集群有 N 个 Master,每个 Master 的连接池独立,总连接数 = PoolSize × N。
### 架构最佳实践
```mermaid