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集群搭建的三种方式
May 31 Redis
Redis集群的关闭与重启操作
Jul 07 Redis
Redis读写分离搭建的完整步骤
Sep 14 Redis
为什么RedisCluster设计成16384个槽
Sep 25 Redis
Redis的字符串是如何实现的
Oct 24 Redis
redis数据结构之压缩列表
Mar 21 Redis
Redis 哨兵机制及配置实现
Mar 25 Redis
redis sentinel监控高可用集群实现的配置步骤
Apr 01 Redis
Redis特殊数据类型bitmap位图
Jun 01 Redis
Redis批量生成数据的实现
Jun 05 Redis
Redis实战之Lettuce的使用技巧详解
Dec 24 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
php动态生成JavaScript代码
2009/03/09 PHP
PHP封装CURL扩展类实例
2015/07/28 PHP
浅谈php中变量的数据类型判断函数
2017/03/04 PHP
php 中phar包的使用教程详解
2018/10/26 PHP
用js实现手把手教你月入万刀(转贴)
2007/11/07 Javascript
JS文本框不能输入空格验证方法
2013/03/19 Javascript
JS随机漂浮广告代码具体实例
2013/11/19 Javascript
javascript中的throttle和debounce浅析
2014/06/06 Javascript
js控制多图左右滚动切换效果代码分享
2015/08/26 Javascript
JS实现队列与堆栈的方法
2016/04/21 Javascript
JavaScript中各种引用类型的常用操作方法小结
2016/05/05 Javascript
点击页面任何位置隐藏div的实现方法
2016/09/05 Javascript
原生js实现弹出层效果
2017/01/20 Javascript
Angular指令之restict匹配模式的详解
2017/07/27 Javascript
Vue2.0基于vue-cli+webpack Vuex的用法(实例讲解)
2017/09/15 Javascript
AngularJS 中的数据源的循环输出
2017/10/12 Javascript
Vue中多个元素、组件的过渡及列表过渡的方法示例
2019/02/13 Javascript
[05:02][DOTA2]DOTA进化论 第一期
2013/09/27 DOTA
简单介绍Python中利用生成器实现的并发编程
2015/05/04 Python
在Python程序中操作文件之flush()方法的使用教程
2015/05/24 Python
PyQt5 加载图片和文本文件的实例
2019/06/14 Python
Pytorch 实现计算分类器准确率(总分类及子分类)
2020/01/18 Python
python连接PostgreSQL过程解析
2020/02/09 Python
Python selenium 加载并保存QQ群成员,去除其群主、管理员信息的示例代码
2020/05/28 Python
python中用ctypes模拟点击的实例讲解
2020/11/26 Python
Python中的面向接口编程示例详解
2021/01/17 Python
整理HTML5移动端开发的常用触摸事件
2016/04/15 HTML / CSS
美国最大的万圣节服装网站:HalloweenCostumes.com
2017/10/12 全球购物
名词解释WEB SERVICE,SOAP,UDDI,WSDL,JAXP,JAXM;JSWDL开发包的介绍。
2012/10/27 面试题
成人教育自我鉴定
2013/11/01 职场文书
新闻编辑专业毕业自荐书范文
2014/02/05 职场文书
党员公开承诺书和承诺事项
2014/03/25 职场文书
班风学风建设方案
2014/05/06 职场文书
实习证明模板
2015/06/16 职场文书
《蜜蜂引路》教学反思
2016/02/22 职场文书