上午刚到公司,准备开始一天的摸鱼之旅时忽然收到了一封监控中心的邮件。java
心中暗道很差,由于监控系统历来不会告诉我应用完美无 bug
,其实系统挺猥琐。数据库
打开邮件一看,果真告知我有一个应用的线程池队列达到阈值触发了报警。缓存
因为这个应用出问题很是影响用户体验;因而立马让运维保留现场 dump
线程和内存同时重启应用,还好重启以后恢复正常。因而开始着手排查问题。安全
首先了解下这个应用大概是作什么的。服务器
简单来讲就是从 MQ
中取出数据而后丢到后面的业务线程池中作具体的业务处理。并发
而报警的队列正好就是这个线程池的队列。运维
跟踪代码发现构建线程池的方式以下:jsp
ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());; put(poolName,executor);
采用的是默认的 LinkedBlockingQueue
并无指定大小(这也是个坑),因而这个队列的默认大小为 Integer.MAX_VALUE
。ide
因为应用已经重启,只能从仅存的线程快照和内存快照进行分析。网站
先利用 MAT
分析了内存,的到了以下报告。
其中有两个比较大的对象,一个就是以前线程池存听任务的 LinkedBlockingQueue
,还有一个则是 HashSet
。
固然其中队列占用了大量的内存,因此优先查看,HashSet
一下子再看。
因为队列的大小给的够大,因此结合目前的状况来看应当是线程池里的任务处理较慢,致使队列的任务越堆越多,至少这是目前能够得出的结论。
再来看看线程的分析,这里利用 fastthread.io 这个网站进行线程分析。
由于从表现来看线程池里的任务迟迟没有执行完毕,因此主要看看它们在干吗。
正好他们都处于 RUNNABLE 状态,同时堆栈以下:
发现正好就是在处理上文提到的 HashSet
,看这个堆栈是在查询 key
是否存在。经过查看 312 行的业务代码确实也是如此。
这里的线程名字也是个坑,让我找了很久。
分析了内存和线程的堆栈以后其实已经大概猜出一些问题了。
这里其实有一个前提忘记讲到:
这个告警是凌晨三点
发出的邮件,但并无电话提醒之类的,因此你们都不知道。
到了早上上班时才发现并当即 dump
了上面的证据。
全部有一个很重要的事实:这几个业务线程在查询 HashSet
的时候运行了 6 7 个小时都没有返回。
经过以前的监控曲线图也能够看出:
操做系统在以前一直处于高负载中,直到咱们早上看到报警重启以后才下降。
同时发现这个应用生产上运行的是 JDK1.7
,因此我初步认为应该是在查询 key 的时候进入了 HashMap
的环形链表致使 CPU
高负载同时也进入了死循环。
为了验证这个问题再次 review 了代码。
整理以后的伪代码以下:
//线程池 private ExecutorService executor; private Set<String> set = new hashSet(); private void execute(){ while(true){ //从 MQ 中获取数据 String key = subMQ(); executor.excute(new Worker(key)) ; } } public class Worker extends Thread{ private String key ; public Worker(String key){ this.key = key; } @Override private void run(){ if(!set.contains(key)){ //数据库查询 if(queryDB(key)){ set.add(key); return; } } //达到某种条件时清空 set if(flag){ set = null ; } } }
大体的流程以下:
Set
。Set
中。这里有一个很明显的问题,那就是做为共享资源的 Set 并无作任何的同步处理。
这里会有多个线程并发的操做,因为 HashSet
其实本质上就是 HashMap
,因此它确定是线程不安全的,因此会出现两个问题:
第一个问题相对于第二个还能接受。
经过上文的内存分析咱们已经知道这个 set 中的数据已经很多了。同时因为初始化时并无指定大小,仅仅只是默认值,因此在大量的并发写入时候会致使频繁的扩容,而在 1.7 的条件下又可能会造成环形链表。
不巧的是代码中也有查询操做(contains()
),观察上文的堆栈状况:
发现是运行在 HashMap
的 465 行,来看看 1.7 中那里具体在作什么:
已经很明显了。这里在遍历链表,同时因为造成了环形链表致使这个 e.next
永远不为空,因此这个循环也不会退出了。
到这里其实已经找到问题了,但还有一个疑问是为何线程池里的任务队列会越堆越多。我第一直觉是任务执行太慢致使的。
仔细查看了代码发现只有一个地方可能会慢:也就是有一个数据库的查询。
把这个 SQL 拿到生产环境执行发现确实不快,查看索引起现都有命中。
但我一看表中的数据发现已经快有 7000W 的数据了。同时通过运维得知 MySQL
那台服务器的 IO
压力也比较大。
因此这个缘由也比较明显了:
因为每消费一条数据都要去查询一次数据库,MySQL 自己压力就比较大,加上数据量也很高因此致使这个 IO 响应较慢,致使整个任务处理的就比较慢了。
但还有一个缘由也不能忽视;因为全部的业务线程在某个时间点都进入了死循环,根本没有执行完任务的机会,然后面的数据还在源源不断的进入,因此这个队列只会越堆越多!
这实际上是一个老应用了,可能会有人问为何以前没出现问题。
这是由于以前数据量都比较少,即便是并发写入也没有出现并发扩容造成环形链表的状况。这段时间业务量的暴增正好把这个隐藏的雷给揪出来了。因此仍是得信墨菲他老人家的话。
至此整个排查结束,而咱们后续的调整措施大概以下:
HashSet
不是线程安全的,换为 ConcurrentHashMap
同时把 value
写死同样能够达到 set
的效果。ConcurrentHashMap
的大小尽可能大一些,避免频繁的扩容。MySQL
中不少数据都已经不用了,进行冷热处理。尽可能下降单表数据量。同时后期考虑分表。JDK1.8
。HashMap
的死循环问题在网上层出不穷,没想到还真被我遇到了。如今要知足这个条件仍是挺少见的,好比 1.8 如下的 JDK
这一条可能大多数人就碰不到,正好又证明了一次墨菲定律。
同时我会将文章更到这里,方便你们阅读和查询。
https://crossoverjie.top/JCSprout/
你的点赞与分享是对我最大的支持