redis使用不当导致应用卡死bug的过程解析


Posted in Redis onJuly 01, 2021

首先说下问题现象:内网sandbox环境API持续1周出现应用卡死,所有api无响应现象

刚开始当测试抱怨环境响应慢的时候 ,我们重启一下应用,应用恢复正常,于是没做处理。但是后来问题出现频率越来越频繁,越来越多的同事开始抱怨,于是感觉代码可能有问题,开始排查。

首先发现开发的本地ide没有发现问题,应用卡死时候数据库,redis都正常,并且无特殊错误日志。开始怀疑是sandbox环境机器问题(测试环境本身就很脆!_!)

于是ssh上了服务器 执行以下命令

top

redis使用不当导致应用卡死bug的过程解析

这时发现 机器还算正常,但是内心还是?,于是打算看下jvm 堆栈信息

先看下问题应用比较耗资源的线程

执行 top -H -p 12798

redis使用不当导致应用卡死bug的过程解析

找到前3个相对比较耗资源的线程

jstack 查看堆内存

jstack 12798 |grep 12799的16进制 31ff

redis使用不当导致应用卡死bug的过程解析

没看出什么问题,上下10行也看看 于是执行

redis使用不当导致应用卡死bug的过程解析

看到一些线程都是处于lock状态。但没有出现业务相关的代码,忽略了。这时候没有什么头绪。思考一番。决定放弃这次卡死状态的机器

为了保护事故现场 先 dump了问题进程所有堆内存,然后debug模式重启测试环境应用,打算问题再显时直接远程debug问题机器

第二天问题再现,于是通知运维nginx转发拿掉这台问题应用,自己远程debug tomcat。

自己随意找了一个接口,断点在接口入口地方,悲剧开始,什么也没有发生!API等待服务响应,没进断点。这时候有点懵逼,冷静了一会,在入口之前的aop地方下了个断点,再debug一次,这次进了断点,f8 N次后发现在执行redis命令的时候卡主了。继续跟,最后在到jedis的一个地方发现问题:

/**
 * Returns a Jedis instance to be used as a Redis connection. The instance can be newly created or retrieved from a
 * pool.
 * 
 * @return Jedis instance ready for wrapping into a {@link RedisConnection}.
 */
protected Jedis fetchJedisConnector() {
   try {
      if (usePool && pool != null) {
         return pool.getResource();
      }
      Jedis jedis = new Jedis(getShardInfo());
      // force initialization (see Jedis issue #82)
      jedis.connect();
      return jedis;
   } catch (Exception ex) {
      throw new RedisConnectionFailureException("Cannot get Jedis connection", ex);
   }
}

上面pool.getResource()后线程开始wait

public T getResource() {
  try {
    return internalPool.borrowObject();
  } catch (Exception e) {
    throw new JedisConnectionException("Could not get a resource from the pool", e);
  }
}

return internalPool.borrowObject(); 这个代码应该是一个租赁的代码 接着跟

public T borrowObject(long borrowMaxWaitMillis) throws Exception {
    this.assertOpen();
    AbandonedConfig ac = this.abandonedConfig;
    if (ac != null && ac.getRemoveAbandonedOnBorrow() && this.getNumIdle() < 2 && this.getNumActive() > this.getMaxTotal() - 3) {
        this.removeAbandoned(ac);
    }

    PooledObject<T> p = null;
    boolean blockWhenExhausted = this.getBlockWhenExhausted();
    long waitTime = 0L;

    while(p == null) {
        boolean create = false;
        if (blockWhenExhausted) {
            p = (PooledObject)this.idleObjects.pollFirst();
            if (p == null) {
                create = true;
                p = this.create();
            }

            if (p == null) {
                if (borrowMaxWaitMillis < 0L) {
                    p = (PooledObject)this.idleObjects.takeFirst();
                } else {
                    waitTime = System.currentTimeMillis();
                    p = (PooledObject)this.idleObjects.pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS);
                    waitTime = System.currentTimeMillis() - waitTime;
                }
            }

            if (p == null) {
                throw new NoSuchElementException("Timeout waiting for idle object");
            }

其中有段代码

if (p == null) {
    if (borrowMaxWaitMillis < 0L) {
        p = (PooledObject)this.idleObjects.takeFirst();
    } else {
        waitTime = System.currentTimeMillis();
        p = (PooledObject)this.idleObjects.pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS);
        waitTime = System.currentTimeMillis() - waitTime;
    }
}

borrowMaxWaitMillis<0会一直执行,然后一直循环了 开始怀疑这个值没有配置

找到redis pool配置,发现确实没有配置MaxWaitMillis,配置后else代码也是一个Exception 并不能解决问题

继续F8 

public E takeFirst() throws InterruptedException {
    this.lock.lock();

    Object var2;
    try {
        Object x;
        while((x = this.unlinkFirst()) == null) {
            this.notEmpty.await();
        }

        var2 = x;
    } finally {
        this.lock.unlock();
    }

    return var2;
}

到这边 发现lock字眼,开始怀疑所有请求api都被阻塞了

于是再次ssh 服务器 安装 arthas ,(Arthas 是Alibaba开源的Java诊断工具)

执行thread命令 

redis使用不当导致应用卡死bug的过程解析

发现大量http-nio的线程waiting状态,http-nio-8083-exec-这个线程其实就是出来http请求的tomcat线程

随意找一个线程查看堆内存

thread -428

redis使用不当导致应用卡死bug的过程解析

这是能确认就是api一直转圈的问题,就是这个redis获取连接的代码导致的,

解读这段内存代码  所有线程都在等 @53e5504e这个对象释放锁。于是jstack 全局搜了一把53e5504e ,没有找到这个对象所在线程。

自此。问题原因能确定是 redis连接获取的问题。但是什么原因造成获取不到连接的还不能确定

再次执行 arthas 的thread -b (thread -b, 找出当前阻塞其他线程的线程)

redis使用不当导致应用卡死bug的过程解析

没有结果。这边和想的不一样,应该是能找到一个阻塞线程的,于是看了下这个命令的文档,发现有下面的一句话

redis使用不当导致应用卡死bug的过程解析

好吧,我们刚好是后者。。。。

再次整理下思路。这次修改redis pool 配置,将获取连接超时时间设置为2s,然后等问题再次复现时观察应用最后正常时干过什么。

添加一下配置

JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
.......
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxWaitMillis(2000);
.......
jedisConnectionFactory.afterPropertiesSet();

重启服务,等待。。。。

又过一天,再次复现

ssh 服务器,检查tomcat accesslog ,发现大量api 请求出现500,

org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource fr
om the pool
    at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:140)
    at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:229)
    at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:57)
    at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:128)
    at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:91)
    at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:78)
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:177)
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:152)
    at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:85)
    at org.springframework.data.redis.core.DefaultHashOperations.get(DefaultHashOperations.java:48)

找到源头第一次出现500地方,

发现以下代码

.......
Cursor c = stringRedisTemplate.getConnectionFactory().getConnection().scan(options);
while (c.hasNext()) {
.....,,
   }

分析这个代码,stringRedisTemplate.getConnectionFactory().getConnection()获取pool中的redisConnection后,并没有后续操作,也就是说此时redis 连接池中的链接被租赁后并没有释放或者退还到链接池中,虽然业务已处理完毕 redisConnection 已经空闲,但是pool中的redisConnection的状态还没有回到idle状态

redis使用不当导致应用卡死bug的过程解析

正常应为

redis使用不当导致应用卡死bug的过程解析

自此问题已经找到。

总结:spring stringRedisTemplate 对redis常规操作做了一些封装,但还不支持像 Scan SetNx等命令,这时需要拿到jedis Connection进行一些特殊的Commands

使用 stringRedisTemplate.getConnectionFactory().getConnection() 是不被推荐的

我们可以使用

stringRedisTemplate.execute(new RedisCallback<Cursor>() {

     @Override
     public Cursor doInRedis(RedisConnection connection) throws DataAccessException {
         
       return connection.scan(options);
     }
   });

来执行,

或者使用完connection后 ,用

RedisConnectionUtils.releaseConnection(conn, factory);

来释放connection.

同时,redis中也不建议使用keys命令,redis pool的配置应该合理配上,否则出现问题无错误日志,无报错,定位相当困难。

 到此这篇关于redis使用不当导致应用卡死bug的过程解析的文章就介绍到这了,更多相关redis导致应用卡死内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
详解Redis实现限流的三种方式
Apr 27 Redis
详解Redis基本命令与使用场景
Jun 01 Redis
使用Redis实现实时排行榜功能
Jul 02 Redis
Redis Cluster 集群搭建你会吗
Aug 04 Redis
基于Redis结合SpringBoot的秒杀案例详解
Oct 05 Redis
redis的list数据类型相关命令介绍及使用
Jan 18 Redis
使用Redis实现点赞取消点赞的详细代码
Mar 20 Redis
Redis实现一个账号只能登录一个设备
Apr 19 Redis
Redis keys命令的具体使用
Jun 05 Redis
Redis实现订单过期删除的方法步骤
Jun 05 Redis
Redis实现主从复制方式(Master&Slave)
Jun 21 Redis
redis lua限流算法实现示例
Jul 15 Redis
Redis主从配置和底层实现原理解析(实战记录)
浅谈Redis中的RDB快照
聊一聊Redis与MySQL双写一致性如何保证
k8s部署redis cluster集群的实现
Jun 24 #Redis
浅析Redis Sentinel 与 Redis Cluster
redis cluster支持pipeline的实现思路
了解Redis常见应用场景
Jun 23 #Redis
You might like
微信自定义菜单的处理开发示例
2015/04/16 PHP
基于PHP的登录和注册的功能的实现
2020/08/06 PHP
IE不出现Flash激活框的小发现的js实现方法
2007/09/07 Javascript
仿163填写邮件地址自动显示下拉(无优化)
2008/11/05 Javascript
JavaScript 捕获窗口关闭事件
2009/07/26 Javascript
JS获取整个页面文档的实现代码
2011/12/15 Javascript
JavaScript DOM进阶方法
2015/04/13 Javascript
BootStrap智能表单实战系列(四)表单布局介绍
2016/06/13 Javascript
Node.js DES加密的简单实现
2016/07/07 Javascript
chrome浏览器如何断点调试异步加载的JS
2016/09/05 Javascript
JavaScript实现清空(重置)文件类型INPUT元素值的方法
2016/11/17 Javascript
图文详解Javascript中的上下文和作用域
2017/02/15 Javascript
详解vue数据渲染出现闪烁问题
2017/06/29 Javascript
微信小程序开发教程之增加mixin扩展
2017/08/09 Javascript
vue mounted组件的使用
2018/06/18 Javascript
浅谈vue引用静态资源需要注意的事项
2018/09/28 Javascript
Vue.js 时间转换代码及时间戳转时间字符串
2018/10/16 Javascript
模块化react-router配置方法详解
2019/06/03 Javascript
React实现类似淘宝tab居中切换效果的示例代码
2020/06/02 Javascript
python批量生成本地ip地址的方法
2015/03/23 Python
python3常用的数据清洗方法(小结)
2019/10/31 Python
python元组拆包实现方法
2021/02/28 Python
分享一个H5原生form表单的checkbox特效代码
2018/02/26 HTML / CSS
Canvas绘制浮动球效果的示例
2017/12/29 HTML / CSS
h5封装下拉刷新
2020/08/25 HTML / CSS
英国Boots旗下太阳镜网站:Boots Designer Sunglasses
2018/07/07 全球购物
有影响力的人、名人和艺术家的官方商品:Represent
2019/11/26 全球购物
中专毕业生自我鉴定范文
2013/11/09 职场文书
生产厂厂长岗位职责
2013/12/25 职场文书
自我评价格式
2014/01/06 职场文书
大学社团计划书
2014/05/01 职场文书
委托证明范本
2014/11/25 职场文书
公司员工管理制度
2015/08/04 职场文书
奇妙的 CSS shapes(CSS图形)
2021/04/05 HTML / CSS
Oracle安装TNS_ADMIN环境变量设置参考
2021/11/01 Oracle
vue自定义右键菜单之全局实现
2022/04/09 Vue.js