Redis 集群故障排查与修复实战:从 Connection refused 到内存打满的七个现场
上个月我们生产环境的认证服务 cluster-app 在凌晨两点突然大面积报 Redis 连接错误。告警把我吵醒时,我第一反应是网络抖动,毕竟那段时间机房刚做过割接。但翻了眼监控就排除了------报错是 Connection refused,不是 timeout,说明 TCP 握手阶段就被拒,不是链路慢。
报错的 IP 是 10.20.13.14:6379,可我翻遍 docker-compose 和 application-cluster.yaml,种子节点只写了 10.20.13.11 和 10.20.13.110。这个 10.20.13.14 压根没在任何配置里。我当时愣了一下,以为配置被谁改过,git blame 了一番,没有。
这就引出了 Redis 集群最容易让人踩坑的地方:客户端连的不是你配的地址,而是集群告诉它的地址。Lettuce 拿到种子节点后,会向集群要一份完整拓扑,然后挨个去连。如果拓扑里躺着一台早已下线的机器,客户端就会执着地去连它,然后 Connection refused。改配置没用,得在集群侧动手术。
下面是我这半年处理过的七类现场,按"现场现象 → 可能原因 → 怎么验证 → 怎么修 → 怎么防"的顺序记下来,命令都能直接抄。
一、Connection refused:死节点藏在集群拓扑里
现场现象
cluster-app 日志密集出现:
io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: /10.20.13.14:6379
Caused by: java.net.ConnectException: Connection refused
应用没改过代码,突然开始连一个陌生 IP。
可能原因
集群元数据里有一台 fail 状态的 master 节点,客户端拓扑刷新后仍然尝试连接它。
怎么验证
先确认集群状态,找问题节点:
bash
redis-cli -h 10.20.13.11 -p 6379 cluster info | grep -E "cluster_state|cluster_slots"
redis-cli -h 10.20.13.11 -p 6379 cluster nodes
CLUSTER NODES 输出里我看到了:
98117d... 10.20.13.14:6379@16379 master,fail - 1791599045285 ... disconnected 0-5460
96d28c... 10.20.13.11:6379@16379 myself,master - 0 0 2 connected 5461-10922
e07a9e... 10.20.13.110:6379@16379 master - 0 ... 3 connected 10923-16383
10.20.13.14 是 master,持有 0-5460 共 5461 个 slot,状态 fail 且 disconnected。cluster_state 必然是 fail,因为这部分 slot 没主。
怎么修
这台节点已经死了,源节点在线才能用 reshard 迁 key,现在只能直接改 slot 归属。我把 0-5460 指派给活着的 10.20.13.11:
bash
DEAD=98117d542f5c185a25ce55cf139022e59206f928
TARGET=96d28ca26d0e0e37511fc4af990d72670d664b1d
for slot in $(seq 0 5460); do
redis-cli -h 10.20.13.11 -p 6379 cluster setslot $slot node $TARGET
done
这一步要清醒:源节点已死,slot 里的 key 迁不过来,会丢。但节点都死了,数据本来也取不到,别纠结。
slot 全部重新指派后,从每个 存活节点上 forget 死节点。漏掉任何一个,Gossip 会把它重新广播回来:
bash
for node in 10.20.13.11 10.20.13.110; do
redis-cli -h $node -p 6379 cluster forget $DEAD
done
验证与预防
bash
redis-cli -h 10.20.13.11 -p 6379 cluster info | grep cluster_state # 期望 ok
redis-cli -h 10.20.13.11 -p 6379 cluster nodes # 不再有 10.20.13.14
最后重启 cluster-app 容器,tail 日志确认不再有 Connection refused。
那次之后我在每个节点的 redis.conf 补了:
conf
cluster-announce-ip 10.20.13.11
cluster-announce-port 6379
cluster-announce-bus-port 16379
Docker 和 NAT 环境里尤其要配,不然集群广播的是容器内部 IP,客户端又连不上。
二、槽位迁移卡死:reshard 跑到一半不动了
现场现象
一次扩容,我对 10.20.13.11 执行 reshard 迁 2000 个 slot 到新节点,命令行卡在 Moving slot 长时间没动静,新节点上 CLUSTER NODES 显示这些 slot 处于 migrating 状态。
可能原因
迁移过程中源节点上那个 slot 里还有大 key 没搬完,或者目标节点断连过一次导致状态不一致;也可能是我之前手动中断过迁移。
怎么验证
看 slot 的实际状态:
bash
redis-cli -h 10.20.13.11 -p 6379 cluster slots | grep -A3 "migrating\|importing"
redis-cli -h 10.20.13.11 -p 6379 cluster nodes | grep -E "migrating|importing|fail"
如果源节点显示 slot 是 migrating、目标节点显示 importing,但两边都没推进,说明迁移卡死。
怎么修
能连上两个节点时,让目标节点先取消 import,再让源节点取消 migrate,回退重来:
bash
redis-cli -h <目标节点> -p 6379 cluster setslot <slot> stable
redis-cli -h <源节点> -p 6379 cluster setslot <slot> stable
源节点已经失联、没法正常回退时,直接在所有存活节点上把 slot 指派给一个明确的 owner,强制结束中间态:
bash
redis-cli -h 10.20.13.11 -p 6379 cluster setslot <slot> node <存活节点ID>
也可以用官方兜底脚本:
bash
redis-cli --cluster fix 10.20.13.11:6379
它会扫描并修复处于中间态的 slot。但 fix 不会替你决定 slot 归谁,migrating/importing 卡死时还是先手动 stable 或 setslot node 更可控。
验证与预防
bash
redis-cli -h 10.20.13.11 -p 6379 cluster info | grep cluster_slots_ok # 期望 16384
迁移前先用 redis-cli --bigkeys 摸一遍大 key,超大的 hash/set 单独处理。别在业务高峰做 reshard,迁移本身会占用带宽和 CPU。
三、CLUSTERDOWN:集群整体拒绝服务
现场现象
cluster-app 报错从 Connection refused 变成:
CLUSTERDOWN The cluster is down
任何读写都返回这个错误,连 redis-cli 直接连某个节点 set 也失败。
可能原因
slot 没有完全覆盖(cluster-require-full-coverage 开着的默认行为),或者同时挂了超过半数 master,或者一台 master 的 slot 没主且没配 cluster-require-full-coverage no。
怎么验证
bash
redis-cli -h 10.20.13.11 -p 6379 cluster info
# 关注 cluster_state:fail、cluster_slots_fail、cluster_slots_ok
redis-cli -h 10.20.13.11 -p 6379 cluster nodes | grep fail
cluster_slots_fail 大于 0 就解释了 CLUSTERDOWN。
怎么修
如果是死节点占着 slot,回到第一节的做法把 slot 指派给存活节点。节点其实活着只是被误判时,先确认网络恢复。至于 CLUSTER RESET HARD 这类操作要谨慎------它会清空数据,生产上基本不用。
缓解:redis.conf 里
conf
cluster-require-full-coverage no
这样个别 slot 不可用时,其余 slot 还能服务,避免一损俱损。这是权衡,不是万能药,丢了的那部分数据该查的还是要查。
验证与预防
bash
redis-cli -h 10.20.13.11 -p 6379 cluster info | grep -E "cluster_state|cluster_slots"
保证每个 master 至少有一个 slave,节点挂一台不至于 slot 失主。
四、节点失联与主从切换失败
现场现象
一台 master 宕机,但它名下的 slave 没自动顶上,cluster_state 长时间 fail。或者 slave 日志里反复出现无法发起故障转移的提示。
可能原因
cluster-node-timeout 设得太大,故障检测慢;slave 自身也负载高或网络隔离,无法发起故障转移;或者 slave 的 cluster-slave-validity-factor 太严,被认为数据太旧不允许 failover。
怎么验证
看超时和有效性参数:
bash
redis-cli -h 10.20.13.11 -p 6379 config get cluster-node-timeout
redis-cli -h 10.20.13.11 -p 6379 config get cluster-slave-validity-factor
redis-cli -h 10.20.13.11 -p 6379 cluster nodes | grep slave
日志里捞切换线索:
bash
grep -E "failover|FAIL|timeout" /data/redis/logs/redis-node-01.log
怎么修
超时默认 15 秒,内网环境我可以压到 5 秒加快检测:
bash
redis-cli -h 10.20.13.11 -p 6379 config set cluster-node-timeout 5000
slave 因为数据太旧拒绝 failover 时,放宽因子:
bash
redis-cli -h <slave> -p 6379 config set cluster-slave-validity-factor 10
默认值 10 表示允许 slave 数据落后 timeout×10 毫秒仍参与 failover;设为 0 则永不因落后被排除,但可能选到数据很旧的 slave,按需调。
确认真的不会自动切换、业务又等不起时,手动触发:
bash
redis-cli -h <slave> -p 6379 cluster failover
注意要在期望成为新 master 的 slave 上执行,且它和旧 master 之间网络要通。
验证与预防
bash
redis-cli -h 10.20.13.11 -p 6379 cluster nodes | grep master
确认每台 master 有 slave 且分布在不同的物理机/可用区。我们用 redis-cli --cluster add-node 补 slave:
bash
redis-cli --cluster add-node 10.20.13.12:6379 10.20.13.11:6379 \
--cluster-slave --cluster-master-id 96d28ca26d0e0e37511fc4af990d72670d664b1d
五、网络分区:脑裂与少数派
现场现象
机房网络抖动后,出现两个"主"声称持有同一批 slot,客户端一部分写 A 一部分写 B,恢复后数据对不上。或者少数派节点把自己标记为 fail,但多数派还在正常服务,少数派上的 cluster-app 实例开始拒绝写。
可能原因
集群在分区期间发生了脑裂,少数派这边如果有写入,恢复后会被多数派覆盖。min-replicas-to-write 这类保护没开。
怎么验证
分区期间分别连两侧节点看谁是 master:
bash
redis-cli -h 10.20.13.11 -p 6379 cluster nodes | grep myself
redis-cli -h 10.20.13.110 -p 6379 cluster nodes | grep myself
如果两边各自认为自己是某些 slot 的 master,就是脑裂。
怎么修
脑裂已经发生、数据可能冲突时,优先保证多数派继续服务,少数派节点重启让它重新以 slave 身份加入:
bash
# 在少数派节点上
redis-cli -h 10.20.13.14 -p 6379 cluster reset
# 然后重新作为 slave 加入
redis-cli --cluster add-node 10.20.13.14:6379 10.20.13.11:6379 --cluster-slave
不要在脑裂期间对少数派做写操作,那部分数据大概率会被覆盖。
验证与预防
redis.conf 加写保护,避免少数派在没足够副本时接受写:
conf
min-replicas-to-write 1
min-replicas-max-lag 10
含义是:只有当至少 1 个 slave 落后不超过 10 秒时才允许写。分区把 slave 隔开后,master 会拒绝写,换取不脑裂。代价是牺牲一点可用性,按业务容忍度决定开不开。
六、大 key / 热 key:迁移抖动和超时的根源
现场现象
reshard 时某个 slot 特别慢,或者某台节点 CPU 周期性飙高、响应延迟抖动,监控上其他节点很闲就它忙。
可能原因
集群里躺着几个百 MB 的 hash 或百万级 set,迁移时一次性搬很慢;或者某个热点 key 被集中访问,单节点扛不住。
怎么验证
扫大 key(对线上实例用 -i 0.1 降速,避免阻塞):
bash
redis-cli -h 10.20.13.11 -p 6379 --bigkeys -i 0.1
热 key 用 Redis 4.0+ 的 --hotkeys:
bash
redis-cli -h 10.20.13.11 -p 6379 --hotkeys
没有 --hotkeys 时,临时开 monitor 抓一段时间再统计前缀:
bash
redis-cli -h 10.20.13.11 -p 6379 monitor | head -50000 > /tmp/mon.txt
怎么修
大 key 拆掉:把大 hash 按字段哈希成多个小 hash,或者把大 set 分桶。热 key 加本地缓存或读写分离,必要时业务侧做 key 打散(加随机后缀再查时汇总)。
验证与预防
拆完后重新 --bigkeys,确认没有超过阈值(我们内部约定单 key 序列化后不超过 10MB,元素数不超过 50 万)。把 --bigkeys 纳入定期巡检。
七、内存打满:OOM 与驱逐
现场现象
cluster-app 报错变成:
OOM command not allowed when used memory > 'maxmemory'
或者大量 key 莫名消失,延迟升高。
可能原因
maxmemory 设得太小,或没设;驱逐策略选了 noeviction(默认),内存到顶直接拒绝写而不是淘汰。或者是某个业务把本该过期的 key 设成了不过期,慢慢涨满。
怎么验证
bash
redis-cli -h 10.20.13.11 -p 6379 info memory | grep -E "used_memory_human|maxmemory_human|maxmemory_policy"
redis-cli -h 10.20.13.11 -p 6379 info stats | grep evicted_keys
used_memory 贴近 maxmemory、evicted_keys 持续增长,就是驱逐在跑但跟不上写入。
怎么修
先确认策略,缓存用途就该放行淘汰:
bash
redis-cli -h 10.20.13.11 -p 6379 config set maxmemory-policy allkeys-lru
redis-cli -h 10.20.13.11 -p 6379 config set maxmemory 8gb
maxmemory 取值:我们按单机内存的 70% 留足给 fork 和碎片。allkeys-lru 是通用缓存场景的安全选择;如果是持久化存储,得用 volatile-ttl 并保证 key 都设了过期时间。
紧急释放可以手动删已知的大 key,但别用 DEL 大 key 会阻塞,改用 UNLINK:
bash
redis-cli -h 10.20.13.11 -p 6379 unlink big_cache_key
验证与预防
bash
redis-cli -h 10.20.13.11 -p 6379 info memory | grep maxmemory
redis-cli -h 10.20.13.11 -p 6379 info stats | grep -E "evicted_keys|rejected_connections"
给 maxmemory 配监控告警,用到 80% 就提醒。所有缓存 key 强制带 TTL,不依赖默认不过期。
集群巡检清单(可直接执行)
每天 / 每次变更后跑一遍:
bash
ENTRY=10.20.13.11
PORT=6379
# 1. 集群状态与 slot 覆盖
redis-cli -h $ENTRY -p $PORT cluster info | grep -E "cluster_state|cluster_slots_ok|cluster_slots_fail"
# 2. 存活节点与角色
redis-cli -h $ENTRY -p $PORT cluster nodes
# 3. 不可达节点
redis-cli -h $ENTRY -p $PORT cluster nodes | grep -E "fail|disconnected" || echo "无失联节点"
# 4. 故障检测与切换参数
redis-cli -h $ENTRY -p $PORT config get cluster-node-timeout
redis-cli -h $ENTRY -p $PORT config get cluster-slave-validity-factor
# 5. 内存与驱逐
redis-cli -h $ENTRY -p $PORT info memory | grep -E "used_memory_human|maxmemory_human|maxmemory_policy"
# 6. 大 key 扫描(降速)
redis-cli -h $ENTRY -p $PORT --bigkeys -i 0.1
# 7. 集群完整性自检
redis-cli --cluster check $ENTRY:$PORT
判断标准:
cluster_state必须为okcluster_slots_ok必须等于 16384,cluster_slots_fail必须为 0- 每台 master 至少挂 1 个 slave,且不在同一物理机
- 所有节点
cluster-node-timeout一致,建议 5000--10000 used_memory不超过maxmemory的 80%maxmemory-policy不为noeviction(除非是纯持久化存储)- 没有超过内部阈值的大 key
- 全部节点集群配置已持久化(
config rewrite),没有临时改了没落盘