在高并发和异步场景下,线程池的使用可以说是无处不在。线程池本质上就是用空间换取时间,因为线程的创建和销毁都是消耗资源和时间的。对于使用大量线程的场景,使用池管理可以延缓线程的销毁,大大提高单线程的性能。多路复用能力进一步提高了整体性能。今天遇到一个典型的线上问题,正好和线程池有关。另外涉及到死锁、jstack命令的使用、不同JDK线程池的适用场景等知识点。同时,整个调查思路可供借鉴。特此记录分享。一度。01业务背景描述广告系统核心计费服务出现上线问题。首先简单说明一下大概的业务流程,方便理解问题。绿色框内的部分是广告召回扣除过程中扣除服务的位置。简单理解:当用户点击广告时,会从C端发起实时扣费请求(CPC,点击扣费模式)。收费服务承担了动作的核心业务逻辑:包括执行反作弊策略、创建扣费记录、点击日志嵌入点等。02问题现象及业务影响12月2日晚上11点左右,我们收到了一个在线告警通知:计费服务线程池任务队列大小远超设定阈值,队列大小随时间持续减小。不断变大。详细的告警内容如下:相应的,我们的广告指标:点击量、收益等也出现了非常明显的下滑,几乎同时发出了业务告警通知。其中,点击量指标对应的曲线如下:在线故障发生在流量高峰期,持续近30分钟后恢复正常。03问题调查及事故处理流程下面详细介绍整个事故调查分析流程。第一步:收到线程池任务队列的告警后,我们立即查看了扣服服务各个维度的实时数据:包括服务调用、超时、错误日志、JVM监控等,没有发现异常。Step2:然后进一步查看扣款服务依赖的存储资源(mysql、redis、mq)和外部服务,发现在事故发生的过程中存在大量慢速数据库查询。上面的慢查询来自于事故中刚刚启动的一个大数据抽取任务,从推演服务的mysql数据库中并发抽取大量数据到hive表中。因为推演过程还涉及写入mysql,所以猜测此时mysql的所有读写性能都会受到影响,进一步发现insert操作的耗时比正常时期要长很多.Step3:我们猜测是数据库慢查询影响了推演过程的性能,导致任务队列积压,所以我们决定立即暂停大数据抽取任务。但是很奇怪:停止抽取任务后,数据库的插入性能恢复到正常水平,但是阻塞队列的大小继续增加,告警并没有消失。第四步:考虑到广告收入还在大幅下滑,进一步分析代码需要很长时间,所以决定马上重启服务看看有没有效果。为了保留事故现场,我们保留了一台服务器,没有重启,但将这台机器从服务管理平台中移除,使其不再接收新的扣费请求。果然,重启服务这个杀手级功能非常有效。各项业务指标恢复正常,告警也没有再出现。至此,整个线上故障已经解决,历时30分钟左右。04问题根源分析过程下面详细说明事故根源分析过程。第一步:第二天上班后,我们猜测是保存事故现场的服务器,队列中积压的任务应该是线程池处理的,所以再次尝试挂载这台服务器来验证我们的猜测,结果完全出乎意料,积压的任务还在,随着新的请求进来,立马又出现了系统告警,于是服务器立刻又被下线了。第2步:线程池中累积的上千个任务,一晚上都没有被线程池处理完。我们猜测应该是出现了死锁的情况。所以打算使用jstack命令转储线程快照,以供详细分析。#找到扣费服务的进程号$jstackpid>/tmp/stack.txt#通过进程号dump线程快照,输出到文件$jstackpid>/tmp/stack.txt的jstack的日志文件中,我立马发现:使用业务线程池中的所有线程都处于等待状态,线程都卡在了截图红框对应的代码行。这行代码调用了countDownLatch的await()方法,即等待计数器变为0释放共享锁。第三步:找到上面的异常之后,我们就非常接近于找到根本原因了。让我们回到代码继续研究。首先,我们检查了业务代码中使用了newFixedThreadPool线程池,核心线程数设置为25。对于newFixedThreadPool,JDK文档解释如下:创建一个固定线程数的可重用线程池,并在共享的无界队列中运行这些线程。如果在所有线程都处于活动状态时提交新任务,则新任务将在队列中等待,直到有线程可用。关于newFixedThreadPool,核心包括两点:1、最大线程数=核心线程数。当所有核心线程都在处理任务时,新进来的任务会被提交到任务队列等待;2、使用无界队列:提交给线程池的任务队列,大小没有限制。如果任务被阻塞或者处理变慢,那么显然队列会越来越大。因此,进一步的结论是:所有核心线程都死锁了,新的任务被错误地倒入无界队列,导致任务队列不断增加。第四步:死锁的原因是什么,我们回到jstack日志文件中提示的那行代码进一步分析。以下是我简化后的示例代码:.success();}/**扣款任务具体业务逻辑*/publicclassChargeTaskBllimplementsRunnable{publicvoidexecute(ChargeTaskchargeTask){//第一步:参数验证verifyInputParam(chargeTask);//第二步:执行反作弊子任务executeUserSpam(SpamHelper.userConfigs);//第三步:执行扣除handlePay(chargeTask);//其他步骤:点击埋点等...}}/***执行反作弊子任务*/publicvoidexecuteUserSpam(List
