SpringBoot+内置Tomcat配置,参数调优,最大并发量,最大连接数
作者:mmseoamin日期:2023-12-21

SpringBoot开发最大的好处是简化配置,内置了Tomcat, 在SpringBoot2.0.x版本中内置Tomcat版本是8.5.x,SpringBoot内置Tomcat的默认设置中,Tomcat的等待队列长度默认是100,Tomcat的最小工作线程数默认分配10,Tomcat的最大线程数是200,最大连接数是10000,至于最大并发量和最大连接数,常常理解成最大并发量就是最大连接数,实际上是有些牵强的,最大连接数并不一定就是最大并发量。

SpringBoot内置Tomcat的包重要配置和类在

package org.springframework.boot.autoconfigure.web;
SpringBoot+内置Tomcat配置,参数调优,最大并发量,最大连接数,第1张 SpringBoot+内置Tomcat配置,参数调优,最大并发量,最大连接数,第2张

内置的tomcat

SpringBoot+内置Tomcat配置,参数调优,最大并发量,最大连接数,第3张

找到ServerProperties中的public static class Tomcat对象

package org.springframework.boot.autoconfigure.web;
…………
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
        //内容
}
SpringBoot+内置Tomcat配置,参数调优,最大并发量,最大连接数,第4张

在public static class Tomcat中有很多配置

        /**
         * Maximum number of worker threads.
         */
        private int maxThreads = 200;
        /**
         * Minimum number of worker threads.
         */
        private int minSpareThreads = 10;
        /**
         * Maximum size, in bytes, of the HTTP post content.
         */
        private int maxHttpPostSize = 2097152;
        /**
         * Maximum size, in bytes, of the HTTP message header.
         */
        private int maxHttpHeaderSize = 0;
        /**
         * Whether requests to the context root should be redirected by appending a / to
         * the path.
         */
        private Boolean redirectContextRoot = true;
        /**
         * Whether HTTP 1.1 and later location headers generated by a call to sendRedirect
         * will use relative or absolute redirects.
         */
        private Boolean useRelativeRedirects;
        /**
         * Character encoding to use to decode the URI.
         */
        private Charset uriEncoding = StandardCharsets.UTF_8;
        /**
         * Maximum number of connections that the server accepts and processes at any
         * given time. Once the limit has been reached, the operating system may still
         * accept connections based on the "acceptCount" property.
         */
        private int maxConnections = 10000;
        /**
         * Maximum queue length for incoming connection requests when all possible request
         * processing threads are in use.
         */
        private int acceptCount = 100;

默认最大连接数maxConnections = 10000

默认队列长度acceptCount = 100

默认最大工作线程数maxThreads = 200

默认最小工作线程数 minSpareThreads = 10

也就是说配置如下

server.tomcat.accept-count = 100

server.tomcat.max-connections = 10000

server.tomcat.max-threads = 200

server.tomcat.min-spare-threads=10

在这里有个点儿

Maximum number of connections that the server accepts and processes at any given time. Once the limit has been reached, the operating system may still accept connections based on the "acceptCount" property
服务器在任何给定时间接受和处理的最大连接数。一旦达到限制,操作系统仍然可以根据“acceptCount”属性接受连接

也就是说当服务器已经达到最大连接数后,操作系统任然可以根据队列长度来接收连接

有时候我们通常会认为在默认配置下,最大并发量就是最大连接数,超过最大连接数10000后会出现tomcat拒绝连接的情况,触发的请求任务超过默认值200(最大线程数)+默认值100(等待队列长度)后,tomcat会拒绝处理请求任务

最大并发量,每个人都它的理解是不一样的

  1. 如果在乎tomcat运行能够同时处理的任务数量,那最大并发量可能理解成最大工作线程数(max-threads)---不包含队列里的数量(acceptCount)

  1. 如果在乎tomcat运行能够接纳的最大最多的任务数量,那最大并发量可以理解成最大连接数(max-connections)+队列长度的数量(accept-count) --- 包含队列里的数量(acceptCount)

通常对SpringBoot内置Tomcat调优主要是针对最大连接数(maxConnections = 10000),队列长度(acceptCount = 100),最大工作线程数(maxThreads = 200)和最小工作线程数 (minSpareThreads = 10)按需设置,一般根据服务器的性能(CPU)以及该程序可能面临的业务峰值(IO数据库等操作)进行参考调优。

测试一下SpringBoot内置Tomcat的最小工作线程和最大工作线程以及最大连接数和队列长度

我用的版本是SpringBoot2.0.5

    
        org.springframework.boot
        spring-boot-starter-parent
        2.0.5.RELEASE
         
    

简单编写一个http请求的Controller

package boot.example.web.tomcat.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
/**
 *  蚂蚁舞
 */
@Controller
@RequestMapping(value="/demo")
public class BootTomcatController {
    @Resource
    private BootTomcatService bootTomcatService;
    @RequestMapping(value="/test/{count}")
    @ResponseBody
    public String test(@PathVariable(name = "count", required = true) int count) throws InterruptedException {
        bootTomcatService.testTomcatThread(count);
        return "hello world " + count;
    }
}

处理业务的Service

package boot.example.web.tomcat.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
 *  蚂蚁舞
 */
@Service
public class BootTomcatService {
    private final Logger log =  LoggerFactory.getLogger(this.getClass());
    void testTomcatThread(int count) throws InterruptedException {
        log.info(count+"");
        //Thread.sleep(40*1000);
    }
}

配置Tomcat的参数

server.port=8080

server.tomcat.accept-count = 2

server.tomcat.max-connections = 12

server.tomcat.max-threads = 6

server.tomcat.min-spare-threads=3

在这里,我把accept-count配置成2 把max-connections配置成12,把max-threads配置成6,min-spare-threads配置成3

新建一个测试线程

package boot.example.web.tomcat;
import cn.hutool.http.HttpRequest;
/**
 *  蚂蚁舞
 */
