上周四凌晨,我们的订单系统在Redis集群主节点自动切换时,所有服务节点同时抛出MOVED异常,整个系统瘫痪了8分钟。你可能觉得奇怪:Redis Cluster不是号称高可用吗?为什么一个主节点故障会导致全员掉线?今天咱们就掰开这事的骨头缝,看看里面藏了多少暗坑。
现象:你以为的高可用,其实有触发条件
当时的情况是这样的:一个承载了12万QPS的Redis Cluster(6节点,3主3从),其中一个主节点因宿主机网络抖动触发故障转移。按照设计,从节点应该无缝升级为主节点,但诡异的是,所有客户端同时报错:
java
// 错误堆栈
redis.clients.jedis.exceptions.JedisMovedDataException: MOVED 1234 10.0.0.2:6379
更离谱的是,即使新主节点已经选举成功,客户端仍然持续报错直到重启。这直接违背了Redis Cluster的设计原则------理论上,客户端收到MOVED后应该自动更新路由表,继续正常工作才对。
根因:客户端缓存与集群状态的不同步
问题出在双重缓存 机制上。以Jedis为例,它的ClusterSlotCache维护了两个关键状态:
- 服务端返回的
MOVED响应中的新节点地址 - 本地缓存的槽位映射表
- 致命点在于 *:当集群发生主从切换时,如果客户端连接池中仍有活跃连接指向旧主节点,这些连接会持续收到
MOVED响应。但由于某些实现缺陷(后面会具体解释),客户端并没有立即销毁这些连接,而是不断用错误的连接重试,形成死循环。
看看问题代码的简化版:
java
// 错误示例:典型的重试逻辑
try {
return jedis.get(key); // 第一次请求
} catch (JedisMovedDataException e) {
redis.renewSlotCache(); // 更新槽位映射
return jedis.get(key); // 第二次请求:可能还在用旧连接!
}
深度拆解:连接池的"僵尸连接"问题
真正的魔鬼在细节里。通过抓包和线程堆栈分析,我们发现根本原因是:
- Jedis的连接池(GenericObjectPool)在归还连接时,不会主动校验连接是否指向失效节点
- 故障转移期间,连接池中的部分连接可能已经指向了旧主节点
- 这些"僵尸连接"被重复借用,导致
MOVED异常持续触发
正确的做法应该是这样:
java
// 修复方案:强制验证连接有效性
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切换必知的三个陷阱
-
客户端版本锁定
不同版本的Redis客户端对
MOVED处理天差地别。比如Jedis 2.x会静默吞掉某些异常,而3.x会抛出JedisRedirectException。一定要全集群统一客户端版本。 -
连接池参数暗坑
properties# 生产环境必须配置的参数 redis.timeout=2000 # 必须小于集群node-timeout redis.maxAttempts=3 # 防止无限重试 redis.testOnBorrow=true # 借连接时校验 -
集群配置的致命参数
Redis服务端的
cluster-node-timeout必须大于网络超时时间,否则会误判节点死亡。我们吃过亏的配置:redis# 错误配置(单位毫秒) cluster-node-timeout 5000 # 正确配置 cluster-node-timeout 15000
终极建议:像对待数据库事务一样对待集群切换
经过这次血泪教训,我们现在对Redis Cluster的运维原则是:任何主节点切换都当作数据库迁移来对待。具体包括:
- 提前在预发布环境模拟各种故障场景
- 切换期间主动降级非核心功能
- 客户端必须实现熔断机制(如Hystrix或Resilience4j)
想知道我们是怎么用Go的redis-go-cluster客户端复现同样问题的?或者你在Kubernetes环境下遇到过更诡异的故障转移场景?欢迎在评论区分享你的实战经历------搞不好咱们能凑出一本《Redis集群避坑大全》。