一次生产环境下的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. 代码审查与压测:代码审查时应关注资源使用模式,压测不仅要看吞吐量,也要观察资源使用趋势,发现潜在泄露。

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

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

相关推荐
uzong4 小时前
十年老员工的项目管理实战心得:有道有术
后端
Victor3565 小时前
MongoDB(31)索引对查询性能有何影响?
后端
Victor3565 小时前
MongoDB(30)如何删除索引?
后端
lizhongxuan6 小时前
多 Agent 协同机制对比
后端
IT_陈寒6 小时前
SpringBoot项目启动慢?5个技巧让你的应用秒级响应!
前端·人工智能·后端
树上有只程序猿7 小时前
2026低代码选型指南,主流低代码开发平台排名出炉
前端·后端
高端章鱼哥7 小时前
为什么说用OpenClaw对打工人来说“不划算”
前端·后端
大脸怪7 小时前
告别 F12!前端开发者必备:一键管理 localStorage / Cookie / SessionStorage 神器
前端·后端·浏览器
用户8356290780517 小时前
使用 C# 在 Excel 中创建数据透视表
后端·python
架构师沉默7 小时前
别又牛逼了!AI 写 Java 代码真的行吗?
java·后端·架构