likes
comments
collection
share

【踩坑日记】我的线程池怎么突然不够用了?

作者站长头像
站长
· 阅读数 27

故事背景

在那一个阴雨连绵好几天的南方5月终于迎来了晴天,心心念念的准点下班看看久违的夕阳

【踩坑日记】我的线程池怎么突然不够用了?

就在那千钧一发收拾下班之际

【踩坑日记】我的线程池怎么突然不够用了? 机器人告警来了!

【踩坑日记】我的线程池怎么突然不够用了?

问题排查

本着还有20分钟,速战速决的想法,放下手上沾满咖啡渍的杯子,立刻ssh上了应用服务器,tail了下应用的日志,发现数据预处理的本地队列莫名其妙的满了

这时候自信的我怀疑起了上游服务,马上iftop了下网卡流量,这时的网卡流量依旧保持在历史平均值

【踩坑日记】我的线程池怎么突然不够用了?

这时候的我慌了,下午刚进行的发版,应该是代码出现了问题,可是预处理数据这一块代码并未改动,只是修正了他计算不生效的bug,难倒之前的大兄弟在这埋下了坑?

【踩坑日记】我的线程池怎么突然不够用了?

马上先回滚应用,将本地环境切换到生产分支,想着本地复现这个问题,问题来了,本地怎么样也复现不了,除了写数据的目的地不一样,其他的都一模一样

本地预处理队列的消费逻辑是写队列的时候同时提交一个消费任务到线程池中排队,队列满的原因是消费不及时,消费不及时的原因应该出现在线程池上,本地复现不了无法debug,只能曲线救国了,继承线程池类,重写execute方法,在提交任务时增加线程池监控

线程池提供了个获取当前线程池的方法

/**
 * Return the underlying ThreadPoolExecutor for native access.
 * @return the underlying ThreadPoolExecutor (never {@code null})
 * @throws IllegalStateException if the ThreadPoolTaskExecutor hasn't been initialized yet
 */
public ThreadPoolExecutor getThreadPoolExecutor() throws IllegalStateException {
    Assert.state(this.threadPoolExecutor != null, "ThreadPoolTaskExecutor not initialized");
    return this.threadPoolExecutor;
}

拿到线程池类后,我们可以通过他的这些方法拿到当前线程池的信息

  • getThreadNamePrefix 获取线程池名词前缀
  • getTaskCount 获取任务总数
  • getCompletedTaskCount 获取已经完成的任务总数
  • getActiveCount 获取活跃的线程数
  • getQueue 获取当前线程池的排队任务

所以重写了execute方法如下

@Override
public void execute(Runnable task) {
    ThreadPoolExecutor pool = getThreadPoolExecutor();
    logger.info("PoolName{}-{},任务总数{},已完成{},活跃{},排队{}",
            this.getThreadNamePrefix(),
            "xxx",
            pool.getTaskCount(),
            pool.getCompletedTaskCount(),
            pool.getActiveCount(),
            pool.getQueue().size());
    super.execute(task);
}

发布到线上后,发现运行着运行着CompletedTaskCount就不再增长了,ActiveCount也达到了上限,Queue开始疯狂增长

问题知道了,可能是提交到这里的线程出现了问题,毕竟前面使用的的本地队列是LinkedBlockingQueue,不会是这里出现了资源竞争排队了吧?

锁定目标

揣着这个疑问,远程上应用宿主机执行了jstack -l [PID] > /serverPath/applicationStack.log 把当前应用的线程信息记录下来

【踩坑日记】我的线程池怎么突然不够用了?

然后马上对文件进行分析,检索线程池名称观察所有的线程是否有有Blocked或者Waiting状态

【踩坑日记】我的线程池怎么突然不够用了?

然而此时并没有发现阻塞的情况,全是RUNNABLE状态,那问题应该就是Runnable状态的线程在持续的干活未能完成任务

抱着这个问题来看看这些Runnable的线程,果然找到了端倪

【踩坑日记】我的线程池怎么突然不够用了?

这些Runnable的线程都在干一个事,那就是从pg链接中查数据,长时间的操作导致线程一直在占用着,后续的任务只能排队,队列满了就根据拒绝策略进行处理了。

解决

【踩坑日记】我的线程池怎么突然不够用了?

随后对这个慢查询进行了优化,把查询利用上了分片键,让sql执行不再查全表,并让查询到的数据合理的缓存在本地缓存和全局缓存上,至此问题解决。

【踩坑日记】我的线程池怎么突然不够用了?

复盘

当我们在异步线程中去处理繁杂的业务时,要注意给定任务超时(JDK9+可以使用CompletableFuture,JDK8可参照其源码进行实现)和对应的兜底保障,避免整个线程池都在处理同一个会产生线程阻塞的数据从而导致线程池无法提供服务

转载自:https://juejin.cn/post/7374055681890943011
评论
请登录