最近小伙伴们找我查的问题里,有两个与线程池相关的,最终都是花了一些时间才揪出原因所在,做一下记录,供以后的自己和其它需要的人参考。
一、异步变同步
现象:
有一个方法,被请求后只是向线程池提交一个任务,然后马上返回,但从日志的 traceId 来看,偶现方法与任务在同一线程执行,接口耗时较长的情况。
分析过程:
这个其实就是一个知识点:当线程池里没有空闲线程,且任务队列已满时,会怎么处理新提交的任务?
可以看下 TheadPoolExecutor 类,这个类里面有几种预定义好的策略(implements RejectedExecutionHandler):
CallerRunsPolicy
AbortPolicy
DiscardPolicy
DiscardOldestPolicy
结合它们的名字以及注释就可以看到,它们分别对应:
调度线程自己执行任务;(有一种例外情况是线程池被 shutdown 了则丢弃任务)
忽略任务,并抛出异常;(默认值)
丢弃任务,不产生异常;
丢弃队列里最老的未被处理的任务,然后重新尝试调度新任务;(例外情况同一)
除此之外,还可以按需自己定义策略。
在我们的场景里,这个线程池使用的 RejectedExecutionHandler 是 CallerRunsPolicy,所以原因就找到了。
解决方案:
因为场景里主要的诉求是这个接口要快速返回,并且不能丢失任务,那这种情况使用消息队列会更加合适,所以将这里的向线程池提交任务,修改为向消息队列发送消息。
二、消失的任务
现象:
从日志可以看到,向线程池里提交了一个任务,找不到该任务执行的记录。
分析过程:
首先是怀疑这个任务被丢弃或者忽略了,经确认,该线程池的 RejectExecutionHandler 是使用的默认的 AbortPolicy,这样的话如果它被忽略,会有异常抛出,但日志里找不到异常记录。
那就是说,它成功进入了任务队列,但是没有被执行,哪里去了呢?
冥思苦想之后,怀疑是不是应用被杀掉了?查看 K8s 控制台里容器的滚动记录,果然在提交任务的时间点附近,应用发过版——破案。
解决方案:
提供两个思路:
在保证任务执行逻辑幂等的前提下,通过消息队列、数据库记录任务状态+重试机制等方式调度任务;
容器优雅下线,确认正在处理的请求和任务都完成后才能被 kill 掉。