一、问题排查
1、问题现象描述
- 接口服务调用超时
- Grafana监控显示kafka消息堆积严重
- 无相关错误日志打印
2、问题具体排查
排查服务情况,未发现异常
- 服务运行正常,JVM正常
- 服务相关机器内存、CPU资源使用情况正常
排查日志,通过日志打印情况发现调用超时的接口服务线程卡死
这一行日志后,还有某些必执行业务逻辑中的日志未打印。且未发现错误日志打印,初步判断为线程卡死。
打印堆栈信息,进一步排查,锁定卡死位置
-
通过jstack -l >threadDump.log打印堆栈日志,并进入网站https://fastthread.io/分析对应的堆栈日志
-
分析火焰图排查异常线程
通过火焰图很容易看出,其他等待线程诸如Epoll的select和线程池核心线程等均为正常阻塞。而图中标识处均为Jedis相关,而Redis相关操作一般不会阻塞,判断为问题线程。
- 查看业务日志阻塞线程[http-nio-8080-exec-8]对应为堆栈信息
分析堆栈以及查看源码可以得出线程阻塞于JedisPool到Jedis对象池中获取Jedis对象失败造成的阻塞。也就是getResource()卡死。该阻塞线程也能与业务日志打印线程ID对应。
- jmap打印Java进程实例对象进一步确认
可以看到,如今存活的Jedis实例有120个。该服务中有两个JedisPool,具体Jedis配置如下:
可以看到最大线程数为100,最大空闲线程数为20。分析为两个线程池,一个为20个空闲Jedis对象,另外一个为100个Jedis对象全部用完,这时候如果需要继续获取Jedis资源就会导致线程阻塞,找到问题点。而max-wait=-1,意味着获取不到资源线程会无限期的等待,这也是线程一直阻塞的原因。
- 查看Jedis源码找到根因(具体如下)
二、问题根因
该问题由于高并发下Jedis框架内存泄漏导致,该问题共设计Jedis版本2.9.1、2.9.2、2.10.0、2.10.1四个版本,具体可参考issues:https://github.com/redis/jedis/issues/1920
对于以下代码(Jedis:2.9.1框架源码),该类为Jedis,dataSoure指代JedisPool,高并发情况下
1、如果A线程图一1662行已归还Jedis对象入 JedisPool 池,由于线程上下文切换导致该线程停在这一步
2、与此同时B线程于 JedisPool池中获取该对象,并赋值 dataSoure(如图二167行)
3、A线程获取到时间片,并执行 图一 1665行代码,导致dataSoure = null
4、B线程使用完毕后,执行close()方法,无法通过returnResource正确归还Jedis对象入池
图一:
图二:
三、问题佐证
通过Arthas对JedisPool对象池中对象具体分析,Jedis对象可分成下列几种
1、发生异常Jedis对象
-
状态为ALLOCATED,dataSource=null
-
由于dataSource=null,无法被归还
-
由于状态为ALLOCATED,无法被取用
2、正常的空闲Jedis对象
- 状态为IDLE,dataSource=null
- 由于状态为IDLE,可以被继续取用
3、正常的已分配Jedis对象
- 状态为ALLOCATED,dataSource=JedisPool对象
- 可通过JedisPool对象的引用正常关闭
四、问题解决方案
1、 Jedis原生解决方案
引入该问题之前(2.9.0版本前)
归还线程后,不去除去JedisPool的引用
-
该方案不去除Jedis中JedisPool的引用,仅在getResource()中重新设定JedisPool对象。
-
该方案不会有高并发下卡死的问题。但是由于空闲状态下Jedis引用JedisPool,Pool也有JedisPool的引用,会有循环依赖的问题。
该问题修复后(2.10.2版本后)
使用局部变量代替全局变量后,先清除全局变量的引用,局部变量随线程栈一起清除。
2、其他框架对比
-
Jedis使用同步和阻塞IO的方式,不支持异步;lettuce和Redisson支持异步,底层是基于netty框架的事件驱动作为通信层。
-
Jedis设计上就是基于线程不安全来设计,一个连接只能被一个线程使用,结合连接池来提高其性能,但是某些涉及到Jedis对象复用的时候未加锁,会有线程安全问题;lettuce和Redisson基于线程安全来设计的,一个连接是被共享使用的,但是也提供了连接池,主要用于事务以及阻塞操作的命令。
框架 | 易用度 | 通讯模式 | 性能 | 高并发问题 | 线程安全问题 | 高级特性 |
---|---|---|---|---|---|---|
Jedis | 简单 | BIO | 较低 | 本文所述内存泄漏 | 有 | 基本不支持 |
Lettuce | 简单 | NIO | 高 | 高并发下操作大key的OOM问题 | 无 | 基本不支持 |
Redisson | 中等 | NIO | 高 | 暂无 | 无 | 支持 |
3、修复建议
- 首先推荐使用Redisson,Redisson为Redis提供了很多高级特性,且性能优异。特别是Redisson实现的分布式锁,是现如今最好的Redis分布式锁方案之一。
- 对于使用SpringBoot原生的RedisTemplate,建议直接使用原生的lettuce,但要考虑并避免lettuce高并发下操作大Key导致OOM问题。
优异。特别是Redisson实现的分布式锁,是现如今最好的Redis分布式锁方案之一。 - 对于使用SpringBoot原生的RedisTemplate,建议直接使用原生的lettuce,但要考虑并避免lettuce高并发下操作大Key导致OOM问题。
- 如果无法避免需要使用Jedis框架,建议避开2.9.1、2.9.2、2.10.0、2.10.1这4个版本。