铁匠 铁匠
首页
收藏
java
架构之路
常用算法
  • Java
  • nginx
  • 系统运维
  • 系统安全
  • mysql
  • redis
参考文档
关于
链接
  • 分类
  • 标签
  • 归档

专注、不予评判地关注当下
首页
收藏
java
架构之路
常用算法
  • Java
  • nginx
  • 系统运维
  • 系统安全
  • mysql
  • redis
参考文档
关于
链接
  • 分类
  • 标签
  • 归档
  • 概览

  • 设计模式

  • 高可用

  • 性能优化

    • 性能测试工具

      • 性能测试神器 wrk 使用教程
    • tomcat 性能优化
      • I/O 模型选择
        • 相关配置
        • apr 依赖库安装
      • 线程池配置
      • 监控
      • 完整配置参考
      • 官方配置说明
  • 分布式

  • 网关

  • 流量治理

  • 数据治理

  • 云原生

  • 网络安全

  • 架构
  • 性能优化
FengJianxin
2021-01-06
目录

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" />
1
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
1
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" />
1
2
3
4
export JAVA_OPTS="${JAVA_OPTS} -Djava.library.path=/usr/local/apr/lib"
1

# 线程池配置

参数说明,在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 上查询。

    accesslog

  • 平均请求耗时

    • 平均请求耗时 = 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.
1
2
3
4
5

用 jconsole 连接 Tomcat JMX

jconsole 192.168.3.3:9001
1

jconsole

常见问题:

  1. 当看到 GC 频繁时,可以适当调整堆大小,如果是 CMS 收集器,YGC 次数频繁,则适当增加新生代空间。FGC 频繁,则适当增加老年代空间
  2. 当系统 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
    22
  • conf/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 &quot;%r&quot; %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)

#tomcat#性能优化
性能测试神器 wrk 使用教程
zookeeper介绍

← 性能测试神器 wrk 使用教程 zookeeper介绍→

最近更新
01
策略模式
01-09
02
模板方法
01-06
03
观察者模式
01-06
更多文章>
Theme by Vdoing | Copyright © 2016-2023 铁匠 | 粤ICP备15021633号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式