作者 | Ethan Zhang
编辑 | Ken Zheng
信也科技基础框架团队基于k8s构建了一套容器云平台。最近我们发现有部门接入容器云平台的时候,容器实例拉入流量的瞬间,上游出现了调用超时的情况,
过了一会儿时间之后就恢复了。在容器云平台中为了实现自助的流量调节,我们会把实例的上下线状态同步到Consul,然后Nginx会定时同步Consul上的实例状态。这样做的好处是上下线状态可以及时同步到Nginx,而且Nginx也不用reload配置文件。相比于之前的虚拟机流量调节不管在实时性和Nginx的稳定性方面都有显著地提升。但是有个副作用是实例刚启动后拉入流量的瞬间并发请求数比以前虚机会高很多。整个机制如下图所示:
为了应付启动后的高流量,我们可以调大Tomcat的初始最小线程数。我们通常认为Tomcat在启动的时候会给 我们预创建这些线程。可是通过监控发现有问题的这些站点的Tomcat的线程数没有在程序启动后到达预期的最小线程数配置,而是在请求进来的时候才开始创建。为什么Tomcat没有给我们预创建线程,我们仔细分析了下原因,给大家分享一下。
大家都知道ThreadPoolExecutor是JDK中标准的一个线程池实现。初始化该对象时可以传入一个参数corePoolSize,控制线程池的核心 线程数的数量。核心线程一旦启动后就会常驻在程序中,线程池接收到任务直接使用核心线程去执行,节省了创建线程的开销。
可是核心线程并不是线程池创建完成之后自动创建的,通过观察ThreadPoolExecutor的execute方法发现,当前启动线程数小于核心线程数 时,每次都会启动一个新的线程去处理任务。代码如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 当前启动线程数小于核心线程数,调用addWorker创建新线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
...
}
所以不管核心线程数设置的多大,ThreadPoolExecutor默认都不会启动线程。这时如果有大量的任务并发进来的话,线程池就会忙于创建新线程, 造成性能开销。好在ThreadPoolExecutor类提供了prestartAllCoreThreads方法,让我们预启动核心线程。这相当于给线程池提前热了身,之后有任务 进来,直接使用已经启动好线程去处理,省去了创建线程的开销。
Tomcat中可以通过配置参数minSpareThreads给Tomcat设置处理请求的核心线程数,该参数的默认值在8.5.x普通版的tomcat下是25,在嵌入式 版本的Tomcat(如Spring Boot使用的Tomcat)中是10。那么Tomcat会对线程池进行预热么?
我们通过查看Tomcat的线程池的配置,发现一个配置参数prestartminSpareThreads。如果配置为true,它会预启动由minSpareThread配置的 线程数量的线程。但是它的默认值是false,这也符合了我们的观察结果。于是我们想把prestartminSpareThreads设置成true,可是Spring Boot的Tomcat并没有 给我们提供这个参数可以设置。我们调大最小线程数就是为了应付启动后的高并发请求,如果不能设置的话岂不是要在启动后不断地调接口去让Tomcat的线程池热身?这个明显不符合我们使用Tomcat的常理,并且有一个奇怪的现象是并不是所有的站点在拉入流量后都会有超时,有这个现象的都是在一个部门或小组下的站点。我们用Spring Boot实现 了一个服务测试发现,Tomcat在启动的时候居然给我们预启动了线程。通过查看Tomcat的源码,Tomcat自己实现了一个继承自JDK中的ThreadPoolExecutor的线程池,并在 构造方法中预启动了线程,测试服务所使用的Tomcat版本是8.5.23。
// org.apache.tomcat.util.threads.ThreadPoolExecutor tomcat自己实现的一个线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
// 预启动了corePoolSize个线程
this.prestartAllCoreThreads();
}
可见虽然没有提供prestartminSpareThreads配置,但是Tomcat自动给我们预启动了线程。可是有问题的一些业务方的站点为什么又不是这个现象呢?所以我们拿到他们的代码之后,反编译了Tomcat的源码,发现Tomcat实现的线程池构造方法中预启动的代码没有了。于是查看Tomcat的版本,发现没有预启动线程的版本是8.5.9。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
// 8.5.9版本这里没有预启动线程的代码
}
在Spring Boot使用的Tomcat中是否预启动线程池中的线程和Tomcat的版本相关,通过Tomcat的代码修历史发现,此问题在8.5.12版本中修复了,GitHub中的修改记录如下:
如果大家的站点对流量比较敏感,建议使用Tomcat8.5.12之后的版本,否则最小线程数设置的越大,对性能的开销也就越大,这也算是Tomcat的一个坑吧。
作者介绍
Ethan Zhang,信也科技布道师、基础框架资深专家、目前主要从事K8s相关的研发、运维工作
更多福利请关注官方订阅号“拍码场”
好内容不要独享!快告诉小伙伴们吧!
想加入我们?长按下方二维码!
喜欢请点击↓↓↓