问题&现象
1、由于系统过一段时间(四五天)commited old区会增大,我们应用中增加每天凌晨一次主动fullgc的任务,但是观察下来发现每天经过system.gc后线程数会增加几个,一直增加到接近300不会增加,并且增加的线程为守护线程。监控图如下:
2、某些机器偶然出现线程数陡增情况:
分析
第一反应为fullgc时会新建gc线程去处理,但是通过jstack指令监控两天的线程变化发现,gc线程名字和数量并没有发生变化,通过jstack两天的线程栈发现,每天增加的是tomcat生成的业务处理线程,http-nio-8080-exec-22,增加的数量也和jstack一样每天大概有四到五个;
重点是如下图,增加的6个线程如下:
这时会有一个疑问是为什么是daemon线程呢,因为守护线程随着jvm的销毁才会销毁,我们跟着springboot内嵌的tomcat源码看起,详细的加载流程和处理请求参考以下文章:springboot加载tomcat
简单说,tomcat内部也有一个类似selector角色去处理有请求的链接,一旦有read和write数据后会交给一个线程池去处理请求;
代码跟踪链路:Connector.startInternal()-->AbstractProtocol.start()-->endpoint.start()--
NioEndpoint.startInternal()-->AbstractEndpoint.createExecutor(),初始化线程池代码如下:
跟踪代码发现初始化核心线程数为10,最大为200,队列为无界队列,守护线程,为什么是守护线程,源码解释如下:主要原因是业务处理线程不销毁是为了减少新建线程带来的性能问题,也减少维护线程长期存活带来的问题
另外tomcat自己实现了线程队列,并不是jdk处理流程那样先判断核心再判断队列是否满,入队列 队列满了新增非核心线程,而是优先新建最大线程数量去处理,当没有线程处理时会放入队列;源码如下:
此时执行的threadpoolexecuter是tomcat自己实现的,但是execute执行调用jdk的方法
重点看上面图中workquue.offer方法,实际是先判断是否达到最大线程数,达到之后才会放入队列中。
如果线程池为空则使用父类线程池执行
如果线程池线程数量等于于最大线程数,进入队列
如果提交的任务数量小于运行的线程数,进入队列
如果线程数小于最大线程数,则返回false,上层代码会尝试创建新的线程
回过头来回答上面提出的问题,
-
因为systemgc会触发g1的fullgc,使用serial old来暂停业务线程去处理,如果此时有请求进来,发现没有可用线程可以处理,就会新增线程去处理请求,直到写入队列;
-
日常流量较大时也会存在业务线程不够用时,会尝试去创建多的线程,所以一半业务线程可以设置大一些,一般两核默认200个线程。
结论
因为目前看下来最大线程数设置为200,且现在200多台机器里业务处理最大线程数为81远远低于200,暂时没出现因为线程数暴增带来的问题,所以目前暂时继续观察。但是这种问题带给我们的思考和重视也要有的,例如如何避免出现大流量导致线程不够用等情况,所以我们还要采取一定的预警方案来提前发现问题和解决问题。
-
短期方案:增加监控报警:线程数超过380时报警
-
长期方案一:如果流量过大超过200线程数,可以适当对tomcat线程数进行调优,根据压测qps rt cpu等进行设置,一般最大线程数可以设置为n*200,n为cpu核数
-
长期方案二:增加单机和集群限流机制,例如接入sentinel等方式
-
紧急方案:一旦出现问题立即重启机器