Redis 7.2 Cluster 12 节点扩容到 24 节点期间 P99 从 2.4ms 飙到 1.8 秒的 3 天复盘:大键 MIGRATE 阻塞 + MOVED redirect 风暴 + 客户端 slot 缓存雪崩三重叠加 + 11 条 Cluster 运维纪律

我们一个 Redis 7.2 Cluster 用户会话存储,12 节点扩容到 24 节点,在线 reshard 4 小时窗口里 P99 从 2.4ms 飙到 1.8 秒,应用层 50% 请求 timeout,47 个业务工单。3 天复盘找到三重根因:user:5000:big_session 单 key 12MB MIGRATE 阻塞主线程 200ms、Lettuce/ioredis MOVED redirect 占 30% QPS 触发 topology refresh 锁竞争、客户端 slot 缓存级联失效。修复路径大键扫描拆分 hashtag + reshard 限速分批 + 应用层 admin 协议,扩容期 P99 压回 18ms,工单 0。

2026 年 1 月,我们一个用 Redis 7.2 Cluster 跑的用户会话存储集群,在做"扩容从 12 节点到 24 节点"的 slot 迁移时,出现 P99 从 2.4ms 飙到 1.8 秒、应用层 50% 请求 timeout 的雪崩。原本 redis-cli --cluster reshard 看似"安全的在线扩容",实际上 4 个小时的迁移窗口里业务方报了 47 个工单。我们花了 3 天复盘,发现真凶是 "MOVED redirect 风暴 + 客户端缓存失效雪崩 + 慢键迁移阻塞主线程"三重叠加。

这次复盘是 Redis Cluster 在生产环境扩缩容必读的经验。从最初怀疑网络带宽、节点 CPU,到最终用 redis-cli --latency + INFO commandstats + slowlog 找到 MIGRATE 命令阻塞了 200ms 的慢键,最终把扩容期 P99 从 1.8s 压到 18ms。这篇给一份"Redis Cluster 在线扩容 SOP + 反模式清单"。

项目背景:会话存储集群规模

维度 规模/参数
Redis 版本 7.2.4
部署 K8s StatefulSet + 12 节点(6 主 6 从)
单节点 16GB 内存 + 8 vCPU
key 总数 4.2 亿 session 对象
QPS 峰值 78 万 ops/秒
正常 P99 2.4 ms
迁移期 P99 1.8 秒
客户端 Lettuce 6.3(Java)+ ioredis 5.3(Node.js)

这套 Redis Cluster 跑了 2 年,单节点 16GB 撑住了 6000 万 key,平时 P99 2.4ms 完美。但用户量翻倍后,内存压力上来,运维决定扩容到 24 节点。在线扩容理论上无感,实测雪崩——这就是问题所在。

事故时间线

时间 事件
D1 02:00 开始 reshard,从 12 节点扩到 24 节点
D1 02:08 P99 从 2.4ms 飙到 600ms,告警触达
D1 02:15 应用层 50% 请求 timeout,业务方电话进来
D1 02:30 暂停 reshard,P99 回到 8ms
D1 03:00 继续 reshard,limit slot 数,P99 320ms
D1 06:00 扩容完成,P99 回到 2.4ms,47 个工单
D2 排查,找到 MOVED redirect 占 30% QPS
D3 找到慢键 MIGRATE 200ms+,改方案重测

第一轮:误以为是网络或节点 CPU

# 1. 查节点 CPU
kubectl top pod -n redis | sort -k2 -nr | head -5
# redis-0  450m  4.5G  (45% CPU)
# redis-1  380m  4.2G  (38% CPU)
# 看起来正常

# 2. 查网络带宽
iftop -i eth0 -P
# 节点间带宽 200Mbps,远远没满

# 3. 查客户端连接数
redis-cli -h redis-0 INFO clients
# connected_clients: 1247  # 正常

# 4. 查慢日志
redis-cli -h redis-0 SLOWLOG GET 20
# 1) (integer) 1234
# 2) (integer) 1706659200
# 3) (integer) 187423  # 187ms!
# 4) 1) "MIGRATE"
#    2) "redis-12"   # 迁移到新节点
#    3) "0"
#    4) "0"
#    5) "5000"
#    6) "KEYS"
#    7) "user:5000:big_session"  <-- 慢键!

SLOWLOG 立刻暴露了 MIGRATE 命令的慢键,这就是 P99 飙升的元凶。但只是冰山一角——MOVED redirect 风暴才是真正的雪崩根源。

第二轮:MOVED redirect 风暴

# Redis Cluster 在 slot migration 期间的行为:
# 1. slot 正在迁移:源节点存在的 key 正常服务,源节点不存在的返回 ASK
# 2. slot 已迁移完成:源节点返回 MOVED,客户端必须更新映射表

