Jedis高并发下内存泄漏导致getResource()卡死问题及解决方法

一、问题排查

1、问题现象描述

  1. 接口服务调用超时
  2. Grafana监控显示kafka消息堆积严重
  3. 无相关错误日志打印

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个版本。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值