线程池存在的坑与实践

1,432 阅读5分钟

案例一 共享线程池,导致次要逻辑拖垮主要逻辑

我一个服务,里面有一个线程池,下单服务会把一些异步请求丢进这个线程池,查询天气等服务也会。

image.png

如图所示,两个服务都会向线程池提交任务,然后等待任务执行完。

这样做有什么问题呢?

如果whether服务的响应时间过慢,会占据大量线程池资源,直接导致没有足够的资源去执行order服务的任务,导致下单失败。由于一个次要服务的抖动引起主要逻辑的失败,这种情况是绝对不允许的,那么也要如何解决这个问题呢?

其实这个问题很简单,做线程池隔离就好了,单独用一个线程池处理order请求,一个线程池处理whether请求,这样就能做到互不影响了。

image.png

案例二 不合理的阻塞队列导致吞吐量断崖式下跌

什么叫断崖式的下跌呢?比如说原来的吞吐量是三位数,出现问题之后直接给你干到了个位数。

特别是io密集型的场景,比较容易出现这个问题。

我有一个请求进来,假设要调用20个接口,假设这20个接口之间没有因果联系,可以直接丢到线程池里面去处理,但是要等到所有请求完成才能够返回,在这种条件下,很可能就会踩到这个坑。

我们把线程池设置成这样:20个核心线程数,100个最大线程数,1024大小的阻塞队列,拒绝策略设置成调度线程处理。

在流量比较低的时候,这时候核心线程+阻塞队列就能够处理请求,但是随着请求量的增大,整体的响应速度会变慢,因为一个请求就会占据所有核心线程,所以会有些请求先放入阻塞队列,不能第一时间执行。

随着请求量的进一步增大,阻塞队列也满了,这个时候会new 线程直到最大线程数,这个时候响应时间是呈现下降趋势的。

到这一步为止,上述的现象是完全符合我们预期的,但是随着请求量的进一步增大,那响应时间可不仅仅是下降这么简单了。

假设我们这时候每秒能够处理200个请求,随着请求量再稍微上升一点点,我们的每秒可能只能处理10个请求了!

为什么会发生这个现象呢?问题就出在这个阻塞队列,我们可以想象一下,当线程池满了之后,我们新来的请求会加入阻塞队列,注意这是一个队列,需要先进先出,如果你这时候有一个子请求放在了队尾呢?你必须等1024个子请求全部执行之后你才有机会去执行,这时候才能返回。

更可怕的是,这种阻塞会让调度线程一起跟着阻塞,比如说tomcat有200个线程,如果全部阻塞在这儿,tomcat就不能对外提供服务了。

实际上真出现这种问题之后,大部分情况下,这种并发调接口的效率是比串行执行还要慢的。

那么这种问题应该如何解决呢?

我们是直接把并行调用降级为串行调用,这样虽然降低了响应速度,但是不会让吞吐量有这种断崖式的下跌,做法很简单,就是把阻塞队列换成SynchronousQueue,在线程数达到最大之后,会直接拒绝策略,让调度线程去执行,实际上就相当于降级成为了串行执行

如果对响应时间比较敏感,可以做限流。

你这时候去看cpu,cpu的利用率这个时候并不高,实际上是没有充分利用服务器资源的。

那么要怎样才能充分利用服务器资源呢?实际上java并没有太好的处理方式,因为java的线程和操作系统线程是一一对应的,创建代价大,你不可能去无限new线程,但是这个的核心问题就在线程不够用。

所以对应这种io密集型的,实际上用java处理就不是很合适了,go是一个不错的选择

案例三 我明明设置了keepalive,为什么线程回收不了

比如我有一个线程池参数设置如下:

核心线程数10,最大线程数20,阻塞队列500,keepalive 10s

我某个服务使用了这个线程池,这是一个线上线程池,时时刻刻都会有流量。

我通过线程池监控发现,由于某些原因,线程池的线程数到达了最大线程数,到了晚上之后,这时候服务负载比较低,但是观察线程池发现,活跃线程数只有2,但是最大线程池一直停留在20,没有会收到10

这个问题其实也比较简单,举个例子,我原来两个人满负荷工作可以完成工作,现在又招了8个人,然后10个人干原来两个人的活,大家都有活干但是大家都比较闲。

线程池也遇到了这种情况,由于是线上服务,时时刻刻都有流量,线程也就没有了真正的空闲时间,导致一直回收不了。

要解决这个问题也很简单,弄个时间窗口,统计线程池是否活跃,在不活跃的时候把keepalive设置成0,隔个几秒再设置回去,这样就能达到回收线程池的目的。