4

一次线程池引发的线上故障分析

 3 years ago
source link: https://tech.youzan.com/-ci-dubboxian-cheng-chi-xian-cheng-hao-jin-fen-xi/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、问题背景

线上监控到大量接口报错,定位到异常机器,将异常机器隔离后,线上服务恢复正常。 拿到业务报错日志如下 Fm3SIGw53R6pntywivFSPmwfjscg.png 异常信息显示Dubbo线程池活跃线程数已经达到最大线程数200,说明线程池资源已经耗尽。

二、问题排查

线程池资源耗尽,猜测Dubbo线程都被某个耗时方法阻塞了,或者线上有异常突发流量。

查看线上监控,发现服务请求流量正常,猜测Dubbo线程是被阻塞住了。

2.1 Dubbo线程为何被阻塞?

通过 jstack 获取Dubbo线程堆栈信息,发现大量Dubbo线程的线程状态都为 WAITING 状态,阻塞在 CompletableFuture#joinFqw5SmQ9cqaqcqLOOgCiQkIsR-66.png 找到相关代码行,简化逻辑如下: FopwPXM2xmJyFyuyXYi4Q-fbcw5_.png

业务代码中自定义了一个 线程数为8固定线程池 executorService ,为了方便表述,该线程池简称 业务线程池 ,分配的线程简称 业务线程

method2 通过从该线程池中获取线程执行多个耗时的子任务,并 join 阻塞等待多个线程执行结束。 Fh48JT_OqLI3zscmN7HbzoKU80sv.png

当接收到一个请求时,由Dubbo线程池分配线程执行 method1 方法, method1 调用 method2method2 从业务线程池中获取线程去执行子任务,并阻塞等待。

Dubbo线程都阻塞在method2,那么说明method2中的多个子任务一直没有执行完成,导致Dubbo线程一直阻塞等待。

那么method2中的子任务为什么一直没有执行完?是因为子任务执行得太慢吗?还是业务线程池出了什么问题?

2.2 子任务为什么一直没执行完成?

分析业务线程堆栈信息,发现8个业务线程都处于 WAITING 状态,阻塞在 CompletableFuture#join 方法。 FkY80sk5gNFDgz5GJy4YIvoJivp1.png 通过业务线程堆栈信息找到相关代码,并将代码简化如下 FusLoS8DAO5NyfRvXiyPMRMbh7h0.pngmethod3 异步调用 method2 ,两个方法都用到了同一个 业务线程池FtgtVTo8q5NlH1qNJU2i801_fwKB.pngmethod3 同时收到8个请求时,8条业务线程都被分配给 method3 去异步调用 method2 后,此时因为业务线程已经达到最大值, method2 中的子任务会进入队列等待被业务线程拉取执行。而此时业务线程又都在 method2 阻塞等待子任务执行完成,两边就陷入了 相互等待 的状态,因此业务线程陷入永久阻塞状态。

2.3 小结

结合以上分析,业务线程池的线程全部都被阻塞住,导致使用该业务线程池的Dubbo线程也全部阻塞。

三、问题解决

在真实的业务代码中其实远非简单的A调B,而是相对比较复杂的调用链 FvZJSaM2bwgILrlCNV86jzprICMF.png

method3 发起异步调用,经过多层中间接口调用到 method2 。其他接口发起同步调用,同样经过多层中间接口调用到 method2

因为经过多层中间接口,所以不能直接将 method2 改成顺序执行多个子任务,会导致其他调用 method2 的接口处理时间延长。抽出一个顺序执行子任务的方法也不太合适,因为涉及到改动多个中间接口,改动相对比较大。

解决方式是隔离线程池,将提交异步任务和多线程执行子任务拆分成两个线程池去处理。

Fofcf3BfLDV7Fvm2jyHDBTEJgaDE.png

拆分成两个线程池之后,无论同时进来多少请求,在 method2 陷入阻塞的都是线程A,不会影响执行子任务的线程B。

将异步调用改成由一个新的线程池提交,这样影响范围就控制在 method3 ,改动也比较小,可以快速修复上线。

四、问题深究

