百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

记一次找因Redis使用不当导致应用卡死bug的过程

mhr18 2024-11-02 11:54 50 浏览 0 评论

作者:小木

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

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

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

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

top

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

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

执行 top -H -p 12798

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

jstack 查看堆内存

jstack?12798?|grep?12799的16进制?31ff


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

看到一些线程都是处于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命令

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

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

thread -428

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

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

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

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

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

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

再次整理下思路。这次修改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状态

正常应为

自此问题已经找到。

总结: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的配置应该合理配上,否则出现问题无错误日志,无报错,定位相当困难。

来源:https://my.oschina.net/xiaomu0082


另:想了解更多 Redis 数据库的知识与用法,欢迎关注墨天轮“Redis 专栏”(地址:https://www.modb.pro/db,点击左下角的“阅读原文”或者扫描下方二维码可直达),此外,墨天轮开放了很多数据库专栏,如 GaussDB、PolarDB、OceanBase、TDSQL、GoldenDB 等众多数据库专栏,欢迎关注学习!

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: