在以往的开发中我们可能会根据项目本身对 Tomcat 进行一些调整,以达到最大化利用 Tomcat 的目的。SpringBoot 使用嵌入式 Tomcat,再像之前那样做 Tomcat 性能调优就显得不那么现实了,为此我们需要了解如何在 SpringBoot 内部给嵌入式 Tomcat 做性能调优。本篇文章只做定的解析,深入到的控制本文不作详细探讨。

0. 调优前的准备

为测试当前 SpringBoot 中嵌入式 Tomcat 的最大性能,需要一个压力测试工具来辅助我们测试性能,目前应用比较多的压测工具有 BenchJMeter ,本文中使用 Bench 作为压测工具。

测试之前,咱先把工具准备好:

下载好之后,把这两个工具的环境变量都配置好,方便直接从控制台执行。

除此之外,把一开始的测试工程中加入一个测试的 DemoController ,用于接收请求压测(为模拟真实业务场景,会在 DemoController 中让线程随机阻塞 100 - 500ms ,以代替数据库连接和业务查询)。最后,把工程打成可执行jar包并启动,等待测试。

jar包启动的方式非常简单:java -jar demo-0.0.1-SNAPSHOT.jar

(本文在进行压测时的物理环境:Windows10 + Intel Core i7-8750H)

1. 使用Bench进行压测

在cmd中执行如下命令:

ab -n 10000 -c 500 http://localhost:8080/test

执行完成后会在控制台打印测试报告:(报告中的指标解释已标注在行尾

1
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
2
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
3
Licensed to The Apache Software Foundation, http://www.apache.org/
4
5
Benchmarking localhost (be patient)
6
Completed 1000 requests
7
Completed 2000 requests
8
Completed 3000 requests
9
Completed 4000 requests
10
Completed 5000 requests
11
Completed 6000 requests
12
Completed 7000 requests
13
Completed 8000 requests
14
Completed 9000 requests
15
Completed 10000 requests
16
Finished 10000 requests
17
18
19
Server Software:
20
Server Hostname:        localhost // 主机名
21
Server Port:            8080      // 端口号
22
23
Document Path:          /test
24
Document Length:        4 bytes
25
26
Concurrency Level:      500            // 并发量
27
Time taken for tests:   15.670 seconds // 所有请求的总耗时
28
Complete requests:      10000          // 成功的请求数
29
Failed requests:        0
30
Total transferred:      1360000 bytes  // 总传输数据量
31
HTML transferred:       40000 bytes    // 总响应数据量
32
Requests per second:    638.17 [#/sec] (mean) // 【重要】每秒执行的请求数量(吞吐量)
33
Time per request:       783.493 [ms] (mean)   // 【重要】客户端平均响应时间
34
Time per request:       1.567 [ms] (mean, across all concurrent requests) // 服务器平均请求等待时间
35
Transfer rate:          84.76 [Kbytes/sec] received // 每秒传输的数据量
36
37
Connection Times (ms)
38
              min  mean[+/-sd] median   max
39
Connect:        0    0   0.2      0       1
40
Processing:   105  738 135.1    742     993
41
Waiting:      105  738 135.2    742     993
42
Total:        105  738 135.1    742     993
43
44
Percentage of the requests served within a certain time (ms)
45
  50%    742
46
  66%    810
47
  75%    847
48
  80%    868
49
  90%    909
50
  95%    931
51
  98%    945
52
  99%    952
53
 100%    993 (longest request)

在测试报告中有两个重要的指标需要咱来关注:

  • Requests per second:每秒执行的请求数量(吞吐量)
    • 吞吐量越高,代表性能越好
  • Time per request:客户端平均响应时间
    • 响应时间越短,代表性能越好

在这里面测得的结果是 638.17 的吞吐量,783.493ms 的平均响应时间,这个响应时间比代码中控制的阻塞时间更长,说明 Tomcat 对500的并发已经有一些吃力了。

下面咱再用更大的并发量来测试效果:

ab -n 50000 -c 2000 http://localhost:8080/test

测得的结果(截取主要部分):

1
Concurrency Level:      2000
2
Time taken for tests:   75.689 seconds
3
Complete requests:      50000
4
Failed requests:        0
5
Total transferred:      6800000 bytes
6
HTML transferred:       200000 bytes
7
Requests per second:    660.60 [#/sec] (mean)
8
Time per request:       3027.564 [ms] (mean)
9
Time per request:       1.514 [ms] (mean, across all concurrent requests)
10
Transfer rate:          87.74 [Kbytes/sec] received

发现吞吐量没有什么太大的变化,但平均响应时间大幅提升,且大概为上面的4倍。可以看得出来,Tomcat 的处理速度已经远远跟不上请求到来的速度,需要进行性能调优。

2. 嵌入式Tomcat调优依据

调优一定要有依据,咱根据现状和之前对 SpringBoot 的学习和原理剖析,应该知道配置大多都是两种形式:

  • 声明式配置:application.propertiesapplication.yml
  • 编程式配置:XXXConfigurerXXXCustomizer

其中,利用配置文件进行配置,最终会映射到 SpringBoot 中的一些 Properties 类中,例如 server.port 配置会映射到 ServerProperties 类中:

1
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
2
public class ServerProperties {
3
    private Integer port;

那我们来大体分析一下对于 Tomcat 的声明式配置,都有哪些可以控制的部分:

2.1 Tomcat的声明式配置

ServerProperties 类中,有一个 Tomcat 的静态内部类:

1
/**
2
 * Tomcat properties.
3
 */
4
public static class Tomcat {
5
       // ......

这里面就是配置嵌入式 Tomcat 的可以供我们配置的映射配置类。咱来看里面的核心属性:

1
/**
2
 * Maximum amount of worker threads.
3
 * 最大工作线程数
4
 */
5
private int maxThreads = 200;
6
7
/**
8
 * Minimum amount of worker threads.
9
 * 最小工作线程数
10
 */
11
private int minSpareThreads = 10;
12
13
/**
14
 * Maximum number of connections that the server accepts and processes at any
15
 * given time. Once the limit has been reached, the operating system may still
16
 * accept connections based on the "acceptCount" property.
17
 * 服务器最大连接数
18
 */
19
private int maxConnections = 10000;
20
21
/**
22
 * Maximum queue length for incoming connection requests when all possible request
23
 * processing threads are in use.
24
 * 最大请求队列等待长度
25
 */
26
private int acceptCount = 100;

可以发现这里面的几个指标,分别控制连接数、线程数、等待数。

咱来分析为什么上面的吞吐量不够大:请求中的关键耗时动作是 Thread.sheep 卡线程,导致吞吐量变大。Thread.sleep 模拟了IO操作、数据库交互等非CPU高速计算的行为,在数据库交互时,CPU资源被浪费,导致无法处理后来的请求,出现资源利用率低的现象。为此,我们需要提高请求并发数,以此来提高CPU利用率。提高请求并发的方法在上面的几个参数中很明显是 maxThreads

3. 调整maxThreads

从源码中很明显看到默认的最大线程数是200,我们在 application.properties 中修改值为 500:

server.tomcat.max-threads=500

修改之后的测试:

1
Concurrency Level:      2000
2
Time taken for tests:   30.910 seconds
3
Complete requests:      50000
4
Failed requests:        0
5
Total transferred:      6800000 bytes
6
HTML transferred:       200000 bytes
7
Requests per second:    1617.61 [#/sec] (mean)
8
Time per request:       1236.391 [ms] (mean)
9
Time per request:       0.618 [ms] (mean, across all concurrent requests)
10
Transfer rate:          214.84 [Kbytes/sec] received

发现吞吐量有明显的提升,且吞吐量的放大倍数大概是前面线程数为 200 时的2.5倍。继续放大该值为 2000:

server.tomcat.max-threads=2000

重新测试效果:

1
Concurrency Level:      2000
2
Time taken for tests:   12.050 seconds
3
Complete requests:      50000
4
Failed requests:        0
5
Total transferred:      6800000 bytes
6
HTML transferred:       200000 bytes
7
Requests per second:    4149.38 [#/sec] (mean)
8
Time per request:       482.000 [ms] (mean)
9
Time per request:       0.241 [ms] (mean, across all concurrent requests)
10
Transfer rate:          551.09 [Kbytes/sec] received

吞吐量又一次明显上升,但注意此时的吞吐量并没有扩大到上一次的 4 倍。继续放大该值为 10000:

server.tomcat.max-threads=10000

重新测试效果:

1
Concurrency Level:      2000
2
Time taken for tests:   13.808 seconds
3
Complete requests:      50000
4
Failed requests:        0
5
Total transferred:      6800000 bytes
6
HTML transferred:       200000 bytes
7
Requests per second:    3621.22 [#/sec] (mean)
8
Time per request:       552.300 [ms] (mean)
9
Time per request:       0.276 [ms] (mean, across all concurrent requests)
10
Transfer rate:          480.94 [Kbytes/sec] received

发现吞吐量竟然下降了!为什么会出现这种现象呢?

4. 现象解释

要解释这个原因,就不得不提到 CPU 的工作原理了。当CPU的核心线程数小于当前应用线程时,CPU为了保证所有应用线程都正常执行,它会在多个线程中来回切换,以保证每个线程都能获得CPU时间。在一个确定的时间点中,一个CPU只能处理一个线程。

所以这个现象就可以这样解释:当开启的 Tomcat 线程过多时,CPU会消耗大量时间在这些 Tomcat 线程中来回切换,导致真正处理业务请求的时间变少,最终导致整体应用处理速度变慢。

由此也可以推出另一种可能:如果业务逻辑中有大量CPU处理工作(如运算、处理数据等),则CPU需要更多的时间用于计算,此时若 Tomcat 线程过多,则处理速度会更慢。

5. 总结

由上面的情况可以总结出以下结论:

  • 应用中大部分业务逻辑都是阻塞型处理(IO、数据库操作等),这种情况下CPU的压力较低,可以适当调大 maxThreads 的值大小。
  • 应用中大部分业务逻辑都是数据处理和计算,这种情况下CPU的压力较大,应适当调小 maxThreads 的值大小。