为什么要做线程池隔离
比如现在有一个系统需要调用三个业务请求,分别是查询订单、查询商品、查询用户,而且这三个业务请求都依赖第三方服务——订单服务、商品服务、用户服务。三个服务均通过RPC调用。当查询订单服务于时,加入响应持续延迟,这时后续有大量的查询订单请求过来,那么容器中的现成数量会持续增加直至CPU资源耗尽,整个服务对外不可用,在集群环境下就会发生雪崩。从一个订单服务不可用最后演变成整个应用不可用。如果我们给调用订单服务的请求分配一个固定的线程池,用一个线程池隔离其他业务,那么就能够防范这样的事故发生,因为线程的使用数不会超过系统负载阈值。
实现一个线程池隔离
方法一
使用一个大的线程池,固定线程池大小,比如1000.通过权重的思路为某个方法分配一个固定大小的线程数。
比如为某一个方法请求分配了10个线程。此时又有两种形式,一种是最多10个,我们称之为“限制型”,另一种是至少10个,我们称之为“保守型”。通过计数器来实现线程的分配。
限制型代码示例:
boolean flag = true;
if (限制型) { //最多只能有10个线程,count表示当前方法的线程数
if (countincrementAndGet() <= 10) {
if (publicCount.incrementAndGet() > 1000) { //publicCount代表实时总的线程数,参与计数,如果大于1000,则在flag置为false,后续不做处理
count.decrementAndGet();
publicCount.decrementAndGet();
flag = false;
}
} else {//大于10个线程,flag置为flase,后续不做处理
flag = false;
count.decrementAndGet();
}
return flag;
}
保守型代码示例:
boolean flag = true;
if (保守型) { //最少10个线程
if (publicCount.incrementAndGet() > 1000) { //同样要判断,如果实时总的线程数大于1000则后续拒绝处理
publicCount.decrementAndGet();
flag = false;
}
return flag;
}
方法二
在方法一中,严格意义上讲,它并不属于线程池隔离,因为它只有一个公用的线程池,然后大家来瓜分它,不过也达到了隔离的目标。下面要说的方法就是每个方法设置真正的线程池。
我们利用ConcurrentHashMap来存储线程池,key是方法名,值是每个方法对应的一个ThreadPool。当请求到来的时候,我们获取方法名,然后直接从Map对象中取到响应的线程池去处理。
public ConcurrentHashMap<String,ThreadPoolExecutor> chm = new ConcurrentHashMap<String,ThreadPoolExecutor>
这两种方法,线程池的粒度都是在方法上,是不是太细了呢?这就需要结合实际的生产情况,也可以按组划分,比如商品为一组,查询单个商品方法、查询商品列表方法等都规划到这个组里面。
线程池隔离的优点
(1)当前面向用户的应用程序得到保护,线程池隔离后,即使一个业务线程池的线程数使用完,也不会影响其他业务。
(2)当依赖服务恢复正常,比如前面说的订单服务,此时应用程序可以立即恢复。这里需要注意,尽管现成吃提供了现成隔离,应用程序也必须有超时时间的设置,不能无限制的阻塞以致线程池一直饱和。
线程池隔离的缺点
增加了CPU的开销,每个业务在执行的时候,会设计请求排队、调度和上下文的切换。