xiaoming728

xiaoming728

SpringBoot 内置 Tomcat 线程数优化配置

2024-01-12
SpringBoot 内置 Tomcat 线程数优化配置

来源:微信公众号-Java学习者社区

日期:2022-05-21 11:45

链接:https://mp.weixin.qq.com/s/5kybQx3VToNrv9_S3ORReQ

标题:深度理解Tomcat的acceptCount、maxConnections、maxThreads

日期:2019-03-22 14:22:40

链接:https://blog.csdn.net/zzzgd_666/article/details/88740198

本文解析springboot内置tomcat调优并发线程数的一些参数,并结合源码进行分析

参数配置

线程池核心线程数

server.tomcat.threads.min-spare: 10

该参数为tomcat处理业务的核心线程数大小,默认值为10,一般跟随CPU核心数

线程池最大线程数

server.tomcat.threads.max: 200

请求处理线程的最大数量。默认值是200(Tomcat7和8都是的)。如果该Connector绑定了Executor,这个值会被忽略,因为该Connector将使用绑定的Executor,而不是内置的线程池来执行任务。

maxThreads规定的是最大的线程数目,并不是实际running的CPU数量;实际上,maxThreads的大小比CPU核心数量要大得多。这是因为,处理请求的线程真正用于计算的时间可能很少,大多数时间可能在阻塞,如等待数据库返回数据、等待硬盘读写数据等。因此,在某一时刻,只有少数的线程真正的在使用物理CPU,大多数线程都在等待;因此线程数远大于物理核心数才是合理的。

换句话说,Tomcat通过使用比CPU核心数量多得多的线程数,可以使CPU忙碌起来,大大提高CPU的利用率

请求最大连接数

server.tomcat.max-connections: 8192

Tomcat在任意时刻接收和处理的最大连接数。当Tomcat接收的连接数达到maxConnections时,Acceptor线程不会读取accept队列中的连接;这时accept队列中的线程会一直阻塞着,直到Tomcat接收的连接数小于maxConnections。如果设置为-1,则连接数不受限制。

默认值与连接器使用的协议有关:NIO的默认值是10000,APR/native的默认值是8192,而BIO的默认值为maxThreads(如果配置了Executor,则默认值是Executor的maxThreads)。

在windows下,APR/native的maxConnections值会自动调整为设置值以下最大的1024的整数倍;如设置为2000,则最大值实际是1024。

请求最大队列数

server.tomcat.accept-count: 100

accept队列的长度;当accept队列中连接的个数达到acceptCount时,队列满,进来的请求一律被拒绝。默认值是100。这个参数实际上和tomcat没有太大关系,它是服务端创建ServerSocket时操作系统控制同时连接的最大数量的,服务端接收连接是通过accept()来的,accept()是非常快的,所以accept-count的不需要太大,正常保持默认值100即可了,acceptCount这个参数和线程池无关,会被映射为backlog参数,是socket的参数,在源码的使用是在NioEndpoint类的initServerSocket方法,在tomcat中的名字是backlog在springboot内置tomcat中名字没有使用backlog而是使用acceptCount

Tomcat线程池处理机制

tomcat最终使用线程池来处理业务逻辑,java默认的线程池的规则:

核心线程数满了则优先放入队列,当队列满了之后才会创建非核心线程来处理

那么tomcat是这样做的吗?

首先如果tomcat这样做,那么当达到核心线程后后续任务就需要等待了,这显然是不合理的,我们通过源码来看下tomcat是如何处理的

AbstractEndpointcreateExecutor创建了处理业务数据的线程池

public void createExecutor() {
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);
}

主要是使用了TaskQueue队列,ThreadPoolExecutor并不是jdk的,而是tomcat重写的。

我们从线程池的处理方法execute看起

public void execute(Runnable command) {
    execute(command,0,TimeUnit.MILLISECONDS);
}
public void execute(Runnable command, long timeout, TimeUnit unit) {
    submittedCount.incrementAndGet();
    try {
        // 核心代码
        super.execute(command);
    } catch (RejectedExecutionException rx) {
        if (super.getQueue() instanceof TaskQueue) {
            final TaskQueue queue = (TaskQueue)super.getQueue();
            try {
                if (!queue.force(command, timeout, unit)) {
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException("Queue capacity is full.");
                }
            } catch (InterruptedException x) {
                submittedCount.decrementAndGet();
                throw new RejectedExecutionException(x);
            }
        } else {
            submittedCount.decrementAndGet();
            throw rx;
        }

    }
}

又调用会jdk的execute

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    // 1、工作线程数小于核心线程数则添加任务,核心线程会处理
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 2、工作线程不小于核心线程数,则放到workQueue队列中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 3、否则添加任务,addWorker会进行创建线程
    else if (!addWorker(command, false))
        reject(command);
}

从这里可以看到jdk线程池的机制,tomcat使用了自己的TaskQueue队列,所以我们看代码2处当核心线程用完了会调用队列的offer方法

我们看TaskQueue的offer

public boolean offer(Runnable o) {
    //we can't do any checks
    // parent就是指线程池,没有线程池则添加到队列
    if (parent==null) return super.offer(o);
    //we are maxed out on threads, simply queue the object
    // 线程数量已经达到了最大线程数,那么只能添加到队列
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
    //we have idle threads, just add it to the queue
    // 如果当前处理的任务数量小于当前线程池中线程的数量,那么任务放到线程池,即相当于马上会有空闲线程来处理
    if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
    //if we have less threads than maximum force creation of a new thread
    // TODO 核心代码,如果当前线程数量还没有达到线程池最大线程池的数量,那么就直接创建线程,这里返回false
    if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
    //if we reached here, we need to add it to the queue
    // 最后的策略,放到队列
    return super.offer(o);
}

可以看到当执行offer时,不是直接放到队列的,当线程池总线程数量还没达到线程池最大线程数时会返回false,返回false时就会执行线程池execute的代码3处,执行addWorker(command, false),也就开始创建新的线程来处理当前任务了

总结

tomcat主要通过使用自己的TaskQueue队列来对线程池做出了不同的策略,也就是tomcat当线程数大于核心数时就会直接创建新的线程来处理,而不是放到队列