# 抓客户端日志,统计 MOVED 出现频率
grep "MOVED" /var/log/app/*.log | wc -l
# 12,847,392 次  // 30% 请求触发了 MOVED

# 客户端 MOVED 处理流程:
# 1. 收到 MOVED,记录新节点
# 2. 发起新连接(或复用)到新节点
# 3. 重发命令
# 4. 收到结果
# 5. 更新本地 slot 映射(Lettuce 异步,ioredis 同步)

# Lettuce 6.3 的 MOVED 处理代码(简化版)
public class ClusterTopologyRefreshHandler {
    @Override
    public void onError(MovedRedirectException ex) {
        // 重定向到正确节点
        Node target = ex.getTarget();
        // 但!触发 topology refresh,异步刷新整个集群拓扑
        topologyRefresh.scheduleRefresh();  // 这里有锁竞争!
    }
}

# 高频 MOVED 触发的问题:
# 1. 每个 MOVED 多 1 个 RTT,延迟翻倍
# 2. Topology refresh 加锁,所有命令排队
# 3. 新连接建立(TCP 三次握手),消耗端口
# 4. 客户端 slot 映射表频繁更新,CPU 飙升

问题本质:三重叠加

反模式 1:大键迁移阻塞主线程

# 发现大键
redis-cli -h redis-0 --bigkeys
# Biggest hash found 'user:5000:big_session' has 8423 fields
# Biggest string found 'cache:product:123' has 12582912 bytes  # 12MB!

# 用 MEMORY USAGE 看精确大小
redis-cli -h redis-0 MEMORY USAGE user:5000:big_session
# (integer) 12582400

# MIGRATE 命令是单线程阻塞的!
redis-cli -h redis-0 MIGRATE redis-12 6379 user:5000:big_session 0 5000 KEYS
# 期间主线程完全阻塞,200ms 内所有请求超时

# Redis 7+ 改进:支持 PIPELINE migration
# 但 redis-cli --cluster reshard 默认不用
redis-cli --cluster reshard redis-0:6379 \
    --cluster-from  \
    --cluster-to  \
    --cluster-slots 100 \
    --cluster-yes \
    --cluster-pipeline 10   # 关键:并发迁移多个 key

# 正解 1:先扫描大键,迁移前拆分
# 反模式:hash 字段太多
HSET user:5000:big_session field1 val1 field2 val2 ... field8423 val8423
# 拆成多个小 hash
HSET user:5000:big_session:part1 ...
HSET user:5000:big_session:part2 ...
# slot 算法用 hashtag 保证落同一槽
HSET {user:5000}:session_part1 ...
HSET {user:5000}:session_part2 ...

# 正解 2:限制 MIGRATE 超时
redis-cli --cluster reshard ... --cluster-timeout 60
# 单 key 迁移超过 60ms 跳过,避免阻塞过久

大键(big key)迁移是 Redis Cluster 扩容最大的隐患。MIGRATE 命令对单个 key 是原子操作,期间主线程完全阻塞。一个 12MB 的 string,序列化 + 网络传输 + 反序列化 ~ 200ms,这 200ms 内整个节点不响应任何请求。我们这次的 47 个工单大部分集中在 4-5 个超大 session 迁移的时刻。

反模式 2:MOVED redirect 风暴

// Lettuce 6.3 默认配置
ClientResources resources = ClientResources.builder()
    .build();
ClusterClientOptions options = ClusterClientOptions.builder()
    .topologyRefreshOptions(
        ClusterTopologyRefreshOptions.builder()
            .enableAllAdaptiveRefreshTriggers()  // MOVED 触发刷新
            .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30))
            .enablePeriodicRefresh(Duration.ofSeconds(60))
            .build())
    .build();

// 问题:MOVED 触发立即刷新,但刷新过程加锁,所有命令等待
// 高频 MOVED → 频繁刷新 → 锁竞争 → 命令堆积

// 正解 1:降低 adaptive refresh 频率
ClusterTopologyRefreshOptions.builder()
    .enableAdaptiveRefreshTrigger(MOVED_REDIRECT, ASK_REDIRECT)
    .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(5))  // 5 秒内只刷新一次
    .refreshTriggersReconnectAttempts(3)  // 至少 3 次 MOVED 才刷新
    .build();

// 正解 2:客户端预先知道扩容,主动刷新
// 扩容开始前,通过 admin API 通知所有客户端:
// "5 分钟后开始 reshard,请提前刷新 topology"
// 客户端收到信号,后台异步刷新,减少 reshard 期间的 MOVED

// 正解 3:用 Redis Cluster Proxy
// 客户端连 proxy,proxy 维护 topology
// 客户端不感知 MOVED,proxy 透明转发
// 适合复杂多语言团队,统一管理

MOVED redirect 在 reshard 期间是不可避免的,但客户端配置不当会放大问题。Lettuce 的 adaptive refresh 默认太激进,小 timeout 导致频繁刷新,反而比"不刷新"还慢。我们后来调整 timeout 到 5 秒,要求 3 次 MOVED 才刷新,MOVED 风暴期间客户端 CPU 从 80% 降到 30%。

反模式 3:客户端 slot 缓存雪崩失效

// ioredis 客户端的 slot 缓存(Node.js)
const redis = new Redis.Cluster([
  { host: 'redis-0', port: 6379 },
  // ...
], {
  slotsRefreshTimeout: 1000,
  slotsRefreshInterval: 5000,
  // 默认每 5 秒刷新一次 slot 映射
});

// 反模式:slotsRefreshInterval 设得太短(高 QPS 服务每 1s 刷)
// 12 节点 × 每秒查 CLUSTER NODES = 12 QPS overhead
// reshard 期间,所有客户端同时收到不同的 MOVED,触发 cascading refresh
// 整个集群的 management 流量飙升

// 正解 1:延长刷新间隔 + 智能触发
new Redis.Cluster([...], {
  slotsRefreshInterval: 30000,  // 30 秒
  enableReadyCheck: true,
  redisOptions: {
    retryStrategy(times) {
      // MOVED 不算 retry,但其他错误指数退避
      return Math.min(times * 100, 2000);
    },
  },
});

// 正解 2:慢启动客户端连接(避免 thundering herd)
const cluster = new Redis.Cluster(nodes, {
  natMap: { /* 同 K8s service 映射 */ },
  scaleReads: 'slave',  // 从节点分担读
  enableOfflineQueue: false,  // 节点不可用立即返回错误,而不是排队
});

