tomcat 性能优化
# I/O 模型选择
IO 模型 | 说明 | 选择场景 |
---|---|---|
BIO | 所有 IO 事件都会阻塞线程,需要通过多线程来处理并发,会增加额外线程切换耗时,线程数越多,线程切换时间开销越大。造成 CPU 负载过大,但使用率不高,没能充分利用 CPU 资源 | 无论什么情况下都不要使用 BIO,尽管在访问量低的情况下跟 NIO 差别不大并且 BIO 的 API 对代码编写要容易很多,但是 tomcat 已经封装了这部分 API,我们并不需要面向 socket 编程,所以不用考虑这个问题 |
NIO | 使用 epoll 多路复用 IO 模型,在等待数据就绪(从 IO 设备读取数据到系统内核)不需要阻塞线程,一次 select 可以查询多个 Channel 事件,但数据从系统内核读取到用户空间还是阻塞操作 | 如果不是对性能要求极高,在绝大多数情况下这种处理方式已经足够。另外在 linux 平台对 NIO2 支持不是很完善,JVM 底层也是使用 epoll 来模拟 NIO2 的实现,跟 NIO 差别不大 ,所以linux 平台下选择 NIO |
NIO2 | 真正意义上的异步 IO,注册 IO 事件时传入回调函数,当数据就绪时调用回调函数完成数据处理 | windows 系统支持比较好,如果是 windows 下运行选 NIO2 |
APR | 是 apache 可移植库,提供了一组映射到下层操作系统的 API,使用 C 语言实现,在 TCP 协议层做了优化,使用 sendfile 和 DirectByteBuffer 实现“零拷贝”,同时避免频繁 GC。同时还使用 OpenSSL 处理 TSL 握手和加解密,如果使用 https 能显著提升数据加解密性能 | 对性能有极高要求或者是 https 场景下选择 APR |
# 相关配置
<!-- NIO -->
<Connector protocol="org.apache.coyote.http11.Http11NioProtocol" />
<!-- NIO2 -->
<Connector protocol="org.apache.coyote.http11.Http11Nio2Protocol" />
<!-- APR -->
<Connector protocol="org.apache.coyote.http11.Http11AprProtocol" />
2
3
4
5
6
# apr 依赖库安装
官方文档:https://tomcat.apache.org/tomcat-8.5-doc/apr.html (opens new window)
依赖:
- APR 1.2+ development headers (libapr1-dev package)
- OpenSSL 1.0.2+ development headers (libssl-dev package)
- JNI headers from Java compatible JDK 1.4+
- GNU development environment (gcc, make)
# 安装依赖
sudo apt install gcc cmake openssl
# 最新版本从 apr 官网下载 https://apr.apache.org/download.cgi
wget https://mirror.bit.edu.cn/apache//apr/apr-1.7.0.tar.gz
tar -zxvf apr-1.7.0.tar.gz
cd apr-1.7.0
./configure && make && sudo make install
wget https://mirror.bit.edu.cn/apache//apr/apr-util-1.6.1.tar.gz
tar -zxvf apr-util-1.6.1.tar.gz
cd apr-util-1.6.1
./configure --with-apr=/usr/local/apr && make && sudo make install
# 安装 tomcat-native
cd ${TOMCAT_HOME}/bin
tar -zxvf tomcat-native.tar.gz
./configure && make && sudo make install
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apr 配置
<!-- SSLEngine 是否启用 ssl -->
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<Connector protocol="org.apache.coyote.http11.Http11AprProtocol" />
2
3
4
export JAVA_OPTS="${JAVA_OPTS} -Djava.library.path=/usr/local/apr/lib"
# 线程池配置
参数说明,在org.apache.catalina.core.StandardThreadExecutor
类定义
参数 | 含义 | 说明 |
---|---|---|
threadPriority | 线程优先级,int,默认值 5 - Thread.NORM_PRIORITY | - |
daemon | 是否是 daemon 线程,boolean,默认值 true | - |
namePrefix | 线程名称前缀,string,默认值 tomcat-exec- | 后面会拼接线程编号 |
maxThreads | 最大线程数,int,默认值 200 | - |
minSpareThreads | 最小线程数,int,默认值 25 | 当线程数超过minSpareThreads 时,多出的线程空闲一段时间会被回收。如果并发数较低,可以适当调小。并发数较高可适当增加 |
maxIdleTime | 线程最大空闲时间,int,默认值 60000 毫秒(1 分钟) | 线程空闲时间超过这个值会被回收,直到线程数剩下 minSpareThreads 个 |
prestartminSpareThreads | 是否在启动时就创建 minSpareThreads 个线程,boolean,默认值 false | 如果是并发比较高,可以设置为true ,在启动时对线程池预热。 |
maxQueueSize | 线程池队列长度,int,默认值 Integer.MAX_VALUE | - |
与java 线程池不同,tomcat 自定义了一个线程池实现,当执行任务线程数 < maxThreads
时,会创建新的线程来执行任务,当 执行任务线程数 = maxThreads
时,新提交的任务才会放到队列中等待执行。
最关键的参数是如何设置maxThreads
,最大化利用 CPU 资源
- 设置小了,可能导致请求进入队列等待,随着并发数的增加 CPU 负载和使用率都没有增加,不能充分利用 CPU
- 设置大了,会增加额外线程切换时间,随着并发数的增加 CPU 负载增加,但是使用率没有显著增加,同样没有充分利用 CPU
线程池大小计算公式:maxThreads = 每秒请求数 * 平均请求耗时 * CPU 核心数
用单核 CPU 举例
需要线程数 | QPS(每秒请求数) | 平均请求耗时(秒) |
---|---|---|
10 | 10 | 1 |
20 | 10 | 2 |
500 | 500 | 1 |
所以只要能知道QPS
和平均请求耗时
就可以确定线程数
QPS
通过统计 accesslog 可以获得,统计的方法很多,例如可以用 elk 收集(这里不展开),在 kibana 上查询。
平均请求耗时
- 平均请求耗时 = IO 阻塞时间 + CPU 处理时间
- 平均请求耗时 = IO 阻塞时间 + CPU 计算时间 + 线程上下文切换时间
对于 IO 密集型的程序,请求耗时住要包括
IO 阻塞时间
和CPU 处理时间
。IO 阻塞时间
和CPU 计算时间
在服务稳定的情况下比较稳定的。只有线程上下文切换时间会随着线程数增加而增加,所以当线程过多时,再增加新的线程不一定能提升吞吐量(线程过多导致 CPU 大部分时间都在做线程切换而不是计算,也就是总的平均请求时间也会增加),所以线程数太多或太少都会影响服务吞吐量。我们可以在访问量较低的时候统计出平均耗时,然后根据公式计算出一个初始值,反复压测增加或减少调整线程数,得到一个最优值。平均请求时间的计算同样可以通过 accesslog 计算,响应时间总和 / 请求次数
# 监控
服务在 7 * 24 小时运行过程中各项数据都可能发生变化,比如:并发数、平均请求时间(随着数据量增加,相同的数据查询耗时也会增加,另外随着需求的不断迭代,代码的复杂度也会增加)。所以非常有必要对服务的指标(吞吐量、响应时间、错误数、线程池、CPU 以及 JVM 内存等)做监控,通过监控判断服务运行是否监控,参数配置是否合理。
Tomcat 通过暴露 JMX 接口提供给开发、运维人员查询相关数据,只需要在启动的时候添加以下 JMX 配置参数,然后通过 jdk 提供的 jconsole 工具连接 JMX 接口就可以可视化观察 Tomcat 运行状态。
export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote"
export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote.port=9001"
export JAVA_OPTS="${JAVA_OPTS} -Djava.rmi.server.hostname=0.0.0.0"
export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote.ssl=false"
export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote.
2
3
4
5
用 jconsole 连接 Tomcat JMX
jconsole 192.168.3.3:9001
常见问题:
- 当看到 GC 频繁时,可以适当调整堆大小,如果是 CMS 收集器,YGC 次数频繁,则适当增加新生代空间。FGC 频繁,则适当增加老年代空间
- 当系统 QPS 没有明显增加,但是活跃线程数增加较多时,说明平均耗时增加了,需要排查下游服务具体是什么原因导致请求平均耗时增加
# 完整配置参考
tomcat 版本:8.5.x
bin/setenv.sh
# tomcat.pid CATALINA_PID="$CATALINA_BASE/logs/tomcat.pid" export LOG_PATH="/path/to/log" # jvm # JAVA_HOME=/path/to/java export JAVA_OPTS="-Djava.security.egd=file:/dev/urandom -Dfile.encoding=UTF-8" export JAVA_OPTS="-Xmx4G -Xms4G -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+ParallelRefProcEnabled" export JAVA_OPTS="-XX:ErrorFile=${LOG_PATH}/hs_err_pid%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_PATH} -XX:+HeapDumpBeforeFullGC -XX:+HeapDumpAfterFullGC -Xloggc:${LOG_PATH}/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintCommandLineFlags" # apr export JAVA_OPTS="${JAVA_OPTS} -Djava.library.path=/usr/local/apr/lib" # jmx export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote" export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote.port=9001" export JAVA_OPTS="${JAVA_OPTS} -Djava.rmi.server.hostname=0.0.0.0" export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote.ssl=false" export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote.authenticate=false" UMASK="0022"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22conf/server.xml
<?xml version="1.0" encoding="UTF-8"?> <Server port="8005" shutdown="SHUTDOWN"> <!-- Security listener. Documentation at /docs/config/listeners.html <Listener className="org.apache.catalina.security.SecurityListener" /> --> <!--APR library loader. Documentation at /docs/apr.html <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" /> --> <Listener className="org.apache.catalina.startup.VersionLoggerListener" /> <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /> <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /> <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" /> <Service name="Catalina"> <Executor name="tomcatThreadPool" namePrefix="tomcat-thread-pool-exec-" prestartminSpareThreads="true" maxThreads="2000" maxQueueSize="100" minSpareThreads="50" maxIdleTime="10000" /> <Connector port="8081" protocol="org.apache.coyote.http11.Http11NioProtocol" enableLookups="false" connectionTimeout="15000" redirectPort="8443" executor="tomcatThreadPool" compression="on" compressableMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/javascript" URIEncoding="utf-8" /> <Engine name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps"> <Valve className="org.apache.catalina.valves.RemoteIpValve" remoteIpHeader="x-real-ip" proxiesHeader="x-forwarded-by" protocolHeader="x-forwarded-proto" /> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="access_log" suffix=".log" pattern="%h %l %u %t "%r" %s %b" /> <Context docBase="/path/to/app/demo" path="" reloadable="false" sessionCookiePath="/" sessionCookieName="APP_SESSION" useHttpOnly="true"> <Resources allowLinking="true"></Resources> </Context> </Host> </Engine> </Service> </Server>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 官方配置说明
https://tomcat.apache.org/tomcat-8.5-doc/config/http.html#Connector_Comparison (opens new window)