Redis集群切换主节点时,服务竟然全员掉线

21 阅读1分钟

上周四凌晨,我们的订单系统在Redis集群主节点自动切换时,所有服务节点同时抛出MOVED异常,整个系统瘫痪了8分钟。你可能觉得奇怪:Redis Cluster不是号称高可用吗?为什么一个主节点故障会导致全员掉线?今天咱们就掰开这事的骨头缝,看看里面藏了多少暗坑。

现象:你以为的高可用,其实有触发条件

当时的情况是这样的:一个承载了12万QPS的Redis Cluster(6节点,3主3从),其中一个主节点因宿主机网络抖动触发故障转移。按照设计,从节点应该无缝升级为主节点,但诡异的是,所有客户端同时报错:

// 错误堆栈
redis.clients.jedis.exceptions.JedisMovedDataException: MOVED 1234 10.0.0.2:6379

更离谱的是,即使新主节点已经选举成功,客户端仍然持续报错直到重启。这直接违背了Redis Cluster的设计原则——理论上,客户端收到MOVED后应该自动更新路由表,继续正常工作才对。

根因:客户端缓存与集群状态的不同步

问题出在双重缓存机制上。以Jedis为例,它的ClusterSlotCache维护了两个关键状态:

  1. 服务端返回的MOVED响应中的新节点地址
  2. 本地缓存的槽位映射表
  • 致命点在于*:当集群发生主从切换时,如果客户端连接池中仍有活跃连接指向旧主节点,这些连接会持续收到MOVED响应。但由于某些实现缺陷(后面会具体解释),客户端并没有立即销毁这些连接,而是不断用错误的连接重试,形成死循环。

看看问题代码的简化版:

// 错误示例:典型的重试逻辑
try {
    return jedis.get(key); // 第一次请求
} catch (JedisMovedDataException e) {
    redis.renewSlotCache(); // 更新槽位映射
    return jedis.get(key); // 第二次请求:可能还在用旧连接!
}

深度拆解:连接池的"僵尸连接"问题

真正的魔鬼在细节里。通过抓包和线程堆栈分析,我们发现根本原因是:

  1. Jedis的连接池(GenericObjectPool)在归还连接时,不会主动校验连接是否指向失效节点
  2. 故障转移期间,连接池中的部分连接可能已经指向了旧主节点
  3. 这些"僵尸连接"被重复借用,导致MOVED异常持续触发

正确的做法应该是这样:

// 修复方案:强制验证连接有效性
try {
    Jedis jedis = pool.getResource();
    try {
        if (!jedis.ping().equals("PONG")) { // 关键检查
            jedis.close();
            throw new JedisException("Connection dead");
        }
        return jedis.get(key);
    } finally {
        pool.returnResource(jedis);
    }
} catch (JedisMovedDataException e) {
    pool.clear(); // 清空整个连接池
    renewSlotCache();
    return get(key); // 重新建立连接
}
  • 耗时对比*:
  • 原方案:错误持续8分钟(直到运维手动重启)
  • 修复后:平均恢复时间1.2秒(实测100次故障模拟)

避坑指南:Redis Cluster切换必知的三个陷阱

  1. 客户端版本锁定
    不同版本的Redis客户端对MOVED处理天差地别。比如Jedis 2.x会静默吞掉某些异常,而3.x会抛出JedisRedirectException。一定要全集群统一客户端版本。

  2. 连接池参数暗坑

    # 生产环境必须配置的参数
    redis.timeout=2000 # 必须小于集群node-timeout
    redis.maxAttempts=3 # 防止无限重试
    redis.testOnBorrow=true # 借连接时校验
    
  3. 集群配置的致命参数
    Redis服务端的cluster-node-timeout必须大于网络超时时间,否则会误判节点死亡。我们吃过亏的配置:

    # 错误配置(单位毫秒)
    cluster-node-timeout 5000
    # 正确配置
    cluster-node-timeout 15000
    

终极建议:像对待数据库事务一样对待集群切换

经过这次血泪教训,我们现在对Redis Cluster的运维原则是:任何主节点切换都当作数据库迁移来对待。具体包括:

  • 提前在预发布环境模拟各种故障场景
  • 切换期间主动降级非核心功能
  • 客户端必须实现熔断机制(如Hystrix或Resilience4j)

想知道我们是怎么用Go的redis-go-cluster客户端复现同样问题的?或者你在Kubernetes环境下遇到过更诡异的故障转移场景?欢迎在评论区分享你的实战经历——搞不好咱们能凑出一本《Redis集群避坑大全》。