查看代码提交记录,发现该业务是最近改成使用 自定义线程池 ,之前是直接用 CompletableFuture 的默认线程池,并且已经稳定运行了很长时间。

逻辑简化如下: FkVS93qJy_Fs7QJVqhNf4Q2IPXws.png

那么问题来了, CompletableFuture 的默认线程池也是共用的线程池,为什么可以正常执行?

4.1 使用默认线程池可以正常执行吗?

我们来验证一下,测试代码如下: Fv4KUt_WOm3Vry0ACJEX-YLI-S0y.png 截取部分执行结果如下: Fnlsfy7kklT_D5Nisa0MZQ_7OHte.png

如输出结果所示,1000个任务都成功执行完成了,没有出现互相等待陷入阻塞的情况,说明可以正常执行完成。

4.2 为什么默认线程池可以正常执行完成?

CompletableFuture 内部包含两种默认线程池,当 ForkJoinPool#getCommonPoolParallelism() 大于1时 使用 ForkJoinPoolcommonPool 线程池,反之则使用内部类 ThreadPerTaskExecutor 执行任务。

  • ThreadPerTaskExecutor 每次执行都会创建线程,因此不会出现任务等待线程空闲的情况。
  • commonPoolForkJoinPool 内部包含的默认线程池,在没有指定系统参数时 并行数Runtime.getRuntime().availableProcessors()-1availableProcessors() 这个方法的目的是获取可并行执行的线程数,这个数值跟主板集成的cpu个数、cpu核数、是否开启超线程都是相关的。

ForkJoinPool#getCommonPoolParallelism() 获取的就是 commonPool 的并行数,我测试的机器获取到的 commonPool 并行数 为7,因此使用的是 ForkJoinPool 线程池。

ForkJoinPool 线程池之所以可以正常执行,关键在 CompletableFuture#join 中的内部实现。截取 CompletableFuture#join 解决此问题的关键时序图如下

FlFAmQVcbLvo2IWZ9tGBIu3vP4ee.png

关键就在于 ForkJoinPool 创建的线程为 ForkJoinWorkerThread 类型,而 ForkJoinPool#managedBlock 判断当前线程是 ForkJoinWorkerThread 类型时会调用 tryCompensate 方法,该方法在特定情况下会去补偿线程确保任务正常执行完成。

截取 tryCompensate 源码如下: FgRUWhk-YB7dch3QmwJguMBlVTak.png 根据 tryCompensate 的源码可以得出:

  • tryCompensate 在经过一系列校验,认为当前陷入阻塞会导致任务无法正常执行时,会尝试补偿创建一条新的线程,确保不出现上述的互等情况。
  • 普通 FokrJoinPool 线程池最多会补偿到 32767 条线程。 commonPool 最多会补偿到 并行数+256 条工作线程,超过则会抛出异常。

因此 CompletableFuture 线程池可以正常执行是因为使用 ThreadPerTaskExecutor 时每次都会创建新的线程,而使用 commonPool 时,在 CompletableFuture#join 进入阻塞之前会去尝试补偿线程。但是也不是无限补偿,当补偿达到一定次数后就会抛出异常。

4.3 小结

使用 CompletableFuture 的默认线程池之所以不会出现互等的情况,是因为提交任务时,如果内部使用的是 ThreadPerTaskExecutor 是会不断创建新线程的,不会因为进入队列阻塞等待被执行而陷入等待。而如果内部使用的是 commonPoolCompletableFuture#join 方法在进入阻塞之前,判断当前线程是 ForkJoinWorkerThread 线程则会在满足条件时先尝试补偿线程,确保有足够的线程去保证任务可以正常执行。

五、总结与思考

本次问题是父子任务都从同一个固定线程池中获取线程,并且父任务会等待子任务执行完成,在并发情况下触发了相互等待,最终导致线程池资源耗尽,从而影响到使用到该业务线程池的Dubbo请求正常执行。

因此项目中应该避免父子任务共用同一个线程池。通过线程池异步调用某个接口时,如果并不清楚接口的底层逻辑,要考虑底层有没有可能用到当前线程池,做好线程池隔离,避免触发此问题。

欢迎关注我们的公众号

coder_qrcode.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK