一次生产环境下的Redis连接耗尽问题排查与解决全过程

引言

在微服务架构中,Redis作为高性能的缓存和分布式锁组件被广泛使用。然而,随着业务量的增长,Redis连接管理不当往往会引发意想不到的故障。本文将记录一次真实的生产环境Redis连接耗尽问题的排查与解决过程,希望能为遇到类似问题的同学提供一些思路和参考。

1. 问题发现

某个周四下午,我们突然收到多条告警:

  • 业务监控:订单服务的API接口超时率飙升,部分请求耗时超过10秒。
  • 应用日志:大量redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool异常。
  • 系统负载:订单服务CPU使用率正常,但Redis服务器CPU略有上升。

初步判断,问题很可能出在Redis客户端连接池上------连接池无法提供可用连接,导致请求阻塞或超时。

2. 初步排查

2.1 检查Redis服务端状态

登录Redis服务器,使用redis-cli info clients查看客户端连接情况:

text

makefile 复制代码
# Clients
connected_clients:512
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0

connected_clients为512,而我们的Redis配置中maxclients为10000,远未达到上限。这说明问题不在服务端连接数限制,而是客户端连接池耗尽。

2.2 查看应用连接池配置

订单服务使用Spring Boot 2.x + Lettuce连接池(默认配置)。检查配置文件:

yaml

yaml 复制代码
spring:
  redis:
    host: redis.example.com
    port: 6379
    lettuce:
      pool:
        max-active: 8   # 连接池最大连接数(默认为8)
        max-idle: 8     # 最大空闲连接
        min-idle: 0     # 最小空闲连接
        max-wait: -1ms  # 获取连接最大等待时间(-1表示无限等待)

max-active=8意味着连接池最多只能创建8个连接。对于一个日均请求量百万级的服务,8个连接显然不够用。但这只是初步怀疑,我们需要确认连接池是否真的用满,以及为何会被占满。

2.3 实时监控连接池状态

为了观察连接池运行情况,我们通过Actuator暴露了Redis连接池指标(需要引入micrometer-corespring-boot-starter-actuator),并实时查看:

text

bash 复制代码
GET /actuator/metrics/redis.lettuce.pool.active

结果:active连接数一直维持在8,且没有下降趋势。说明连接池已满,且长时间没有释放。

3. 深入分析

3.1 代码审查

我们重点审查了所有使用Redis的代码,特别是涉及事务、管道和订阅发布的地方。很快发现一处可疑代码:

java

scss 复制代码
public void processOrder(Order order) {
    // 业务逻辑...
    List<Object> results = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
        // 在管道中执行多个操作
        connection.set(("key:" + order.getId()).getBytes(), order.getData().getBytes());
        connection.expire(("key:" + order.getId()).getBytes(), 3600);
        connection.sAdd("order:queue".getBytes(), order.getId().toString().getBytes());
        // 注意:这里没有调用connection.close(),但Lettuce会自动归还?
        return null;
    });
    // ...后续操作
}

这段代码使用了executePipelined,但在回调中并没有显式释放连接。根据Lettuce的文档,executePipelined执行完毕后会自动释放连接。那为何连接还会泄露?

3.2 压测复现

为了验证代码是否存在泄露,我们在测试环境模拟高并发请求,并监控连接池指标。同时开启Redis的monitor命令观察客户端命令执行情况。

压测命令(使用wrk):

bash

arduino 复制代码
wrk -t10 -c100 -d60s http://order-service/process

观察指标:

  • 活跃连接数从0开始上升,很快达到8并保持。
  • 压测结束后,活跃连接数缓慢下降,但仍有部分连接未被释放(最终稳定在4左右)。

这说明确实存在连接泄露:部分连接在执行完管道操作后没有被正确归还到池中。

3.3 定位泄露点

进一步调试发现,当管道操作中抛出异常时(比如某个命令失败),Lettuce的自动释放机制可能失效。我们查看executePipelined的源码(Lettuce 5.x)发现,它内部会通过try-finally确保连接释放,但如果在回调中发生某些特定异常,可能会导致连接被标记为损坏而无法回池。

我们注意到代码中使用了connection.setexpiresAdd,但并未处理任何异常。如果其中某个命令因为网络抖动或数据格式问题抛出异常,executePipelined会捕获并重新抛出,但连接状态可能已经损坏。此时Lettuce会关闭该连接,而不是归还到池中。频繁的异常导致连接被销毁,而新连接被创建,最终max-active限制使得连接池被占满(因为创建新连接需要时间,且旧连接未及时释放)。

3.4 查看异常日志

搜索应用日志,果然发现大量Redis命令执行异常:

text

csharp 复制代码
Caused by: io.lettuce.core.RedisCommandExecutionException: ERR value is not an integer or out of range
    at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:138)
    ...

原来是某个字段存储时使用了错误的数据类型,导致命令执行失败。每次失败都会使连接被销毁,新的请求需要创建新连接,而创建连接的过程是同步阻塞的,导致请求堆积,最终连接池耗尽。

4. 根因分析

综合以上信息,问题根因可归纳为:

  1. 连接池配置过小max-active=8无法支撑业务峰值,连接成为瓶颈。
  2. 代码中存在未妥善处理异常的Redis操作:当管道内命令执行失败时,连接被标记为不可用并销毁,导致连接泄露。
  3. 异常未被记录和监控:业务代码未捕获异常,也未对Redis操作失败进行降级或重试,导致异常传播至上层,引发接口超时。

5. 解决方案

5.1 立即调整连接池配置

根据业务QPS和Redis响应时间,重新估算连接池大小。经验公式:连接数 = (QPS * 平均耗时(ms)) / 1000。估算后,我们将max-active调整为50,max-idle调整为20,并设置max-wait为3000ms(避免无限等待)。

yaml

yaml 复制代码
spring:
  redis:
    lettuce:
      pool:
        max-active: 50
        max-idle: 20
        min-idle: 5
        max-wait: 3000ms

5.2 修复代码中的连接泄露

针对管道操作,增加异常捕获,确保无论是否发生异常,都能正确释放连接(虽然Lettuce理论上会自动释放,但主动捕获并记录异常有助于排查)。

优化后的代码:

java

scss 复制代码
public void processOrder(Order order) {
    try {
        List<Object> results = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            connection.set(("key:" + order.getId()).getBytes(), order.getData().getBytes());
            connection.expire(("key:" + order.getId()).getBytes(), 3600);
            connection.sAdd("order:queue".getBytes(), order.getId().toString().getBytes());
            return null;
        });
        // 处理results...
    } catch (Exception e) {
        // 记录异常,并根据业务决定是否重试或降级
        log.error("Redis pipeline执行失败,orderId: {}", order.getId(), e);
        // 可考虑将失败任务放入本地缓存或MQ,后续补偿
    }
}

同时,我们检查了所有使用RedisTemplate的地方,确保没有遗漏连接释放。对于SessionCallbackexecute等操作,也都遵循了类似原则。

5.3 增加监控告警

在连接池指标上增加告警,当redis.lettuce.pool.active接近max-active时发出预警,以便及早介入。

5.4 处理异常数据

修复了导致命令执行失败的数据源,确保写入Redis的值类型正确。

6. 验证与效果

修改配置并发布后,观察指标:

  • 活跃连接数稳定在20-30之间,远低于50的上限。
  • 接口超时率降为0,平均耗时恢复正常。
  • 异常日志不再出现。

后续一周,我们再未收到类似告警。

7. 总结与反思

这次故障虽然持续时间不长,但给我们带来了深刻的教训:

  1. 配置即代码:默认配置不一定适合生产环境,上线前应根据业务预估进行压测,调整连接池、线程池等关键参数。
  2. 异常处理不容忽视:即使框架声称会自动释放资源,我们也要考虑异常路径下的资源管理。主动捕获并记录异常,有助于快速定位问题。
  3. 监控先行:对关键指标(如连接池使用率、命令失败率)进行监控,能够在问题发生前预警,避免演变成故障。
  4. 代码审查与压测:代码审查时应关注资源使用模式,压测不仅要看吞吐量,也要观察资源使用趋势,发现潜在泄露。

希望本文的排查思路能对大家有所帮助。在分布式系统日益复杂的今天,每一个细节都可能成为系统的瓶颈,只有持续优化和监控,才能保证系统的稳定可靠。

如果对你有帮助,欢迎点赞、收藏、关注,后续会分享更多生产环境故障排查实战和后端优化技巧~

相关推荐
自珍JAVA8 分钟前
Gobrs-Async 框架
后端
xdscode13 分钟前
Spring 依赖注入方式全景解析
java·后端·spring
青柠代码录21 分钟前
【Spring】@Component VS @Configuration
后端
喵个咪1 小时前
go-wind-cms 微服务架构设计:为什么基于 Kratos?
后端·微服务·cms
神奇小汤圆1 小时前
百度面试官:Redis 内存满了怎么办?你有想过吗?
后端
喵个咪1 小时前
Headless 架构优势:内容与展示解耦,一套 API 打通全端生态
前端·后端·cms
开心就好20251 小时前
HTTPS超文本传输安全协议全面解析与工作原理
后端·ios
小江的记录本1 小时前
【JEECG Boot】 JEECG Boot——数据字典管理 系统性知识体系全解析
java·前端·spring boot·后端·spring·spring cloud·mybatis
神奇小汤圆1 小时前
Spring Batch实战
后端
喵个咪1 小时前
传统 CMS 太笨重?试试 Headless 架构的 GoWind,轻量又强大
前端·后端·cms