public class ThreadTest extends Thread {
    private final int count;
    public ThreadTest(int count){
        this.count = count;
    }
    public void run() {
        try {
            //  设置超时时间很大
            String result = HttpRequest.get("http://127.0.0.1:8080/demo/test/"+count).timeout(10000*1000).execute().body();;
            System.out.println(result);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

这里用到hutool的http请求

cn.hutool

hutool-all

5.5.6

启动的main方法

package boot.example.web.tomcat;
public class TestTomcat {
    public static void main(String[] args) {
        for (int i = 1; i < 30; i++) {
            ThreadTest test = new ThreadTest(i);
            test.start();
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(i);
        }
    }
}

启动SpringBoot Tomcat 然后启动测试的main方法 30次请求访问

控制台日志

SpringBoot+内置Tomcat配置,参数调优,最大并发量,最大连接数,第5张

可以看到他的工作线程是3个,完全是符合server.tomcat.min-spare-threads=3的配置,而且线程数的使用没有达到最大线程数

假如我们把它的工作线程给临时阻塞着,故意让他达到最大线程数,40s时间

    void testTomcatThread(int count) throws InterruptedException {
        log.info(count+"");
        Thread.sleep(40*1000);
    }

如此的话运行程序 请求30次

可以看到控制台首先进入了6个请求,阻塞着处理任务,那么配置server.tomcat.max-threads = 6是生效的,等40s之后6个任务线程处理完了业务,接下来又是6个任务线程处理,再等40后还处理了2个任务线程,请求的次数30次 实际处理任务数加起来只有14次

SpringBoot+内置Tomcat配置,参数调优,最大并发量,最大连接数,第6张

请求端日志(有报错)

SpringBoot+内置Tomcat配置,参数调优,最大并发量,最大连接数,第7张

报错信息如下

Exception in thread "Thread-14" java.lang.RuntimeException: cn.hutool.core.io.IORuntimeException: ConnectException: Connection refused: connect
at boot.example.web.tomcat.ThreadTest.run(ThreadTest.java:24)
Caused by: cn.hutool.core.io.IORuntimeException: ConnectException: Connection refused: connect
at cn.hutool.http.HttpRequest.send(HttpRequest.java:1164)
at cn.hutool.http.HttpRequest.execute(HttpRequest.java:969)
at cn.hutool.http.HttpRequest.execute(HttpRequest.java:940)
at boot.example.web.tomcat.ThreadTest.run(ThreadTest.java:21)
Caused by: java.netCaused by: java.net.ConnectException: Connection refused: connect
at java.netat java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
at java.netat java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:85)
at java.netat java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.netat java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.netat java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.netat java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
at java.netat java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.netat java.net.Socket.connect(Socket.java:589)
at sun.netat sun.net.NetworkClient.doConnect(NetworkClient.java:175)
at sun.netat sun.net.www.http.HttpClient.openServer(HttpClient.java:463)
at sun.netat sun.net.www.http.HttpClient.openServer(HttpClient.java:558)
at sun.netat sun.net.www.http.HttpClient.(HttpClient.java:242)
at sun.netat sun.net.www.http.HttpClient.New(HttpClient.java:339)
at sun.netat sun.net.www.http.HttpClient.New(HttpClient.java:357)
at sun.netat sun.net.www.protocol.http.HttpURLConnection.getNewHttpClient(HttpURLConnection.java:1220)
at sun.netat sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConnection.java:1156)
at sun.netat sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:1050)
at sun.netat sun.net.www.protocol.http.HttpURLConnection.connect(HttpURLConnection.java:984)
at cn.hutool.http.HttpConnection.connect(HttpConnection.java:377)
at cn.hutool.http.HttpRequest.send(HttpRequest.java:1159)
... 3 more
14:18:57:25
Exception in thread "Thread-15" java.lang.RuntimeException: cn.hutool.core.io.IORuntimeException: ConnectException: Connection refused: connect
at boot.example.web.tomcat.ThreadTest.run(ThreadTest.java:24)
Caused by: cn.hutool.core.io.IORuntimeException: ConnectException: Connection refused: connect
at cn.hutool.http.HttpRequest.send(HttpRequest.java:1164)
at cn.hutool.http.HttpRequest.execute(HttpRequest.java:969)
at cn.hutool.http.HttpRequest.execute(HttpRequest.java:940)
at boot.example.web.tomcat.ThreadTest.run(ThreadTest.java:21)
Caused by: java.netCaused by: java.net.ConnectException: Connection refused: connect
at java.netat java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
at java.netat java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:85)
at java.netat java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.netat java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.netat java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.netat java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
at java.netat java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.netat java.net.Socket.connect(Socket.java:589)
at sun.netat sun.net.NetworkClient.doConnect(NetworkClient.java:175)
at sun.netat sun.net.www.http.HttpClient.openServer(HttpClient.java:463)
at sun.netat sun.net.www.http.HttpClient.openServer(HttpClient.java:558)
at sun.netat sun.net.www.http.HttpClient.(HttpClient.java:242)
at sun.netat sun.net.www.http.HttpClient.New(HttpClient.java:339)
at sun.netat sun.net.www.http.HttpClient.New(HttpClient.java:357)
at sun.netat sun.net.www.protocol.http.HttpURLConnection.getNewHttpClient(HttpURLConnection.java:1220)
at sun.netat sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConnection.java:1156)
at sun.netat sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:1050)
at sun.net.www.protocol.http.HttpURLConnection.connect(HttpURLConnection.java:984)
at cn.hutool.http.HttpConnection.connect(HttpConnection.java:377)
at cn.hutool.http.HttpRequest.send(HttpRequest.java:1159)
... 3 more

关键点在于

Caused by: cn.hutool.core.io.IORuntimeException: ConnectException: Connection refused: connect

也就是说30次请求,总共处理了14次请求,剩下的16次请求被tomcat给拒绝了----达到tomcat的最大能接纳的请求数后拒绝多余的请求连接。

SpringBoot+内置Tomcat配置,参数调优,最大并发量,最大连接数,第8张 SpringBoot+内置Tomcat配置,参数调优,最大并发量,最大连接数,第9张

