记一次找因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里的"老六"数据结构,专治各种不服
-
大家好,我是你们的码农段子手,今天要给大家讲一个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应用开发中,使用多线程编程有很多好处,比如可以同时处理多个任务,提高程序的并发性;可以充分利用计算机的多核处理器,使得程序能够更好地利用计算机的资源,...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)