1、背景
笔者所负责的一个线上系统在最近产生了一个难以排查的故障,该故障导致系统每隔几天就必须重启才能够恢复正常,每次重启的间隔时间不等,有时候三四天就得重启一次,有时候七八天才需要重启一次,如若不重启,那系统就无法使用,包括登录验证码也无法刷出。
为了排查这个问题,集结了部门专业运维以及小组架构众多一起拉应用dump进行分析,分析过程中只发现应用线程数微高,但仍在可接受范围内,甚至没有发现存在堆栈溢出及报错情况,该问题也不了了之,每次出现假死现象时暂时也只有通过重启应用解决问题,因此排查该问题的重担就落到笔者身上。
2、故障解析
至于为什么知道是由redis引起的系统假死,那是因为某次系统又再次面临假死问题时,笔者发现有的接口能够正常访问,而有的接口却拿不到响应,顺着这条线索,在对多次系统假死时现象的分析后得出这两类接口的差异之处,就是一类接口存在redis操作,而另一类接口不存在redis操作。
2.1 连接池资源无法归还
至此,得出了系统是因redis而引起的假死,首先我们排除了redis服务端的问题,因为这套redis被多个应用所使用,而单单笔者所负责的系统存在这个问题,因此问题在于系统之内,排除业务代码引起的假死,那问题就只在于所使用的redis框架本身。
我们使用的是spring-data-redis-2.1.5.RELEASE的集成环境,底层使用的是2.9.1版本的jedis框架,分析Jedis源码可以看出当有用户请求操作redis时,由于笔者所负责系统使用的是redis哨兵集群,因而首先通过下图中的getResource()方法获取到一个Jedis连接对象,并将具体的Pool对象给予Jedis连接实例,以供后续redis操作完成后进行连接池资源的归还等。
分析完是如何从连接池获取jedis连接池实例后,下一步我们再来看看连接池资源的归还,下图即是连接池资源归还过程的主干源码,此处的this.dataSource便是上文提及的给予Jedis连接实例的Pool对象,此处是先进行连接池资源的归还,再将该redis连接实例的dataSource实例置空,那在高并发请求的环境下就会存在这么一个问题,当某个Jedis连接实例刚把资源归还给连接池后,就有下一个用户端请求获取到了该Jedis连接实例,而当前线程又恰好将该Jedis连接实例的dataSource置空,那就会导致下一个用户端请求在进行redis操作完成后,无法进行连接池资源的归还,以至于出现连接池连接数被打满的假象,导致无连接可用。
2.2 请求无法得到响应
我们再来回顾上文提到的连接实例获取的过程,Jedis实例最终是通过Pool类的getResource()方法来获取的。
在进行borrowObject时,需要传入一个时间参数,该时间默认情况下为-1,即永远阻塞等待,当连接池资源耗尽时,takeFirst拿到的连接实例状态都是非空闲的,那这个时候p永远为null,也就导致客户端请求永远阻塞等待获取可用连接。
3、解决方案
解决该问题的有效方案有两种,其一是升级Jedis框架,其二是使用其他redis框架,如lettuce、redission等,此处小编受限于历史业务,选择了方案一。
方案一升级Jedis版本后再阅读源码可以发现,close方法先置空Jedis连接实例,再进行连接池资源的归还,对高并发环境下多个客户端持有相同Jedis实例时的同步操作可能导致的问题进行了防治。