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

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

mhr18 2024-11-02 11:54 31 浏览 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 等众多数据库专栏,欢迎关注学习!

相关推荐

B站收藏视频失效?mybili 收藏夹备份神器完整部署指南

本内容来源于@什么值得买APP,观点仅代表作者本人|作者:羊刀仙很多B站用户都有过类似经历:自己精心收藏的视频突然“消失”,点开一看不是“已被删除”,就是“因UP主设置不可见”。而B站并不会主动通知...

中间件推荐初始化配置

Redis推荐初始化配置bind0.0.0.0protected-modeyesport6379tcp-backlog511timeout300tcp-keepalive300...

Redis中缓存穿透问题与解决方法

缓存穿透问题概述在Redis作为缓存使用时,缓存穿透是常见问题。正常查询流程是先从Redis缓存获取数据,若有则直接使用;若没有则去数据库查询,查到后存入缓存。但当请求的数据在缓存和数据库中都...

后端开发必看!Redis 哨兵机制如何保障系统高可用?

你是否曾在项目中遇到过Redis主服务器突然宕机,导致整个业务系统出现数据读取异常、响应延迟甚至服务中断的情况?面对这样的突发状况,作为互联网大厂的后端开发人员,如何快速恢复服务、保障系统的高可用...

Redis合集-大Key处理建议

以下是Redis大Key问题的全流程解决方案,涵盖检测、处理、优化及预防策略,结合代码示例和最佳实践:一、大Key的定义与风险1.大Key判定标准数据类型大Key阈值风险场景S...

深入解析跳跃表:Redis里的&quot;老六&quot;数据结构,专治各种不服

大家好,我是你们的码农段子手,今天要给大家讲一个Redis世界里最会"跳科目三"的数据结构——跳跃表(SkipList)。这货表面上是个青铜,实际上是个王者,连红黑树见了都要喊声大哥。...

Redis 中 AOF 持久化技术原理全解析,看完你就懂了!

你在使用Redis的过程中,有没有担心过数据丢失的问题?尤其是在服务器突然宕机、意外断电等情况发生时,那些还没来得及持久化的数据,是不是让你夜不能寐?别担心,Redis的AOF持久化技术就是...

Redis合集-必备的几款运维工具

Redis在应用Redis时,经常会面临的运维工作,包括Redis的运行状态监控,数据迁移,主从集群、切片集群的部署和运维。接下来,从这三个方面,介绍一些工具。先来学习下监控Redis实时...

别再纠结线程池大小 + 线程数量了,没有固定公式的!

我们在百度上能很轻易地搜索到以下线程池设置大小的理论:在一台服务器上我们按照以下设置CPU密集型的程序-核心数+1I/O密集型的程序-核心数*2你不会真的按照这个理论来设置线程池的...

网络编程—IO多路复用详解

假如你想了解IO多路复用,那本文或许可以帮助你本文的最大目的就是想要把select、epoll在执行过程中干了什么叙述出来,所以具体的代码不会涉及,毕竟不同语言的接口有所区别。基础知识IO多路复用涉及...

5分钟学会C/C++多线程编程进程和线程

前言对线程有基本的理解简单的C++面向过程编程能力创造单个简单的线程。创造单个带参数的线程。如何等待线程结束。创造多个线程,并使用互斥量来防止资源抢占。会使用之后,直接跳到“汇总”,复制模板来用就行...

尽情阅读,技术进阶,详解mmap的原理

1.一句话概括mmapmmap的作用,在应用这一层,是让你把文件的某一段,当作内存一样来访问。将文件映射到物理内存,将进程虚拟空间映射到那块内存。这样,进程不仅能像访问内存一样读写文件,多个进程...

C++11多线程知识点总结

一、多线程的基本概念1、进程与线程的区别和联系进程:进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程;线程:是运行中的实际的任务执行者。可以说,进程中包含了多...

微服务高可用的2个关键技巧,你一定用得上

概述上一篇文章讲了一个朋友公司使用SpringCloud架构遇到问题的一个真实案例,虽然不是什么大的技术问题,但如果对一些东西理解的不深刻,还真会犯一些错误。这篇文章我们来聊聊在微服务架构中,到底如...

Java线程间如何共享与传递数据

1、背景在日常SpringBoot应用或者Java应用开发中,使用多线程编程有很多好处,比如可以同时处理多个任务,提高程序的并发性;可以充分利用计算机的多核处理器,使得程序能够更好地利用计算机的资源,...

取消回复欢迎 发表评论: