上周做了在定时任务服务上做了一个缓存预热功能,周五发到了测试环境,周一来发现竟然OOM,原因大概是不能再创建线程之类的。
然后从grafana看了下这几天的内存趋势图,发现果然一直在涨。
开始排查。
如果你的pod启动命令添加了:XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/logs/HeapDump_Gc/${HOSTNAME}.hprof
联系运维把dump文件拉下来,用工具分析。
不幸的事,我进入到该pod并没有生成dump文件,只好换另外一种方式。
换个思路,本地启动,用arthas监控。
本地跑项目,启动arthas抓dump日志,放到jProfiler工具里分析(JDK自带的也可以)。
对比发现,无特殊重大对象。
后来,在pod中执行 top -H -p 进程id 。【查看一个进程创建的线程数】
发现项目进程竟创建了2000多个线程。
怎么排查是什么线程?
也用jProfiler工具。
可以看到,手动GC,是可以回收的。
因此又去看了下实时的线程,发现有一个定时任务线程不断增加,且一直未被回收(一时太激动,忘了截图了)。
看来是因为创建太多线程,达到线程创建上限了(至于为什么线程不停在创建,甚至内存都快满了,也没有触发垃圾回收,这里还有待研究)。
后经过排查发现是下面这个定时任务线程池,每启动一个任务,就创建一个线程,且没有触发垃圾回收,因此就线程创建上限,OOM了。
解决办法,手动指定定时任务线程池:
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(16);
return scheduler;
}
关于为什么会不听创建线程,详情可参考@EnableAsync注解的使用:SpringBoot中并发定时任务的实现、动态定时任务的实现(看这一篇就够了)- 每日博客
本项目正是由于误用异步线程:
- 误把每个定时任务类上都加上@EnableAsync注解,其实不用,只需要在启动类加即可。
- Spring默认线程池为了兼容并发性,设置并发线程无线创建,而旧的线程又没有触发GC,没有及时回收,所以达到进程创建线程上限,报OOM异常。
所以,这项目前人还是留下很多坑的,要多注意!
加餐:
关于一个进程最多可创建多少线程?
引用:
那么假设创建一个线程需要占用 10M 虚拟内存,总共有 3G 虚拟内存可以使用。于是我们可以算出,最多可以创建差不多 300 个(3G/10M)左右的线程。
pod栈空间为8g。
64 位系统的虚拟内存大小,理论上可以创建无数个线程。
事实上,肯定创建不了那么多线程,除了虚拟内存的限制,还有系统的限制。
比如下面这三个内核参数的大小,都会影响创建线程的上限:
- /proc/sys/kernel/threads-max,表示系统支持的最大线程数,默认值是
14553
; - /proc/sys/kernel/pid_max,表示系统全局的 PID 号数值的限制,每一个进程或线程都有 ID,ID 的值超过这个数,进程或线程就会创建失败,默认值是
32768
; - /proc/sys/vm/max_map_count,表示限制一个进程可以拥有的VMA(虚拟内存区域)的数量,具体什么意思我也没搞清楚,反正如果它的值很小,也会导致创建线程失败,默认值是
65530
。
从这几个参数上看,能创建上万个线程,那么限制出现在哪里了呢?
看一下Pod的可用内存大小:
看下JVM的启动参数设置:
java -jar -Dcom.sun.management.jmxremote.authenticate=false 非认证登录
-Dcom.sun.management.jmxremote.port=1899
-Dcom.sun.management.jmxremote.ssl=false
-Xms512m 堆初始内存 -Xmx7048m 堆对大内存 -Xmn1024m 年轻代大小 -Xss1m 每个线程的堆栈大小(重点在这里,1.8g内存,每个1m,差不多能创建2000个左右)
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=512m
-XX:+UseCompressedOops
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
-XX:MaxTenuringThreshold=6
-XX:+ExplicitGCInvokesConcurrent
-XX:+ParallelRefProcEnabled
-Xloggc:/home/logs/HeapDump_Gc/${HOSTNAME}-gc.log
-XX:+PrintGCDateStamps -XX:+PrintGCDetails
-XX:+PrintGCApplicationStoppedTime
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/logs/HeapDump_Gc/${HOSTNAME}.hprof
-Dfile.encoding=UTF-8
-Duser.timezone=GMT+08
-Dspring.profiles.active=${profile}
后续:
修改之后,可以看到正常允许,没有OOM产生,同时达到一定上限也会触发GC回收了。
关于springBoot的线程池: