一次Redis连接数打满导致业务雪崩的排查记录

描述

问题背景

某在线教育平台在一个工作日上午 10:00(业务高峰时段)收到大量线上报警:

用户端:页面加载失败、提交作业超时

服务端:Java 应用频繁报出 redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool

Redis 监控:connected_clients 达到上限 maxclients=10000,rejected_connections 开始出现

更严重的是,Redis 连接数打满后,连锁导致依赖 Redis 的认证服务、会话服务、缓存服务全部不可用,流量进一步转移到剩余的正常服务,引发了小范围的业务雪崩。

一、Redis 连接机制快速理解

1.1 Redis 如何处理连接

Redis 是单线程事件循环模型(6.0 之后网络 IO 可多线程,但核心处理仍是单线程)。每个客户端连接占用一个文件描述符(fd),Redis 通过 epoll 或 kqueue 处理事件。

关键配置项:

 

maxclients 10000         # 最大连接数(默认 10000,Redis 2.4+)
timeout 0                # 连接空闲超时(0 表示永不超时)
tcp-keepalive 300        # TCP keepalive 间隔

 

1.2 连接数打满的影响

当连接数达到 maxclients 后,Redis 不再接受新连接,并直接在日志中输出:

 

# Redis 9001  refused connection (maxclients)
-ERR max number of clients reached

 

此时所有依赖 Redis 的服务都会受到影响:

新请求无法获取 Redis 连接 → 业务线程阻塞

阻塞线程不断重试 → 线程池打满 → Web 容器无可用线程

上游服务的健康检查失败 → 从注册中心摘除节点

流量转移到剩余节点 → 剩余节点的 Redis 连接也暴增 → 级联故障

二、排查过程

2.1 直接检查 Redis 连接状况

 

# 登录 Redis 查看连接数
$ redis-cli -h  -p 6379 -a  INFO clients
# Clients
connected_clients:10000
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0
cluster_connections:0
maxclients:10000

 

connected_clients=10000 且 maxclients=10000,连接数已打满。

2.2 查看连接分布

 

# 列出所有客户端连接
$ redis-cli CLIENT LIST
id=12345 addr=10.0.1.12:54321 fd=23 name= age=12345 idle=456 flags=N ...
id=12346 addr=10.0.1.13:54322 fd=24 name= age=12300 idle=12 flags=N ...
...

 

输出列含义:

addr:客户端 IP 和端口

age:连接存在时间(秒)

idle:连接空闲时间(秒)

flags:N 表示普通客户端,M 表示主从,S 表示从库

按 IP 统计连接数:

 

# 统计各客户端 IP 的连接数
$ redis-cli CLIENT LIST | awk '{print $2}' | cut -d= -f2 | cut -d: -f1 | sort | uniq -c | sort -rn | head -10
   3000 10.0.1.12
   2800 10.0.1.14
   1500 10.0.1.13
   1200 10.0.1.15
    ...

 

2.3 查找僵尸连接

 

# 查找空闲时间大于 300 秒的连接
$ redis-cli CLIENT LIST | awk -F'[ =]' '{for(i=1;i<=NF;i++) if($i=="idle") print $(i+1), $0}' | awk '$1 > 300' | head -20

# 更简洁的方式
$ redis-cli CLIENT LIST | grep -E "idle=[0-9]{4,}"

 

发现大量连接的 idle 值在 600~3600 秒之间,说明这些连接长时间空闲但没有被回收。timeout 配置为 0(永不超时)。

注:Redis 7.0+ 引入 CLIENT NO-TOUCH 和 CLIENT NO-EVICT 指令,但 timeout 仍是控制僵尸连接的主要参数。

2.4 确认系统级限制

Redis 的 maxclients 受系统文件描述符限制和内核参数限制:

 

# Redis 进程的 fd 限制
$ cat /proc/$(pidof redis-server)/limits | grep "Max open files"
Max open files            10024                10024                files

# 系统级 fd 限制
$ cat /proc/sys/fs/file-max
100000

# 当前 fd 使用量
$ cat /proc/sys/fs/file-nr
30000   0   100000

 

Redis 进程的 Max open files 为 10024,与 maxclients 10000 非常接近(Redis 本身还需要占用少量 fd 用于监听端口、AOF 写入等)。

2.5 应用层排查

检查应用侧的 Redis 连接池配置:

 

 

          
            
            
    
    
    
    

 

3000 个应用实例(部署了 15 个节点 × 每个节点 maxTotal=200) × 连接复用不足 → 实际连接数远超预期。

三、根因分析

经过以上排查,确定本次事故由三个因素叠加导致:

3.1 直接原因:应用发布后连接数暴增

当天凌晨发布新版本后,应用启动时连接池的 minIdle=10 导致每个节点预先建立 10 条连接。发布方式是滚动更新,旧节点和新节点短暂共存,连接数翻倍。部分旧节点上的连接在优雅关闭时未正确释放,变成僵尸连接。

3.2 放大因素:timeout = 0

Redis 配置 timeout=0(永不主动断开空闲连接)。大量僵尸连接(idle > 600s)未被回收,持续占用连接槽位。等到上午业务高峰到来时,新连接请求直接打到上限。

3.3 雪崩链路

 

Redis 连接满(maxclients=10000)
  → 新请求无法获取连接 → JedisConnectionException
  → 应用层无熔断降级 → 线程被重试阻塞
  → Web 容器线程池打满 → 健康检查接口超时
  → 注册中心摘除节点 → 流量转移到剩余节点
  → 剩余节点 Redis 连接数飙升 → 也打满
  → 服务全面不可用

 

四、快速止血方案

4.1 方案 A:临时增大 maxclients(推荐首选)

 

# 临时修改(重启后失效)
$ redis-cli CONFIG SET maxclients 20000

 

注意:增大 maxclients 的同时必须增大系统的 fd 限制:

 

# 临时修改 Redis 进程的 fd 限制
$ prlimit --pid $(pidof redis-server) --nofile=30000

# 或修改系统级限制
$ ulimit -n 65535

 

永久修改 /etc/security/limits.conf:

 

# 添加以下行
root       soft    nofile      65535
root       hard    nofile      65535
redis      soft    nofile      65535
redis      hard    nofile      65535

 

Redis 配置文件中的 maxclients 也要同步修改:

 

# /etc/redis/redis.conf
maxclients 20000

 

4.2 方案 B:启用 timeout 自动清理僵尸连接

 

# 设置空闲超时 60 秒(立即生效)
$ redis-cli CONFIG SET timeout 60

 

60 秒后,所有空闲超过 60 秒的连接会被 Redis 自动关闭。这会触发以下效果:

 

# 观察连接数变化
$ watch -n 5 'redis-cli INFO clients | grep connected_clients'

 

timeout 值应根据业务心跳间隔设置。一般建议:

Web 应用 + 连接池:timeout=60~300

长连接订阅/推送:timeout=600~3600

仅做缓存:timeout=60

4.3 方案 C:批量清理异常连接

如果上述方案来不及等待,直接按条件 kill 连接:

 

# 按类型 kill(kill 所有普通客户端连接,保留主从复制连接)
$ redis-cli CLIENT KILL TYPE normal

# 按 IP 段 kill(如果某台应用服务器连接异常)
$ redis-cli CLIENT KILL addr 10.0.1.12:0

# 跳过 skipme(是否 kill 当前连接,默认 yes)
$ redis-cli CLIENT KILL addr 10.0.1.12:0 skipme no

 

4.4 方案 D:重启应用(让连接池重建)

作为兜底方案,重启应用让连接池重新初始化:

 

# 重启应用服务(确保是滚动重启)
$ systemctl restart app-service

 

重启后连接数会在短时间内回归正常水平(minIdle 重新建立)。

五、长期治理方案

5.1 连接池最佳实践

生产环境合理的连接池配置:

 


     
            
             
              

     
     

     
     
    
    
    

     
    

 

配置原则:

maxTotal 不要过大:单个应用节点对单个 Redis 实例的 maxTotal 建议 20~100,视并发度而定。连接数不是越多越好,Redis 处理 10000 个空闲连接和 500 个活跃连接的开销完全不同。

设置 maxWaitMillis:避免线程无限等待连接,建议 1000~3000ms。

开启 testWhileIdle:定时检测空闲连接是否可用,避免连接被 Redis 侧关闭后应用仍在使用。

代码中正确归还连接:使用 try-with-resources(Jedis 3.x+ 支持)或 finally 块确保 close。

 

// Jedis 3.x 推荐用法
try (Jedis jedis = jedisPool.getResource()) {
    jedis.set("key", "value");
} // 自动归还连接,无需显式 close

// Jedis 2.x 必须显式 close
Jedis jedis = null;
try {
    jedis = jedisPool.getResource();
    jedis.set("key", "value");
} finally {
    if (jedis != null) {
        jedis.close();  // 归还到连接池而非真正关闭
    }
}

 

5.2 使用连接代理收敛架构

如果有大量客户端直连 Redis,考虑引入连接代理层:

 

[应用实例] x 50个 → [Twemproxy / Predixy] → [Redis 主从]

 

优势:

连接数从 M × N 降为 M + N(M 是应用节点,N 是 Redis 节点)

代理层可以缓冲突发连接请求

支持读写分离和故障转移

缺点:

增加一层网络跳转,延迟增加 0.1~0.5ms

代理本身可能成为瓶颈

5.3 连接限额和防火墙防护

 

# 限制单台应用服务器的 Redis 连接数(iptables)
$ iptables -A INPUT -p tcp --dport 6379 -m connlimit --connlimit-above 50 -j REJECT

 

5.4 熔断降级和重连退避

应用侧必须实现熔断机制:

 

// 使用 Resilience4j CircuitBreaker
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)           // 50% 失败率触发熔断
    .waitDurationInOpenState(Duration.ofSeconds(30))  // 熔断后等 30 秒
    .slidingWindowSize(10)
    .build();

// 或简单的退避策略
int baseDelay = 100;  // 基础等待 100ms
for (int i = 0; i < maxRetries; i++) {
    try {
        return jedisPool.getResource();
    } catch (Exception e) {
        Thread.sleep(baseDelay * (long)Math.pow(2, i));  // 指数退避
    }
}

 

5.5 监控和告警

必须监控的 Redis 连接指标:

 

# Prometheus redis_exporter 已暴露的关键指标
redis_connected_clients
redis_config_maxclients
redis_rejected_connections_total

 

告警阈值建议:

指标 告警阈值 严重级别
connected_clients/maxclients > 80% Warning
connected_clients/maxclients > 90% Critical
rejected_connections > 0 立即 P0 Emergency

六、生产环境注意事项

CONFIG SET maxclients 需要联动调整多个参数:

缺了任何一个,maxclients 设置不生效。

Redis 配置 maxclients

系统的 ulimit -n

内核的 fs.file-max

/etc/security/limits.conf

不要在高峰期修改 timeout:如果 timeout 从 0 改为一个很小的值(如 30),会瞬间断开大量连接,导致客户端连接池出现批量重建连接的场景,可能触发 CPU 暴涨和网络抖动。

CLIENT KILL 需要注意 skipme:如果当前连接也在 kill 范围内,会导致当前命令执行中断。默认 skipme=yes 不 kill 当前连接。

云 Redis 服务的 maxclients 限制:各云厂商 Redis 实例的 maxclients 可能与实例规格绑定(如 2GB 实例 = maxclients 10000),不能无限上调。提工单前先确认产品文档。

连接池泄漏排查:如果频繁出现连接打满但 CLIENT LIST 看不出来异常,检查应用代码中是否将 Jedis 实例作为成员变量而非局部变量使用,或者异常分支未执行 close。

七、总结

Redis 连接数打满导致业务雪崩的排查和治理可以分为三层:

第一层:快速止血

 

CONFIG SET maxclients 20000    # 增大上限
CONFIG SET timeout 60          # 清理僵尸连接
CLIENT KILL TYPE normal        # 批量杀异常连接

 

第二层:排查根因

 

检查连接分布 → 分析空闲时间 → 核对连接池配置 → 确认系统限制

 

第三层:架构治理

 

连接池规范化 → 熔断降级 → 代理收敛 → 监控告警 → 应急预案

 

本次故障的核心教训是:

timeout=0 在生产环境是高风险配置,必须设置合理的空闲超时

连接池的 minIdle/maxTotal 要根据实际并发度计算,而非随意配置

服务必须有熔断降级机制,否则单一组件故障会级联扩散

发布过程中要监控连接数变化,发现异常立即回滚或调整

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分