图片来自抱图网。老板直接批评我,说我也是连带责任。我带领的团队素质不达标,造成了一次重量级的生产事故。幸运的是,没有造成财产损失!首先说一下问题现象:内网沙盒环境API卡死1周,所有API无响应。一开始测试抱怨环境响应慢,我们重启应用,应用恢复正常,所以没有处理。但是后来,出问题的频率越来越频繁,越来越多的同事开始抱怨,于是觉得代码可能有问题,开始排查。首先发现是本地开发的IDE没有发现问题。应用卡住的时候,数据库和Redis都是正常的,也没有特别的错误日志。开始怀疑是沙盒环境机器的问题(测试环境本身就很脆弱)。于是用ssh登陆服务器,执行如下命令:这时候发现机器还是正常的,但是心里还是??,于是打算看看JVM栈信息。我们先来看看问题应用的资源消耗线程。执行top-H-p12798,如下图:找到前3个比较耗资源的线程:jstack查看堆内存。jstack12798|grep12799十六进制31ff。没看出有什么问题,还看了最上面和后面的10行。然后执行:看到部分线程处于锁状态。但是没有业务相关的代码,忽略。这时,我毫无头绪。想想看。决定放弃卡机。为了保护事故现场,先将问题进程的堆内存全部dump出来,然后以debug模式重启测试环境应用,当问题再次出现时直接远程调试问题机器。第二天问题又出现了,于是通知运维Nginx将问题应用转发下架,自己远程调试Tomcat。随便找了一个接口,断点就在接口的入口处。悲剧开始了,什么都没有发生!API等待服务响应,但未命中断点。这个时候有点懵,先冷静一下,在入口前的AOP处下断点,再次调试,这次进入了断点。F8N次后,发现在执行Redis命令的时候卡住了。继续跟踪,终于在jedis的一个地方发现了问题:);}Jedisjedis=newJedis(getShardInfo());//强制初始化(seeJedisissue#82)jedis.connect();returnjedis;}catch(Exceptionex){thrownewRedisConnectionFailureException("CannotgetJedisconnection",ex);}}在池之上。在getResource()之后线程开始等待。publicTgetResource(){try{returninternalPool.borrowObject();}catch(Exceptione){thrownewJedisConnectionException("无法从池中获取资源",e);}}returninternalPool.borrowObject();此代码应为租赁代码。连接跟踪:publicTborrowObject(longborrowMaxWaitMillis)throwsException{this.assertOpen();AbandonedConfigac=this.abandonedConfig;if(ac!=null&&ac.getRemoveAbandonedOnBorrow()&&this.getNumIdle()<2&&this.getNumActive()>this.getMaxTotal()-3){this.removeAbandoned(ac);}PooledObjectp=null;booleanblockWhenExhausted=this.getBlockWhenExhausted();longwaitTime=0L;while(p==null){booleancreate=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){thrownewNoSuchElementException("Timeoutwaitingforidleobject");}其中有一段代码: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将始终执行,然后一直循环,开始怀疑这个值没有配置。找到Redispool配置,发现MaxWaitMillis确实没有配置。配置后else代码也是Exception,无法解决问题。继续F8:publicEtakeFirst()throwsInterruptedException{this.lock.lock();Objectvar2;try{Objectx;while((x=this.unlinkFirst())==null){this.notEmpty.await();}var2=x;}finally{this.lock.unlock();}returnvar2;}我发现这里有lock这个词,开始怀疑是不是所有的requestAPI都被block了。然后在ssh服务器上重新安装arthas(Arthas是阿里巴巴开源的Java诊断工具),执行thread命令:发现大量http-nio线程处于等待状态,http-nio-8083-exec-这个线程其实就是发送HTTP请求的Tomcat线程。随便找个线程查看堆内存线程-428:这个确认是API常循环的问题,是Redis获取连接的代码导致的。解释这段内存代码,所有线程都在等待@53e5504e对象释放锁。于是jstack全局搜索了53e5504e,没有找到这个对象所在的线程。此后确定问题原因是Redis连接获取的问题。但是,无法获得连接的原因尚未确定。再次执行arthas的thread-b(thread-b找出当前哪些线程正在阻塞其他线程)。没有结果。这和我想的不一样。应该能找到被阻塞的线程,于是看了下这个命令的文档,发现了下面这句话:嗯,我们恰好是后者……让我重新整理一下思路。这次修改Redispool配置,设置连接超时为2s,然后观察问题再次出现时应用做了什么。添加如下配置:JedisConnectionFactoryjedisConnectionFactory=newJedisConnectionFactory();......JedisPoolConfigconfig=newJedisPoolConfig();config.setMaxWaitMillis(2000);......jedisConnectionFactory.afterPropertiesSet();重启服务等待。..改天,又一次。ssh登录服务器,查看Tomcataccesslog,发现大量API请求出现500。org.springframework.data.redis.RedisConnectionFailureException:无法获取Jedisconnection;嵌套异常是redis.clients.jedis.exceptions.JedisConnectionException:无法从池中获取资源g.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:140)atorg.springframework.data。redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:229)atorg.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:57)atorg.springframework.data.redis.core.RedisConnectionUtils。doGetConnection(RedisConnectionUtils.java:128)atorg.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:91)atorg.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:78)atorg。springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:177)atorg.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:152)atorg.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:85)atorg.springframework.data.redis.core.DefaultHashOperations。get(DefaultHashOperations.java:48)找到了第一次出现500的来源,找到了如下代码:......Cursorc=stringRedisTemplate.getConnectionFactory().getConnection().scan(options);while(c.hasNext()){...,,}分析这段代码:stringRedisTemplate.getConnectionFactory().getConnection()在获取到pool中的redisConnection后,没有进行后续操作,也就是说Redis中的链接此时连接池被租用后,不释放也不归还连接池。虽然业务已经处理完毕,redisConnection处于空闲状态,但是池中的redisConnection状态还没有回到空闲状态。正常应该是:问题已经发现了。总结:SpringstringRedisTemplate封装了一些常用的Redis操作,但目前还不支持ScanSetNx等命令。这时候就需要获取jedisConnection来执行一些特殊的Commands。不推荐:stringRedisTemplate.getConnectionFactory().getConnection()我们可以使用下面的代码来执行:或者使用连接后执行:RedisConnectionUtils.releaseConnection(conn,factory);释放连接。同时,在Redis中不推荐使用keys命令。RedisPool的配置要合理设置,否则出现问题时没有错误日志也没有报错,定位难度较大。作者:小木-_-编辑:陶佳龙来源:my.oschina.net/xiaomu0082/blog/2990388