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 相关文章推荐
详解RedisTemplate下Redis分布式锁引发的系列问题
Apr 27 Redis
Windows中Redis安装配置流程并实现远程访问功能
Jun 07 Redis
Redis基于Bitmap实现用户签到功能
Jun 20 Redis
浅谈Redis中的RDB快照
Jun 29 Redis
使用Redis实现实时排行榜功能
Jul 02 Redis
Redis集群的关闭与重启操作
Jul 07 Redis
Redis Cluster 集群搭建你会吗
Aug 04 Redis
redis中lua脚本使用教程
Nov 01 Redis
Redis 哨兵机制及配置实现
Mar 25 Redis
redis复制有可能碰到的问题汇总
Apr 03 Redis
Redis如何实现验证码发送 以及限制每日发送次数
Apr 18 Redis
muduo TcpServer模块源码分析
Apr 26 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使用自定义方法实现数组合并示例
2016/07/07 PHP
Yii2实现同时搜索多个字段的方法
2016/08/10 PHP
PHP实现的曲线统计图表示例
2016/11/10 PHP
script标签属性用type还是language
2015/01/21 Javascript
BootStrap selectpicker
2016/06/20 Javascript
BootStrap Table前台和后台分页对JSON格式的要求
2017/06/28 Javascript
iscroll实现下拉刷新功能
2017/07/18 Javascript
three.js中文文档学习之通过模块导入
2017/11/20 Javascript
vue 实现模糊检索并根据其他字符的首字母顺序排列
2019/09/19 Javascript
Object.keys() 和 Object.getOwnPropertyNames() 的区别详解
2020/05/21 Javascript
[40:56]2018DOTA2亚洲邀请赛 3.31 小组赛 A组 Liquid vs TNC
2018/04/01 DOTA
[02:10]三分钟回顾完美世界城市挑战赛
2019/01/24 DOTA
Python字典操作简明总结
2015/04/13 Python
Python2与python3中 for 循环语句基础与实例分析
2017/11/20 Python
python用户管理系统的实例讲解
2017/12/23 Python
python下解压缩zip文件并删除文件的实例
2018/04/24 Python
numpy实现合并多维矩阵、list的扩展方法
2018/05/08 Python
Python实现的本地文件搜索功能示例【测试可用】
2018/05/30 Python
python生成九宫格图片
2018/11/19 Python
Python 离线工作环境搭建的方法步骤
2019/07/29 Python
基于Python爬取素材网站音频文件
2020/10/21 Python
Python爬虫之Selenium实现键盘事件
2020/12/04 Python
屈臣氏马来西亚官网:Watsons马来西亚
2019/06/15 全球购物
颇特女士香港官网:NET-A-PORTER香港
2021/03/08 全球购物
计算机工程学院个人求职信
2013/10/05 职场文书
半年思想汇报
2013/12/30 职场文书
幼儿园元旦家长感言
2014/02/27 职场文书
邓小平理论心得体会
2014/09/09 职场文书
教师师德师风个人整改方案
2014/09/18 职场文书
国际贸易本科毕业生求职信
2014/09/26 职场文书
2014年党的群众路线学习心得体会
2014/11/05 职场文书
幼师个人总结范文
2015/02/28 职场文书
工作失职自我检讨书
2015/05/05 职场文书
公司员工违法违章行为检讨书
2019/06/24 职场文书
30岁前绝不能错过的10本书
2019/08/08 职场文书
CSS3通过var()和calc()函数实现动画特效
2021/03/30 HTML / CSS