设为首页 加入收藏

TOP

线程池shutdown引发TimeoutException(一)
2023-08-06 07:50:02 】 浏览:91
Tags:程池 shutdown 引发 TimeoutException

问题描述

分享一个发版过程服务报错问题,问题出现在每次发版,服务准备下线的时候,报错的位置是在将任务submit提交给线程池,使用Future.get()引发的TimeoutException,错误日志会打印下面的"error"。伪代码如下:

List<Future<Result<List<InfoVO>>>> futures = new ArrayList<>();
lists.forEach(item -> {	
	futures.add(enhanceExecutor.submit(() -> feignClient.getTimeList(ids)));	
);
futures.forEach(
	item -> {
		try {
			Result<List<InfoVO>> result = item.get(10, TimeUnit.SECONDS);			
		} catch (InterruptedException | TimeoutException | ExecutionException e) {
			log.error("error", e);
	    }
	}
);

代码逻辑非常简单,就是将一个Feign接口的调用提交给线程池去并发执行,最终通过Feture.get()同步获取结果,最多等待10s。
线程池的配置参数是:核心线程数为16,最大线程数为32,队列为100,解决策略为CallerRunsPolicy,意为当线程无法处理任务时,任务交还给调用线程执行。

问题分析

问题分析的开始走了一些弯路,因为Timeout异常给人最直观的感受就是接口超时了,加上这个接口也确实偶尔超时,所以我们用arthas分析了一下接口执行时间,发现接口并不慢,结合上面的线程池参数,基本不会出现超时。同时通过grafana上的监控,分析接口的qps和执行时间,基本可以排除是接口超时这一点。

后来开始怀疑是不是对方服务也在下线,因为我们几个服务多数时候会一起更新,从而导致Feign出现异常,还使用了resilience4j,它里面也有超时和线程池,会不会是它在这种场景下出现问题导致。
这里又绕了一个圈,通过各种google,github,chatgpt后,没有发现相关资料。这后来也给我一个警示就是,在怀疑相关组件之前,要先排查完自己的代码,没有头绪时不要一下子钻进去。

后来结合日志的时间线,重新梳理。上面的线程池是我们自己封装的线程池,支持监控、apollo动态修改线程池参数,日志跟踪traceId打印,执行任务统计,服务下线线程退出等功能,这很像美团技术团队提到的线程池,不过我们基于自己的需求进行封装,使用起来更简单、轻量。
服务优雅下线这篇,我们写到

在服务下线前该线程池会响应一个event bus消息,然后执行线程池的shutdown方法,本意是服务下线时,线程池不再接收新的任务,并触发拒绝策略。那会不会是这里出现问题呢?
结合上面的代码,当线程池shutdown后,执行CallerRunsPolicy策略,再submit应该就会阻塞。这就是我们平时理解的,当队列满了,就继续开启线程至maximumPoolSize,如果线程数已经达到maximumPoolSize,并且队列也满了,此时就触发解决策略。
如下代码,当第三次submit的时候就阻塞了,符合上面说的情况。

	    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.CallerRunsPolicy());
		threadPoolExecutor.submit(() -> {
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
			}
		});
		threadPoolExecutor.submit(() -> {
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
			}
		});
		//到这里就阻塞了
		threadPoolExecutor.submit(() -> {
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
			}
		});

那如何期间shutdown了呢?按照网上的很多介绍,如果线程池shutdown了,再提交任务,就触发拒绝策略。这句话本身没有错,但也没有完全对,坑就在这里。 如果你执行下面的代码,会发现和上面是不一样的,第三个submit不会阻塞了。

	    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.CallerRunsPolicy());
		threadPoolExecutor.submit(() -> {
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
			}
		});
		threadPoolExecutor.submit(() -> {
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
			}
		});
        
        	//加了这一行
        	threadPoolExecutor.shutdown();

		//这里不会阻塞了...
		threadPoolExecutor.submit(() -> {
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
			}
		});

为什么会这样呢,我们跟踪下源码,发现它确实会走到拒绝策略,但在CallerRunsPolicy拒绝策略里面有一个判断,如果线程池不是shutdown的,就直接调用Runnable的run方法,这里使用的是调用者线程,所以调用者线程会阻塞,如果线程池是shutdown的,就什么也不做,相当于任务丢弃了。

按照这个说法,如果我在最后使用Future接收一下submit的返回值,然后调用Future.get方法,会发生什么?

	    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(1), new ThreadPoolExecut
首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇keycloak~MFA多因子认证 下一篇我真的不想再用mybatis和其衍生框..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目