嵌入式容器:嵌入式Tomcat的优化和配置
在以往的开发中我们可能会根据项目本身对 Tomcat 进行一些调整,以达到最大化利用 Tomcat 的目的。SpringBoot 使用嵌入式 Tomcat,再像之前那样做 Tomcat 性能调优就显得不那么现实了,为此我们需要了解如何在 SpringBoot 内部给嵌入式 Tomcat 做性能调优。本篇文章只做定性的解析,深入到量的控制本文不作详细探讨。
0. 调优前的准备
为测试当前 SpringBoot 中嵌入式 Tomcat 的最大性能,需要一个压力测试工具来辅助我们测试性能,目前应用比较多的压测工具有 Bench 和 JMeter ,本文中使用 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.properties
或application.yml
- 编程式配置:
XXXConfigurer
或XXXCustomizer
其中,利用配置文件进行配置,最终会映射到 SpringBoot 中的一些 Properties 类中,例如 server.port
配置会映射到 ServerProperties
类中:
1 | "server", ignoreUnknownFields = true) (prefix = |
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
的值大小。
原文作者: LinkedBear
原文链接: http://yoursite.com/2019/12/02/嵌入式容器:嵌入式Tomcat的优化和配置/
版权声明: 转载请注明出处(必须保留作者署名及链接)