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 08 Redis
浅谈redis缓存在项目中的使用
May 20 Redis
深入理解redis中multi与pipeline
Jun 02 Redis
浅析Redis Sentinel 与 Redis Cluster
Jun 24 Redis
使用Redis实现实时排行榜功能
Jul 02 Redis
Redis字典实现、Hash键冲突及渐进式rehash详解
Sep 04 Redis
使用redis生成唯一编号及原理示例详解
Sep 15 Redis
基于Redis结合SpringBoot的秒杀案例详解
Oct 05 Redis
Redis模仿手机验证码发送的实现示例
Nov 02 Redis
使用RedisTemplat实现简单的分布式锁
Nov 20 Redis
Redis安装使用RedisJSON模块的方法
Mar 23 Redis
Redis基本数据类型List常用操作命令
Jun 01 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中配置文件操作 如config.php文件的读取修改等操作
2012/07/07 PHP
thinkphp在低版本Nginx 下支持PATHINFO的方法分享
2016/05/27 PHP
如何运行/调试你的PHP代码
2020/10/23 PHP
一些常用的JavaScript函数(json)附详细说明
2011/05/25 Javascript
jquery自定义属性(类型/属性值)
2013/05/21 Javascript
如何在Linux上安装Node.js
2016/04/01 Javascript
打造自己的jQuery插件入门教程
2016/09/23 Javascript
详解jQuery中的DOM操作
2016/12/23 Javascript
使用nodejs下载风景壁纸
2017/02/05 NodeJs
原生js实现可拖拽效果
2017/02/28 Javascript
Vue动态实现评分效果
2017/05/24 Javascript
JavaScript类的继承方法小结【组合继承分析】
2018/07/11 Javascript
解决Vue.js由于延时显示了{{message}}引用界面的问题
2018/08/25 Javascript
layui将table转化表单显示的方法(即table.render转为表单展示)
2019/09/24 Javascript
extjs4图表绘制之折线图实现方法分析
2020/03/06 Javascript
微信小程序实现简单购物车功能
2020/12/30 Javascript
vue 使用饿了么UI仿写teambition的筛选功能
2021/03/01 Vue.js
[02:25]专访DOTA2负责人Erik 国际邀请赛暂不会离开西雅
2014/07/21 DOTA
Python扫描IP段查看指定端口是否开放的方法
2015/06/09 Python
python模拟Django框架实例
2016/05/17 Python
python 字典(dict)按键和值排序
2016/06/28 Python
Python入门_条件控制(详解)
2017/05/16 Python
Python实现重建二叉树的三种方法详解
2018/06/23 Python
Python3 实现文件批量重命名示例代码
2019/06/03 Python
windows上安装python3教程以及环境变量配置详解
2019/07/18 Python
Django用户认证系统 Web请求中的认证解析
2019/08/02 Python
python运用sklearn实现KNN分类算法
2019/10/16 Python
Python实现读取并写入Excel文件过程解析
2020/05/27 Python
python如何输出反斜杠
2020/06/18 Python
基于Python采集爬取微信公众号历史数据
2020/11/27 Python
美国派对用品及装饰品网上商店:Shindigz
2016/07/30 全球购物
卡拉威高尔夫官方网站:Callaway Golf
2020/09/16 全球购物
东方红海科技面试题软件测试方面
2012/02/08 面试题
家长会主持词开场白
2015/05/29 职场文书
新入职员工工作总结
2015/10/15 职场文书
python blinker 信号库
2022/05/04 Python