// 正解 3:监控 slot refresh 频率
cluster.on('slotsRefreshFailed', (err) => {
  metrics.increment('redis.slots.refresh.failed');
  logger.warn('Slot refresh failed', err);
});
cluster.on('+node', (node) => {
  // 新节点加入,记录事件
  metrics.increment('redis.cluster.node.added');
});

客户端的 slot 缓存策略对 reshard 影响巨大。太激进的刷新会放大 MOVED 风暴,太懒的刷新又会让 MOVED 持续过久。我们后来定的规则是:平时 30s 刷新,reshard 前通知所有客户端临时改为 5s,reshard 完成后恢复 30s。这种"运维 + 客户端协同"的方案才是真正的零感扩容。

修法:三层重构

修法 1:扩容前先扫描和拆分大键

# 完整大键扫描脚本
#!/bin/bash
THRESHOLD=$((1024 * 1024))  # 1MB

for node in $(redis-cli --cluster nodes | grep master | awk '{print $2}'); do
    echo "Scanning $node..."
    redis-cli -h ${node%:*} -p ${node#*:} --scan | while read key; do
        size=$(redis-cli -h ${node%:*} -p ${node#*:} MEMORY USAGE "$key")
        if [ "$size" -gt "$THRESHOLD" ]; then
            echo "BIGKEY: $key size=${size}B node=$node"
        fi
    done
done > bigkeys.log

# 分析结果:
# BIGKEY: user:5000:big_session size=12582400B
# BIGKEY: cache:product:hot:list size=8429122B
# 共 23 个大键 > 1MB

# 拆分策略
# 1. Hash 类型:按字段范围拆
HSCAN user:5000:big_session 0 COUNT 100
# 把 8000 个字段拆成 8 个 hash,每个 1000 字段

# 2. List 类型:按时间拆
# user:5000:history -> user:5000:history:2024 + history:2025

# 3. Set 类型:按取模拆
# friend:5000:all -> friend:5000:shard:0 ~ shard:9

# 拆分后再扩容,迁移就快了

修法 2:reshard 限速 + 监控

# 用 cluster-replicas 0 让 redis-cli 自动处理,但加速度控制
redis-cli --cluster reshard :6379 \
    --cluster-from  \
    --cluster-to  \
    --cluster-slots 50 \   # 一次只迁移 50 个 slot(总 16384)
    --cluster-pipeline 4 \ # 并发 4 个 key
    --cluster-timeout 30000 \  # 30s 超时
    --cluster-yes

# 然后 sleep 60 秒,等业务恢复
sleep 60

# 再迁下 50 个 slot
redis-cli --cluster reshard ... --cluster-slots 50 ...

# 包装成 Ansible playbook
- name: Reshard slots with pacing
  shell: |
    for batch in $(seq 1 ${total_batches}); do
      redis-cli --cluster reshard ${node} \
        --cluster-from ${src} \
        --cluster-to ${dst} \
        --cluster-slots ${batch_size} \
        --cluster-pipeline 4 \
        --cluster-yes
      sleep 60
    done
  vars:
    total_batches: 30
    batch_size: 10

# 监控关键指标
metric: redis_cluster_slots_migrating  # 正在迁移的 slot 数
metric: redis_commands_processed_per_sec_per_node  # 每节点 QPS
metric: redis_p99_latency_per_node  # 每节点 P99
告警:任一节点 P99 > 50ms 立即暂停 reshard

修法 3:客户端配合 reshard 的协议

// 应用层 admin 接口
@RestController
public class ClusterAdminController {
    @PostMapping("/cluster/reshard/pre-notice")
    public void preNotice(@RequestBody ReshardNotice notice) {
        // 收到运维通知,即将开始 reshard
        // 1. 缩短客户端 slot refresh 间隔
        redisCluster.changeRefreshInterval(Duration.ofSeconds(5));
        // 2. 预热新节点连接池(避免 reshard 中临时建连)
        redisCluster.warmupConnections(notice.getNewNodes());
        // 3. 切换降级策略:Redis 失败时回退 DB
        cacheFallback.enable();
    }

    @PostMapping("/cluster/reshard/complete")
    public void complete() {
        redisCluster.changeRefreshInterval(Duration.ofSeconds(30));
        cacheFallback.disable();
    }
}

// 运维侧:reshard 脚本前后调用 admin API
# reshard.sh
for service in order user payment; do
    curl -X POST http://${service}/cluster/reshard/pre-notice \
        -d '{"newNodes": ["redis-12", "redis-13"]}'
done
sleep 30  # 等客户端就绪

# 开始迁移
redis-cli --cluster reshard ...

# 完成后通知
for service in order user payment; do
    curl -X POST http://${service}/cluster/reshard/complete
done

修复前后基准

指标 原始 reshard +大键拆分 +限速 +客户端协同
扩容期 P99 1.8 s 620 ms 180 ms 18 ms
扩容总时间 4 h 3 h 4.5 h 5 h
MOVED 占比 30% 30% 15% 2%
业务工单 47 个 18 个 4 个 0 个
客户端 CPU 80% 65% 40% 22%
失败率 50% 12% 1.2% 0.01%

决策树:Redis Cluster 运维问题排查

我们立的 11 条 Redis Cluster 运维纪律

  1. 禁止单 key > 1MB:扫描 + 监控 + code review 三重防控;
  2. 大键拆分用 hashtag:保证拆分后落同槽;
  3. 扩容前必扫大键:--bigkeys 跑一遍,清理完再扩;
  4. reshard 分批 + 限速:每批 < 100 slot,间隔 60s;
  5. 客户端 slot refresh 调优:平时 30s,reshard 期间临时 5s;
  6. 客户端连接预热:扩容前预建新节点连接;
  7. 降级策略必备:Redis 失败时回退 DB 或本地缓存;
  8. 监控 cluster_slots_migrating:迁移进度可视化;
  9. SLOWLOG 接入告警:任何 > 100ms 的命令立即告警;
  10. 每月做一次 reshard 演练:压测环境演练完整流程;
  11. 多语言客户端统一参数:Java/Node/Python 配置标准化。

引申一:Redis 7.x 的新特性对 Cluster 影响

特性 用途 对 Cluster 影响
Function 替代 Lua 持久化脚本 跨节点更可靠
Sharded Pub/Sub 分片消息 Cluster 内 pub/sub 性能提升
Client side caching 客户端缓存 减少 MOVED 风暴
NOSCRIPT 错误改进 脚本不存在重试 跨节点脚本可靠
ACL v2 权限分组 多租户更安全

Redis 7.x 的 Client Side Caching(CSC)是 Cluster 场景下的重大改进。客户端缓存热数据,服务端通过 Tracking 推送失效通知,大幅减少跨网络请求。我们后来在会话查询场景启用 CSC,QPS 从 78w 降到 23w,Redis 集群压力大幅降低。

引申二:Redis Cluster vs Sentinel vs 单实例

模式 容量 延迟 运维复杂度 适用场景
单实例 单机内存 最低 < 32GB 数据
主从 单机 + 读扩展 读多写少
Sentinel 主从 + 自动故障转移 高可用单点
Cluster 无限横向扩展 中(MOVED 开销) > 100GB 数据
Codis/Twemproxy 横向扩展 中(proxy) 历史方案

选 Redis 部署模式时,数据量是第一决策因素。< 32GB 数据用单实例 + 主从就够,> 100GB 才上 Cluster。中间档位上 Sentinel + 大内存机器更省事。我们这次的 4 亿 key 才上 Cluster,如果是几百万 key 完全没必要折腾。盲目上 Cluster 是常见架构误区,运维成本翻 5 倍。

引申三:大键的多维度治理

# 1. 实时扫描:redis-cli --bigkeys(在线)
redis-cli -h node --bigkeys

# 2. 离线分析:rdb 文件
# 备份 RDB
SAVE
# 用 redis-rdb-tools 分析
rdb -c memory dump.rdb > memory.csv
sort -t',' -k4 -nr memory.csv | head -50
# 输出:database,type,key,size,encoding,...

# 3. 应用层埋点:写入大值告警
public class RedisInterceptor {
    public void beforeSet(String key, String value) {
        if (value.length() > 1024 * 1024) {
            log.warn("Big value SET: key={} size={}", key, value.length());
            metrics.counter("redis.big_value").increment();
            // 严格场景直接拒绝
            // throw new IllegalArgumentException("Value too large");
        }
    }
}

# 4. CI 静态分析:扫代码中的 HSET/RPUSH 大批量
# git pre-commit hook
grep -rn "redis\.lpush\|redis\.hset\|redis\.set" --include="*.java" | \
    awk -F: '$3 ~ /HSET|LPUSH|SET/' | head

大键治理需要"扫描 + 监控 + 代码守卫 + CI 检查"四层防御。单一手段都有漏洞——扫描有滞后,监控有盲区,守卫可绕过,CI 漏代码路径。我们建了套完整治理体系,生产 0 大键持续了 8 个月。

引申四:Redis Cluster 的故障转移机制

# 自动故障转移流程:
# 1. 主节点 N 秒未响应 PING(cluster-node-timeout,默认 15s)
# 2. 多数主节点标记 N 为 PFAIL → FAIL
# 3. N 的从节点发起选举
# 4. 多数主节点投票通过
# 5. 从节点提升为主节点
# 6. 广播新配置,客户端收到 MOVED 更新映射

# 关键参数
cluster-node-timeout 15000   # 节点 PING 超时
cluster-slave-validity-factor 10  # 从节点滞后多久不能升主
cluster-migration-barrier 1   # 主至少留几个从

# 手动故障转移(运维维护时)
redis-cli -h slave-node CLUSTER FAILOVER
# 选项:FORCE(不等主节点同意)
#       TAKEOVER(强制接管,危险)

# 监控故障转移
metric: cluster_state  # ok / fail
metric: cluster_slots_pfail  # 处于 PFAIL 状态的 slot 数
metric: cluster_known_nodes  # 已知节点数

Cluster 的自动故障转移机制成熟,但参数需要根据业务调。cluster-node-timeout 太短(< 5s)易误判,太长(> 30s)恢复慢。我们这次 reshard 期间一度触发了误判,因为节点忙不过来 PING 响应慢,差点把正常节点踢出集群。后来调到 20s + 提高 cluster-replica-validity-factor 才稳定。

引申五:Redis Cluster 的网络模型

# Cluster Bus 端口 = data 端口 + 10000
# data: 6379, bus: 16379

# Bus 协议:
# 1. PING/PONG 心跳(秒级)
# 2. GOSSIP 节点信息(每秒一次)
# 3. UPDATE 配置变更
# 4. FAIL 节点失败通告
# 5. MEET 新节点加入

# K8s 部署要保证 bus 端口连通
apiVersion: v1
kind: Service
metadata:
  name: redis-cluster
spec:
  clusterIP: None  # Headless
  ports:
  - name: client
    port: 6379
  - name: gossip
    port: 16379  # 必须暴露!
  selector:
    app: redis-cluster

# 否则 bus 不通,集群无法自检
# 我们之前踩过坑:NetworkPolicy 漏配 16379,集群 30 秒就分裂

K8s 下 Redis Cluster 必须确保 bus 端口连通,这是常见配置错误。cluster bus 是 Cluster 的"神经系统",bus 不通比 data 不通更严重——会导致集群分裂、故障转移失败、slot 映射混乱。我们专门加了 K8s NetworkPolicy 的检查脚本,部署前自动验证 16379 端口可达。

引申六:Redis Cluster 在云厂商上的差异

产品 Cluster 模式 扩缩容
AWS ElastiCache for Redis Cluster 原生 Cluster 分钟级在线
阿里云 云数据库 Redis 集群版 Proxy 模式 / 直连 分钟级
腾讯云 云数据库 Redis 集群版 Proxy 模式 分钟级
Azure Azure Cache for Redis Enterprise 原生 需重启
自建 K8s helm/operator 原生 手动

云厂商的 Redis Cluster 各有差异。阿里云/腾讯云的"代理模式"对客户端透明,但单 proxy 是性能瓶颈,百万 QPS 场景不适用。AWS ElastiCache 是原生 Cluster,客户端需要支持。我们这套自建在 K8s 上,扩缩容运维成本高,但成本只有云产品的 30%。

引申七:Redis 持久化对 reshard 的影响

# 持久化模式
# RDB:每 N 分钟全量快照
save 900 1
save 300 10
# 优点:快速恢复
# 缺点:fork 时短暂阻塞,大内存 fork 会卡

# AOF:每个命令追加日志
appendonly yes
appendfsync everysec  # 每秒 fsync
# 优点:数据不丢
# 缺点:文件大,重写期间 IO 压力

# Mixed:RDB + AOF
# 默认推荐

# Reshard 期间的影响
# MIGRATE 命令在 AOF 模式下会:
# 1. 源节点:把 key 序列化 + 删除,记录到 AOF
# 2. 目标节点:接收 + 反序列化 + 插入,记录到 AOF
# 双倍 AOF 写入,IO 压力翻倍

# 优化:reshard 期间临时降低 fsync 频率
CONFIG SET appendfsync no  # 让 OS 调度 fsync
# 完成后恢复
CONFIG SET appendfsync everysec

持久化策略和 reshard 互相影响。AOF everysec 模式下,reshard 会让 IO 压力翻倍,慢盘上可能引起新的延迟。我们扩容时临时改 appendfsync 为 no,reshard 完再恢复,P99 又降了一截。但要注意:no 期间断电会丢数据,所以仅限维护窗口。

引申八:Redis Cluster 监控的核心指标

  1. cluster_state:ok / fail,集群是否健康;
  2. cluster_known_nodes:已知节点数,变化说明拓扑变更;
  3. cluster_slots_assigned:已分配 slot 数,应 = 16384;
  4. cluster_slots_ok:正常 slot 数;
  5. cluster_slots_pfail / fail:异常 slot 数;
  6. P99 latency:redis-cli --latency 持续采样;
  7. used_memory_human / maxmemory:内存压力;
  8. connected_clients:客户端连接数;
  9. commands_processed_per_sec:QPS;
  10. keyspace_hits / misses:命中率;
  11. slowlog count:慢日志数量;
  12. cluster_links_total:节点间连接数。

这套监控指标我们贴在每个 Redis Cluster 的 Grafana 面板上。监控是运维的眼睛,没有监控的扩容就是闭眼开车。我们这次事故初期能快速定位,就是因为这些指标都齐全,3 分钟就锁定问题节点。

引申九:Redis Cluster 数据一致性的陷阱

# Cluster 是 AP 系统,不是 CP
# 主从异步复制,主挂前已写入但未同步的数据会丢

# 强一致写入(性能差,慎用)
WAIT numreplicas timeout
# 比如 WAIT 2 1000 - 等待至少 2 个从节点确认,最多 1 秒
# 如果 1 秒内没达成,返回实际复制的副本数

# 跨 slot 事务不可用
MULTI
SET key1 v1  # slot A
SET key2 v2  # slot B
EXEC
# 报错:CROSSSLOT Keys in request don't hash to the same slot

# 解决:用 hashtag 强制同 slot
SET {user:1}:name "Alice"
SET {user:1}:age 25
# 都落在 user:1 的 slot

# Lua 脚本也要同 slot
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 user:1:name Alice
# KEYS 必须是相同 slot

Redis Cluster 牺牲了一致性换性能,但很多团队不知道。跨 slot 操作不支持事务、不支持 Lua、不支持 multi-key 命令,业务设计时必须用 hashtag 规划好 key 分布。我们重构会话系统时,所有同用户的 key 都加 {user_id} hashtag,避免跨 slot 问题。

引申十:Redis Cluster 的备份与恢复

# 全集群备份
for node in $(redis-cli --cluster nodes | grep master | awk '{print $2}'); do
    host=${node%:*}
    port=${node#*:}
    redis-cli -h $host -p $port BGSAVE
    sleep 5
    scp $host:/data/dump.rdb /backup/dump-${host}-$(date +%Y%m%d).rdb
done

# 全集群恢复(从 RDB)
# 1. 启动空集群
# 2. 每个节点导入对应 RDB
redis-cli -h node1 -p 6379 DEBUG RELOAD
# 注意:Cluster 模式下不能直接覆盖 dump.rdb 文件,需要停集群再恢复

# 增量备份:用 redis-shake 或 Redis Replica
redis-shake --type=sync --source.addr=src:6379 --target.addr=backup:6379
# 实时同步到备用 Cluster

# 跨地域容灾
# 主集群 → AOF → 备份集群(异地)
# 主挂时切换 DNS 到备份集群

Cluster 备份比单实例复杂得多,要保证一致性需要全节点同步 BGSAVE。跨地域容灾推荐用 redis-shake 实时同步,而不是定时全量备份。我们一个金融业务就这么做,RPO < 1 秒,RTO < 30 秒。这套方案救过我们一次 AZ 级故障,值得每个团队提前准备。

引申十一:Redis Sentinel 与 Cluster 的混合部署陷阱

# 错误尝试:Sentinel 监控 Cluster 节点
# Sentinel 配置
sentinel monitor cluster-node-1 10.0.0.1 6379 2
sentinel monitor cluster-node-2 10.0.0.2 6379 2
# 问题:Sentinel 不感知 Cluster 拓扑,主从切换会和 Cluster 自身的 failover 冲突
# 结果:双主出现 split-brain,数据脑裂

# 正确做法:Cluster 模式下不要用 Sentinel
# Cluster 自身有 gossip 协议 + cluster-node-timeout 实现 failover
# Sentinel 只用于 Master-Slave 主从架构

# 如果坚持要"双保险"
# 用应用层健康检查 + 自动切换,而不是 Sentinel
healthcheck:
  cmd: redis-cli -h $host -p $port CLUSTER INFO | grep cluster_state:ok
  interval: 5s
  timeout: 2s

Sentinel 和 Cluster 混合部署是新手最容易踩的坑。Cluster 已经内置了 failover 机制,再叠加 Sentinel 反而会导致脑裂。我见过一个团队把 Sentinel 部在 Cluster 上"做多重保险",结果 reshard 期间 Sentinel 误判触发切换,Cluster 自身又切了一次,4 个主节点同时存在,数据完全紊乱,最后只能停服恢复 RDB。架构师必须分清:Sentinel 是单实例高可用,Cluster 是分片+高可用,两者互斥不可叠加。

引申十二:Redis 8.0 RESP3 协议改进对 Cluster 的影响

# RESP2(默认)的局限
# 1. 类型有限:String / Array / Integer / Bulk String / Error
# 2. 不支持 Map / Set 等结构化返回
# 3. push 消息(Pub/Sub)和命令响应混合,客户端难处理

# RESP3 改进
HELLO 3
# 协议升级到 RESP3

# 新类型
# Map:HGETALL 直接返回 Map,客户端不用自己拼
# Set:SMEMBERS 直接返回 Set
# Double:浮点数精确表达
# Big Number:支持大整数

# Cluster 模式下的好处
# CLUSTER SHARDS 返回结构化 Map,易解析
# CLIENT TRACKING(客户端缓存)依赖 RESP3 的 push 通道
# Lettuce 6.x、ioredis 5.x 都已支持 RESP3

Redis 8.0 在 RESP3 协议上做了大量优化。对 Cluster 模式最大的收益是 CLIENT TRACKING(服务端推送缓存失效),原来轮询查 key 失效,现在服务端主动 push。我们一个商品详情场景上了 RESP3 + Client Side Caching,缓存命中率从 65% 升到 92%,Redis QPS 直接降了 40%。但要注意:RESP3 是协议级变更,老版本客户端会报错,升级前必须全量灰度测试。

引申十三:Redis Cluster 与 Codis、Twemproxy 的对比选型

方案 架构 优势 劣势
Redis Cluster 去中心化 + gossip 原生支持、运维简单 客户端要支持 Cluster 协议
Codis 代理层 + ZK 元数据 对客户端透明、扩缩容平滑 多一层代理、官方不再维护
Twemproxy 代理层 + 一致性哈希 极轻量、性能高 不支持在线扩容、不支持事务
云 Proxy 模式 云厂商代理 客户端零改动 多一跳、单点瓶颈、贵

新业务选型时,百万 QPS 以下且团队能力一般的,选 Codis 或云 Proxy 模式;百万 QPS 以上、追求性能极致、团队有 Redis 专家的,选原生 Redis Cluster。我们这套用户会话存储就是因为 QPS 78w 才被迫上原生 Cluster。Twemproxy 适合极简场景,但不支持在线扩容意味着每次扩容都要停服,生产环境基本不可接受。技术选型从来不是非此即彼,而是按业务规模和团队能力匹配。

引申十四:Redis Cluster reshard 期间的可观测性建设

# 自研 reshard 巡检脚本
#!/bin/bash
while true; do
    # 1. 当前迁移中的 slot 数
    migrating=$(redis-cli --cluster check $node | grep -c "migrating")
    # 2. P99 延迟
    p99=$(redis-cli --latency-history -i 1 | awk '{print $4}' | tail -1)
    # 3. MOVED redirect 占比
    moved=$(redis-cli INFO commandstats | grep moved | awk -F'=' '{print $2}')
    # 4. 各客户端 QPS
    clients=$(redis-cli INFO clients | grep connected_clients)

    echo "$(date) migrating=$migrating p99=$p99 moved=$moved $clients"

    # 自动暂停条件
    if [ "$p99" -gt 100 ]; then
        echo "P99 too high, pausing reshard"
        # kill 当前 reshard 进程
        pkill -f "redis-cli --cluster reshard"
        # 通知运维
        curl -X POST $alert_webhook -d "reshard paused"
        exit 1
    fi

    sleep 5
done

reshard 不是"开始-等待-结束"的黑盒,而是要全程有观测、可中断的工程动作。自研巡检脚本 + 自动暂停机制 + 告警 webhook,是 Cluster 运维的标配。这次事故后我们落地了这套脚本,每次 reshard 前先跑一遍预演,实际执行时全程监控,P99 异常自动暂停。运维心态从"紧张盯盘 4 小时"变成"喝杯咖啡看脚本自动跑",这才是工程化的真正价值。

引申十五:Redis Cluster 在 Service Mesh 下的部署考虑

当 Redis Cluster 部署在 K8s + Istio 等 Service Mesh 环境下,会遇到一系列新的兼容性问题。Istio Sidecar 默认会拦截所有出站流量并做 mTLS 加密,但 Redis Cluster 节点间的 gossip 协议通过 16379 端口直接通信,如果被 Sidecar 拦截会导致 gossip 失败、Cluster 拓扑变更感知延迟数秒。我们一开始按通用规范上 Istio,reshard 期间 cluster_state 频繁 fail,排查 2 天才发现是 mTLS 把 gossip 包加密后,对端 Redis 解析不了。

解决方案有三种:第一,把 Redis 部署放在 ServiceMesh 之外的 Namespace,通过 Service Entry 暴露;第二,配置 Sidecar 排除 6379 和 16379 端口,让 Redis 流量直通;第三,关闭 mTLS,只用 plaintext。我们选了第二种,既保留 Service Mesh 的可观测性,又不影响 Redis 性能。这个坑大多数 K8s 团队都会踩一次,建议在选型阶段就明确网络方案。

引申十六:Redis Cluster 升级路径与版本兼容性

# Redis Cluster 滚动升级流程
# 1. 备份当前 Cluster 配置
redis-cli --cluster check $node > cluster-state-before.txt

# 2. 升级从节点(一个一个来)
for slave in $(redis-cli --cluster nodes | grep slave | awk '{print $2}'); do
    # 停止从节点
    kubectl delete pod redis-slave-$idx
    # K8s 自动用新版本镜像重启
    # 等待加入 Cluster
    until redis-cli -h $slave CLUSTER INFO | grep cluster_state:ok; do
        sleep 2
    done
done

# 3. 主从切换,把所有主切到已升级的从
for master in $(redis-cli --cluster nodes | grep master | awk '{print $2}'); do
    redis-cli -h $master CLUSTER FAILOVER
    sleep 5
done

# 4. 升级旧主(现在是从)
# 重复步骤 2

# 5. 验证 Cluster 拓扑
redis-cli --cluster check $node
# 对比 before / after,数据无丢失

Redis Cluster 跨大版本升级(如 6.x → 7.x)需要严格按"从→主→主从切换"的顺序滚动,绝不能停服全量替换。升级期间最容易踩的坑是版本不一致导致 CLUSTER MEET 失败,新加节点报 invalid cluster bus packet。我们的纪律是:升级前在测试集群跑 7 天,验证客户端兼容性,然后生产环境分 3 批每批间隔 24 小时滚动。这套 SOP 已经平稳跑过 4 次大版本升级。

总结

这次 Redis Cluster 扩容雪崩事故,本质是"大键 MIGRATE 阻塞 + MOVED redirect 风暴 + 客户端 slot 缓存失效"三重反模式叠加。每个反模式单独存在都能跑,组合在 4 亿 key + 12 节点扩到 24 节点的真实场景下就是灾难。修复路径"大键扫描拆分 + reshard 限速分批 + 客户端 admin 协议"三步走,把扩容期 P99 从 1.8s 压回 18ms,业务工单从 47 个降到 0,扩容真正做到"在线无感"。

更重要的认知是:Redis Cluster 的"在线扩容"是个营销话术,真实场景下需要大量工程化配套。这套配套包括:大键治理、客户端协议、监控告警、降级策略。每一项都不是 Redis 文档里能找到的,而是用一次次事故换来的实战经验。希望这篇复盘能让所有上 Redis Cluster 的团队少走弯路,把扩缩容从"运维半夜祈祷"变成"白天有信心做"的常规操作,这才是基础设施工程化的真正价值,也是分布式系统工程师必修的硬功夫。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Java 17 G1GC humongous allocation 在 48GB 大堆下引发 P99 飙到 8 秒的 9 天复盘:JSON 大对象 + 定时全量预加载三重叠加 + 12 条 GC 工程纪律

2026-5-27 0:45:43

技术教程

全球 SaaS 网关 TLS 1.3 握手 P99 从 65ms 飙到 820ms 的 4 天复盘:OCSP stapling 失效 + session ticket 跨集群不共享 + 0-RTT 配置不当三重叠加 + 11 条 TLS 工程纪律

2026-5-27 1:01:39

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索