Redis 集群故障排查与修复实战:从 Connection refused 到内存打满的七个现场

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 必须为 ok
  • cluster_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),没有临时改了没落盘
相关推荐
酒神dnspup1 小时前
IPv6 网站访问异常怎么查?DNSPup 双栈检测、DNS 与端口排障指南
运维·云计算·域名·dns·dnspup
StackTraces1 小时前
从零跑通 Meilisearch 本地搜索服务:6 步,外加 4 个容易卡住的地方
数据库·后端
CoLiuRs1 小时前
订单不是一张表:订单聚合与状态机
数据库·架构
用户6222884048191 小时前
Redis 内存管理与过期淘汰策略:从 maxmemory 到 LRU / LFU,一篇讲透
java·redis
shuoshuohaohao1 小时前
Spring-boot-3 -注解 yaml配置 -日志
java·数据库·mysql
码完就睡2 小时前
Linux——线程的创建与同步
linux·运维
IT邦德2 小时前
Bond-DBCheck 邦德智能巡检平台助力 DBA
数据库·dba
FY@-@2 小时前
Shell 脚本编程:条件判断与循环
linux·运维·前端
Peregrine92 小时前
Linux 基本指令大全
linux·运维·服务器