Redis Cluster 脑裂,缓存数据为何对不上?

问题背景

公司使用 Redis Cluster 6.2 作为分布式缓存,6 节点(3 主 + 3 从),部署在 3 台物理服务器上(每台 1 主 1 从),采用默认的 Gossip 协议进行节点间通信和故障检测。Cluster 承载了商品详情缓存、用户 Session 和实时价格缓存,日均请求量约 5 万 QPS。

故障现象

10 月 19 日下午 3:00,部署了 Redis 相关的 Kubernetes 网络策略变更后,业务逐渐出现异常:商品详情页展示的价格与实际价格不一致,部分用户登录状态丢失需要重新登录。

1
2
3
4
5
6
7
$ redis-cli -c -h node01 -p 6379 CLUSTER INFO
cluster_state:fail
cluster_slots_assigned:16384
cluster_slots_ok:8192
cluster_slots_fail:8192       # 8192个槽位标记为失败
cluster_known_nodes:4         # 原本6个节点,只看到4个
cluster_size:2                # 只看到2个主节点

集群状态 fail,一半的 hash slots 不可用。

1
2
3
4
5
6
7
$ redis-cli -c -h node04 -p 6379 CLUSTER INFO
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_fail:0
cluster_known_nodes:4         # 也只看到4个节点
cluster_size:3                # 看到3个主节点

两个不同节点看到完全不同的集群视图——node01 看见 2 主,认为集群失败;node04 看见 3 主,认为集群正常。这就是典型的脑裂(Split-Brain)

排查过程

第一步:确认 Gossip 通信状态

1
2
3
4
5
$ redis-cli -h node01 -p 16379 CLUSTER NODES
a1b2c3... 10.0.1.1:6379@16379 myself,master - 0 1697700123456 9 connected 0-5460
d4e5f6... 10.0.2.1:6379@16379 master - 0 1697700123458 12 connected 5461-10922
g7h8i9... 10.0.2.2:6379@16379 slave - 0 1697700123460 14 connected
j0k1l2... 10.0.3.1:6379@16379 master,fail? - 1697700100000 1697700080000 8 disconnected 10923-16383

节点 j0k1l2(node03 的主节点)被标记为 fail?(疑似下线)。但从另一个子集群看,node03 正常运行且已被提升为独立的主节点。

第二步:分析故障触发时间线

1
2
3
4
5
6
15:02:30  K8s网络策略变更,node01/02和node03之间的Gossip端口被短暂阻断
15:02:35  node01/02判定node03 PFAIL(疑似下线),广播PFAIL消息
15:02:40  node01/02所有节点收到PFAIL→确认FAIL→触发failover
15:02:42  node01/02子集群的从节点提升为新的主节点,接管原node03的slots
15:02:45  网络恢复
15:02:46  node03发现自己被FAIL→重新选举自己为主节点→两个主节点同时持有相同slots

网络仅中断了约 10 秒(15:02:30–15:02:40),但 Redis Cluster 的 cluster-node-timeout 默认值为 15000ms(15 秒)——网络中断时间 < cluster-node-timeout,理论上不应该触发 failover

第三步:发现配置不一致

1
2
3
4
5
$ redis-cli -h node01 -p 6379 CONFIG GET cluster-node-timeout
"15000"

$ redis-cli -h node03 -p 6379 CONFIG GET cluster-node-timeout
"5000"         # node03上只有5秒!

node03 的 cluster-node-timeout 被手动改成了 5000ms(5 秒)。这 5 秒刚好落在网络中断的 10 秒窗口内——于是 node01 和 node02 在 5 秒后判定 node03 下线,触发 failover。而 node03 同样在 5 秒后判定 node01/02 下线,也自己触发了一轮选举。

第四步:为什么 node03 的 timeout 是 5 秒?

追溯配置历史发现:两周前一名开发在做本地 Redis 测试时,通过 CONFIG SET 临时改了 node03 的 timeout 方便测试自动故障转移性能,但测试结束后忘记改回来。这个"临时"配置被 Redis 持久化到了 redis.conf 中。

解决方案

紧急恢复

1
2
3
4
5
6
7
# 1. 统一 cluster-node-timeout
redis-cli -h node03 -p 6379 CONFIG SET cluster-node-timeout 15000
redis-cli -h node03 -p 6379 CONFIG REWRITE

# 2. 强制集群重置
redis-cli --cluster fix 10.0.1.1:6379
# 让修复工具重新协商主从关系和slot分配

清理不一致数据

脑裂期间两个子集群都接收了写入,需要找出冲突的 key 并以主集群数据为准做覆盖。

根因分析

直接原因:两个节点 cluster-node-timeout 配置不一致(5s vs 15s),网络瞬时中断的 10 秒刚好落在 5 秒的故障检测窗口内却还没到 15 秒,造成两边互相认为对方下线,触发双向 failover 形成脑裂。

深层原因:

  • CONFIG SET 是实时生效但不区分"临时"和"永久"——通过 CONFIG REWRITE 会持久化
  • 缺乏 Redis 配置的变更审计和一致性检查机制
  • cluster-node-timeout 这个关键参数没有统一管理

预防措施

  1. 配置统一管理:所有 Redis 节点的关键参数通过 Ansible/Puppet 统一推送,禁止手动配置修改
  2. 配置一致性检查:定期巡检所有 Redis 节点的 cluster-node-timeoutcluster-slave-validity-factor 等关键参数
  3. 最小 timeout 经验值cluster-node-timeout 建议 ≥ 网络最大 RTT × 3,生产环境推荐 15-30 秒
  4. 脑裂检测脚本:定期从不同节点执行 CLUSTER NODES 对比集群视图,不一致立即告警
  5. 网络变更窗口:任何可能影响 Redis 节点间通信的网络变更,必须在 Redis 低峰窗口执行

总结

Redis Cluster 的脑裂在理论上很少发生——前提是所有节点的关键配置一致。一个被人随手改了 5 秒 timeout 的节点,在网络抖动的瞬间就成了集群分裂的导火索。“临时"配置不应该出现在生产环境,如果一定需要测试,请在独立的 dev 集群中做。对 Redis Cluster 来说,一致性不是可选项,是存活前提。

使用 Hugo 构建
主题 StackJimmy 设计