30次请求进入tomcat后,16条被拒接,14条请求被处理,处理先是最大线程数max-threads=6,在处理的时候,有6条请求加上正在处理的6条满足最大连接数max-connections=12,在这里,还有2条,是accept-count=2配置的2条,这2条请求任务恰恰是最容易忽略的,如此最终处理了14条请求,也就是说tomcat拒绝请求的条件是大于了最大连接数+队列长度的数量。

这里的测试是在设置很大超时时间下进行的

timeout(10000*1000)

tomcat参数调优配置(和硬件本身的支撑有很大关系)

server.tomcat.min-spare-threads=10

默认最小的线程数10就可以,这个参数没有多大必要去配置,默认的已经很好了,建议配置范围10-50之间

server.tomcat.max-threads = 200

处理任务的最大线程数默认200,一般应用都是支持的,如果要优化看应用(写的程序)复不复杂,需不需要依托计算机的算力,也就是会不会大量消耗cpu,如果大量消耗cpu,那么这个max-threads不能设置过大,如果仅仅只是普通的入库查询操作,增删改查,max-threads可以设置大一些,但是也不能过大,过大会导致请求的响应变慢 ,建议设置在200-1200之间,大概是min-space-threads的20倍

server.tomcat.max-connections = 10000

最大连接线程数,这个值默认10000已经够大了,有时候真正的业务还没有达到这个值都已经要多服务部署了,因此该参数没有增大的必要,但是可以改小,改到max-thread的20倍左右

server.tomcat.accept-count = 100

至于队列中的默认100这个值,也满足需求了,非要改建议大于min-spare-threads小于max-threads之间的某个倍数值就可以,这个参数不能设置太小。

一般的请求都是有超时机制的,一个http请求,可能几十秒后都还没有得到数据,那就会自动超时,自动超时并不代表被tomcat拒绝,可能是tomcat还没有开始处理到它

这里记录一下SpringBoot内置Tomcat配置多端口启动

    @Bean
    public TomcatServletWebServerFactory getFactory() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        Connector[] connectors = this.connectors();
        if (connectors != null && connectors.length > 0) {
            tomcat.addAdditionalTomcatConnectors(connectors);
        }
        return tomcat;
    }
    private Connector[] connectors() {
        String ports = "8081,8082,8083,8084";
        if (StringUtils.isEmpty(ports)) {
            return null;
        }
        String[] port = ports.split(",");
        List connectors = new ArrayList<>();
        for (String s : port) {
            Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
            connector.setPort(Integer.parseInt(s));
            connector.setScheme("http");
            //connector.setRedirectPort(8041);
            connectors.add(connector);
        }
        return connectors.toArray(new Connector[]{});
    }
15:16:08.692 spring-boot-logging [restartedMain] INFO o.s.b.d.a.OptionalLiveReloadServer - LiveReload server is running on port 35729
15:16:08.734 spring-boot-logging [restartedMain] INFO o.s.j.e.a.AnnotationMBeanExporter - Registering beans for JMX exposure on startup
15:16:08.752 spring-boot-logging [restartedMain] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
15:16:08.754 spring-boot-logging [restartedMain] INFO o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
15:16:08.767 spring-boot-logging [restartedMain] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8081"]
15:16:08.773 spring-boot-logging [restartedMain] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8082"]
15:16:08.777 spring-boot-logging [restartedMain] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8083"]
15:16:08.782 spring-boot-logging [restartedMain] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8084"]
15:16:08.797 spring-boot-logging [restartedMain] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port(s): 8080 (http) 8081 (http) 8082 (http) 8083 (http) 8084 (http) with context path ''
15:16:08.802 spring-boot-logging [restartedMain] INFO boot.example.web.tomcat.AppTomcat - Started AppTomcat in 5.725 seconds (JVM running for 6.629)
Hello World!

这一条

Tomcat started on port(s): 8080 (http) 8081 (http) 8082 (http) 8083 (http) 8084 (http) with context path ''