原文地址:http://www.linzichen.cn/article/1575774767477161984
有次在打开自己网站的时候,发现请求一直在转圈圈,没有页面响应。第一反应是服务器被攻击了?
服务器状态
但随即就否定了这个可能,正经大佬谁会闲着没事找这个小站的麻烦。如果是学网络的同学拿着本站练手还有可能,但是本站知道的人寥寥无几,平时更是无人踏足,也不太可能。于是纠结中,还是打开了服务器的后台,发现各项指标一切正常,也就不存在被黑的情况。
Nginx状态
网站域名是通过nginx转发到后台服务的,随后想到了是不是nginx 的问题,于是查看了下nginx 的状态,发现nginx 也是正常的,且网站的静态资源可以正常访问。看了下log,果然发现存在 error 日志,打开看下,异常信息为:
upstream timed out (110: Connection timed out) while reading response header from upstream
很明显,请求上游服务器超时,那上游服务器可不就是后台应用嘛,大致定位到问题源头了,在程序端。
应用程序排查
查看后台日志,发现接口可以正常进来,但是程序走到某个地方停住了,且线程已经在这儿堆积了几十个在等待了。
首先觉得不可能是并发导致的,因为并发再高,只要程序是正常的,即使没有做熔断降级处理,也不至于一直阻塞在同一个地方。且每次请求线程都是依次递增阻塞,说明之前的线程压根就没有释放。很明显是程序在某个地方被锁住了。
锁排查
分析到锁了,首先想到的 是不是与其他服务的连接被阻塞了,导致一直获取不到连接,且没有设置超时时间,所以程序就一直锁在某个地方。心里觉得不太可能,首先不存在并发,且日志里没有 连接超时相关的打印。但处于不确定因素,还是对程序里连接的其他服务挨个排查了下。
es排查
网站首页的列表数据是查的 es,会不会是 es 阻塞了,但是日志里很明显有 es 响应的数据返回(201行有日志输出),所以不是es 的问题。
mysql 排查
项目里并没有复杂的业务,且读多写少,不会存在数据库层死锁的情况。出于严谨,还是查看了下数据库的连接数,发现也没有问题。
redis 排查
用第三方工具连接了redis,发现也可以正常查询,说明也不是因为redis阻塞的
Jstack 排查
百思不得其解,看了半天代码,实在没有发现哪里存在问题,于是打算用 jstack 分析下堆栈信息。
1、获取java进程的pid
[xxx@VM-16-3-centos /]# ps -ef | grep java
xxx 3932 25457 0 21:50 pts/0 00:00:00 grep --color=auto java
xxx 20472 1 1 20:43 ? 00:01:11 java -jar xxx.jar
获取到 程序 pid 为 20472
2、查看进程里占用资源最多的线程
虽然发生了死锁,但是查看 cpu 的资源占用的并不多,找到进程中占用资源最多的线程的 pid 20586
。
3、将pid转16进制
[xxx@VM-16-3-centos /]# printf "%x\n" 20586
506a
得到 506a
4、jstack查询
根据堆栈信息,发现 CompletableFuture 中的线程都处在 WAITING(parking)
中,定位到图中异常处代码的地方,发现在 CompletableFuture.allOf().get()
处发生了阻塞。
CompletableFuture.allOf(categoryFuture, hotArticleFuture, websiteInfoFuture).get() ;
排查到此,大致明白了阻塞的原因。
阻塞原因
在接口中,除了查询文章列表信息外,还会将网站的公共数据也查询出来,由于这些数据之间是不存在前后查询顺序的,所以为了提高接口的响应速度。我在代码里 通过 CompletableFuture 做了几个异步查询。
// 查分类信息
CompletableFuture<Void> categoryFuture = CompletableFuture.runAsync(() -> {
List<CategoryVo> categoryList = categoryService.selectCategoryList();
commonData.setCategoryList(categoryList);
}, threadPoolExecutor);
// 查热门文章
CompletableFuture<Void> hotArticleFuture = CompletableFuture.runAsync(() -> {
List<CommonData.HotArticle> hotArticleList = selectHotArticle();
commonData.setHotArticleList(hotArticleList);
}, threadPoolExecutor);
// 查网站信息
CompletableFuture<Void> websiteInfoFuture = CompletableFuture.runAsync(() -> {
CommonData.WebsiteInfo websiteInfo = selectWebsiteInfo();
commonData.setWebsiteInfo(websiteInfo);
}, threadPoolExecutor);
这些异步任务的线程是来自同一个线程池 threadPoolExecutor
。但代码仅仅是这样,还不至于产生阻塞的情况,真正阻塞的是,我在每一个异步任务的 service 方法中,又细分了多个子异步任务,且它们都公用的一个线程池。
比如在查询网站信息 selectWebsiteInfo()
方法中,里面又细分为查询文章数目
、评论数目
、运行天数
等任务,还有一些其他的日志记录等代码 都是通过异步去实现的。
而以上这个情况,如果线程池设置的核心线程数
不多,那么就很容易造成一种现象:
父任务在等待子任务结束,而子任务又在等待父任务释放资源。所以就造成了死锁状态。
验证
为了验证上述结论是否正确,通过一个demo 复现一下场景。
public class ThreadPoolTest {
public static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
3,50,5, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
) ;
public static void main(String[] args) throws Exception {
List<CompletableFuture<Void>> list = new ArrayList<>(0) ;
for(int i = 0 ; i < 3 ; i++) {
// 2、线程池中就3个空闲线程,因为做了 sleep,所以 3个资源都给了父任务
CompletableFuture<Void> parentTask = CompletableFuture.runAsync(() -> {
try {
System.out.println("父任务执行了:" + Thread.currentThread().getName());
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
// 3、子任务在等待父任务释放资源,父任务在等待子任务执行完,死锁
CompletableFuture<Void> childTask = CompletableFuture.runAsync(() -> {
System.out.println("子任务执行了:" + Thread.currentThread().getName());
}, threadPoolExecutor);
childTask.join() ;
}, threadPoolExecutor);
list.add(parentTask) ;
}
// 1、开始创建3个异步任务并执行
CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).get();
threadPoolExecutor.shutdown();
System.out.println("exit");
}
}
输出结果是阻塞的,接下来再根据 jstack 分析下原因:
根据 jps
,找到类的 pid:
根据 jstack 15769
,找到线程阻塞原因:
根据日志,发现线程代码阻塞在第33
行,而 33行 正式子任务获取线程执行的地方。
由上可知,问题产生的原因确实是因为线程池获取连接导致死锁阻塞了。
解决方案
1、提高线程池的核心线程数,但是此方法治标不治本。
2、嵌套线程之间最好不用同一个线程池,做线程池隔离,避免死锁问题。
总结
1、不建议直接使用CompletableFuture的get()方法,而是使用future.get(5, TimeUnit.SECONDS);方法指定超时时间。
2、在使用CompletableFuture的时候线程池拒绝策略最好使用AbortPolicy,如果线程池满了直接抛出异常中断主线程,达到快速失败的效果。
3、耗时的异步线程和CompletableFuture的线程做线程池隔离,让耗时操作不影响主线程的执行。