引言
在微服务架构中,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-core和spring-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.set、expire、sAdd,但并未处理任何异常。如果其中某个命令因为网络抖动或数据格式问题抛出异常,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. 根因分析
综合以上信息,问题根因可归纳为:
- 连接池配置过小 :
max-active=8无法支撑业务峰值,连接成为瓶颈。 - 代码中存在未妥善处理异常的Redis操作:当管道内命令执行失败时,连接被标记为不可用并销毁,导致连接泄露。
- 异常未被记录和监控:业务代码未捕获异常,也未对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的地方,确保没有遗漏连接释放。对于SessionCallback、execute等操作,也都遵循了类似原则。
5.3 增加监控告警
在连接池指标上增加告警,当redis.lettuce.pool.active接近max-active时发出预警,以便及早介入。
5.4 处理异常数据
修复了导致命令执行失败的数据源,确保写入Redis的值类型正确。
6. 验证与效果
修改配置并发布后,观察指标:
- 活跃连接数稳定在20-30之间,远低于50的上限。
- 接口超时率降为0,平均耗时恢复正常。
- 异常日志不再出现。
后续一周,我们再未收到类似告警。
7. 总结与反思
这次故障虽然持续时间不长,但给我们带来了深刻的教训:
- 配置即代码:默认配置不一定适合生产环境,上线前应根据业务预估进行压测,调整连接池、线程池等关键参数。
- 异常处理不容忽视:即使框架声称会自动释放资源,我们也要考虑异常路径下的资源管理。主动捕获并记录异常,有助于快速定位问题。
- 监控先行:对关键指标(如连接池使用率、命令失败率)进行监控,能够在问题发生前预警,避免演变成故障。
- 代码审查与压测:代码审查时应关注资源使用模式,压测不仅要看吞吐量,也要观察资源使用趋势,发现潜在泄露。
希望本文的排查思路能对大家有所帮助。在分布式系统日益复杂的今天,每一个细节都可能成为系统的瓶颈,只有持续优化和监控,才能保证系统的稳定可靠。
如果对你有帮助,欢迎点赞、收藏、关注,后续会分享更多生产环境故障排查实战和后端优化技巧~