Tomcat自身的调优是针对conf/server.xml中的几个参数的调优设置。首先是对这几个参数的含义要有深刻而清楚的理解。以tomcat8.5为例,讲解参数。

同时也得认识到一点,tomcat调优也受制于linux内核。linux内核对tcp连接也有几个参数可以调优。

因此可以将tomcat调优分为:

  • linux内核优化
  • java虚拟机调优
  • tomcat自身优化

1 tomcat自身优化

1.1 maxThreads 

tomcat创建的最大线程数,也就是同时处理的请求最大并发数。默认值是200。

1.1.1 maxThreads如何配置

一般的服务器操作都包括量方面:

  • 计算(主要消耗cpu)
  • 等待(io、数据库等)

第一种极端情况,如果业务逻辑是纯粹的计算,那么系统响应时间的主要限制就是cpu的运算能力,此时maxThreads应该尽量设的小,降低同一时间内争抢cpu的线程个数,可以提高计算效率,提高系统的整体处理能力。

第二种极端情况,如果我业务逻辑纯粹是IO或者数据库,那么响应时间的主要限制就变为等待外部资源,此时maxThreads应该尽量设的大,这样才能提高同时处理请求的个数,从而提高系统整体的处理能力。此情况下因为tomcat同时处理的请求量会比较大,所以需要关注一下tomcat的虚拟机内存设置和linux的open file限制。

在测试时遇到一个问题,maxThreads设置的比较大。比如3000,当服务的线程数大到一定程度时,一般是2000出头,单次请求的响应时间就会急剧的增加。这是为什么呢?原因可能是cpu在线程切换时消耗的时间随着线程数量的增加越来越大,cpu把大多数时间都用来在这2000多个线程直接切换上了,当然cpu就没有时间来处理程序逻辑了。

其实多线程本身并不能提高cpu效率,线程过多反而会降低cpu效率。

当cpu核心数<线程数时,cpu就需要在多个线程直接来回切换,以保证每个线程都会获得cpu时间,即并发执行。所以maxThreads的配置绝对不是越大越好。

现实应用中,业务逻辑一般都会包含以上两种类型(计算、等待),所以maxThreads的配置并没有一个最优值,一定要根据具体情况来配置。

最好的做法是:在不断测试的基础上,不断调整、优化,才能得到最合理的配置。

1.2 acceptCount

当tomcat的线程数达到了最大时,接收排队的最大请求个数。默认值为100。

maxThreads与acceptCount这两个值是如何起作用的呢?

情况1:接受一个请求,此时tomcat启动的线程数没有到达maxThreads,tomcat会起动一个线程来处理此请求。

情况2:接受一个请求,此时tomcat起动的线程数已经到达maxThreads,tomcat会把此请求放入等待队列,等待空闲线程。

情况3:接受一个请求,此时tomcat起动的线程数已经到达maxThreads,等待队列中的请求个数也达到了acceptCount,此时tomcat会直接拒绝此次请求,返回connection refused。

对于第3种情况,在看过一篇分析connection timeout问题产生的原因后,等待队列的请求个数这个值可能是由acceptCount参数决定,也有可能由linux内核参数net.core.somaxconn决定。

关联:

网上有分析linux上TCP connection timeout的原因,这篇文章中提到一个内核参数 net.core.somaxconn。那么tomcat的acceptCount与net.core.somaxconn到底是什么关系呢。 

参考实验现象分析:

1. 将tomcat的acceptCount设置为3000 ,net.core.somaxconn设置为8192

那么用ss -lt 指令查看在tomcat起的端口上的send_q值是3000  可见这是acceptCount的值。

2.将tomcat的acceptCount设置为10000,net.core.somaxconn设置为8192

同样用ss -lt指令查看在tomcat起的端口上的send_q值是8192,可见这是somaxconn的值。

所以acceptCount设置的值要一般小于net.core.somaxconn这个参数,这样acceptCount的值才会起作用。net.core.somaxconn 这个参数默认值是128 ,所以需要改这个参数值。后面再介绍改这个值的方法。

1.2.1 acceptCount如何配置

一般是设置的跟maxThreads一样大,这个值应该是主要根据应用的访问峰值与平均值来权衡配置的。

  • 如果设的较小,可以保证接受的请求较快相应,但是超出的请求可能就直接被拒绝
  • 如果设的较大,可能就会出现大量的请求超时的情况,因为我们系统的处理能力是一定的。

1.3 maxConnections

maxConnections是指Tomcat允许的同时存在的最大连接数。

1.4 connectionTimeOut

connectionTimeOut=10000是说建立一个socket连接后,如果一直没有收到客户端的FIN,也没有数据过来,那么此连接也必须等到10s后,才能被超时释放,tomcat就直接释放这个连接。以毫秒为单位,server.xml默认设置是20秒。


修改方法:
  vi server.xml 打开server.xml文件
将 
<!--
 <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
 maxThreads="150" minSpareThreads="4"/>
 -->
修改为:
  <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="1500" minSpareThreads="50" prestartminSpareThreads="true"/>

将

<Connector 
 port="8080" 
 protocol="HTTP/1.1" 
 connectionTimeout="20000" 
 redirectPort="8443" 
 />
修改为

 <Connector executor ="tomcatThreadPool" port="8009" protocol="org.apache.coyote.http11.Http11Nio2Protocol"  connectionTimeout="20000" maxConnections="10000"  redirectPort="8443" acceptCount="1500"/>

下面的图为TCP三次握手与accept交互:

Tomcat调优总结-LMLPHP

SYN队列称为半连接队列,由内核参数 net.ipv4.tcp_max_syn_backlog 设置.

Accept队列称为完全连接队列,三次握手已经完成,但还未被应用层接收(accept),但也处于ESTABLISHED状态。队列长度由listen的backlog参数和内核的 net.core.somaxconn 参数共同决定。由listen()函数的第二个参数 backlog 指定,内核硬限制由 net.core.somaxconn 限制,即队列长度实际的值由min(backlog,somaxconn) 来决定

客户端使用connect向服务器发送TCP连接,三次握手就发生了。

当1.1步骤 客户端首先发送SYN到达服务端后,内核会把连接信息放到SYN队列中,同时回一个SYN+ACK包给客户端。一段时间后,客户端再次发来ACK包后,内核会把连接从SYN队列中取出,再把这个连接放到ACCEPT队列中。应用服务器调用accept时,其实就是直接从ACCEPT队列中取出已经建立成功的连接套接字。

还有一张图是TCP握手建立连接的流程和队列

Tomcat调优总结-LMLPHP

1.5 Tomcat原理概要 

Tomcat大致分为两个部分,Connector组件及Container组件。Connector组件负责控制入口连接,并关联着一个Executor。Container负责Servlet容器的实现,Executor负责具体的业务逻辑,如Servlet的执行。一个请求到达服务器后,经过以下关键几步,参见下图:

Tomcat调优总结-LMLPHP

  1. OS与客户端握手并建立连接,并将建立的连接放入完成队列,不妨叫Acceptor Queque。这个队列的长度就是Connector的acceptCount值。

  2. Tomcat中的acceptor线程,不断从Acceptor Queque中获取连接。

  3. Acceptor Queque队列中没有连接,Acceptor线程继续监视

  4. Acceptor Queque队列中有新连接,Acceptor线程将检查当前的连接数是否超过了maxConnections

  5. 如果超过maxConnections,则阻塞。直到连接数小于maxConnections,acceptor线程将请求交由Executor负责执行。

  6. Executor将分配worker线程来处理请求数据的读取,处理(servlet的执行)以及响应。

acceptCount

acceptCount 实际上是Bind Socket时候传递的backlog值,在linux平台下含义是已经建立连接还没有被应用获取的连接队列最大长度。此时,如果请求个数达到了acceptCount,新进的请求将抛出refuse connection.

2 Linux内核参数优化

2.1 linux系统对当前用户的单一进程同时可打开的文件数量的限制

 查看系统允许当前用户进程打开的文件数量的限制:ulimit -u ,默认值为1024 。即是Linux操作系统对一个进程打开的文件句柄数量的限制。

对于想支持更高数量的TCP并发连接的通讯处理程序,就必须修改Linux对当前用户的进程同时打开的文件数量的软限制(soft limit)和硬限制(hardlimit)。其中软限制是指Linux在当前系统能够承受的范围内进一步限制用户同时打开的文件数;硬限制则是根据系统硬件资源状况(主要是系统内存)计算出来的系统最多可同时打开的文件数量。通常软限制小于或等于硬限制。

修改方法:

sudo vi /etc/security/limits.conf 

增加如下:

prouser  soft   nofile 65536
prouser  hard nofile 65536

prouser  soft  nproc 65536

prouser hard nproc 65536

修改完后保存此文件。

2.2 Linux网络内核对TCP连接的有关限制 

修改方法:

sudo vi /etc/sysctl.conf
增加如下:
net.ipv4.tcp_tw_reuse = 1 
net.ipv4.tcp_tw_recycle = 1 
net.ipv4.tcp_fin_timeout = 30 
net.ipv4.ip_local_port_range = 10000 65000 
net.ipv4.tcp_max_syn_backlog = 8192 
net.ipv4.tcp_max_tw_buckets = 10000
net.core.somaxconn=8192      accept队列的长度跟这个参数有关
 
sudo /sbin/sysctl -p
实时生效

JVM调优

JAVA_OPTS="$JAVA_OPTS -server -Xmn2000m -Xms4000m -Xmx4000m -XX:PermSize=128m -XX:+UseConcMarkSweepGC -XX:MaxPermSize=512m -Djuli-logback.configurationFile=file:$CATALINA_HOME/conf/logback.xml"

默认值:

<!--
 <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
 maxThreads="150" minSpareThreads="4"/>
 -->

修改为:

<Executor
 name="tomcatThreadPool"
 namePrefix="catalina-exec-"
 maxThreads="500"
 minSpareThreads="30"
 maxIdleTime="60000"
 prestartminSpareThreads = "true"
 maxQueueSize = "100"
/>

参数解释:

  • maxThreads:最大并发数,默认设置 200,一般建议在 500 ~ 800,根据硬件设施和业务来判断
  • minSpareThreads:Tomcat 初始化时创建的线程数,默认设置 25
  • maxIdleTime:如果当前线程大于初始化线程,那空闲线程存活的时间,单位毫秒,默认60000=60秒=1分钟。
  • prestartminSpareThreads:在 Tomcat 初始化的时候就初始化 minSpareThreads 的参数值,如果不等于 true,minSpareThreads 的值就没啥效果了
  • maxQueueSize:最大的等待队列数,超过则拒绝请求

4 生产环境调优实战

4.1 tomcat 启动慢

在线上环境中,我们经常会遇到类似的问题,就是tomcat 启动比较慢,查看内存和cpu,io都是正常的,但是启动很慢,有的时候长达几分钟,这到底是什么原因导致的?

4.1.1 tomcat 获取随机值阻塞

tomcat的启动需要产生session id,这个产生需要通过java.security.SecureRandom生成随机数来实现,随机数算法使用的是”SHA1PRNG”,但这个算法依赖于操作系统的提供的随机数据,在linux系统中,这个值又依赖于/dev/random 和/dev/urandom

/dev/random :阻塞型,读取它就会产生随机数据,但该数据取决于熵池噪声,当熵池空了,对/dev/random 的读操作也将会被阻塞。
/dev/urandom: 非阻塞的随机数产生器,它会重复使用熵池中的数据以产生伪随机数据。这表示对/dev/urandom的读取操作不会产生阻塞,但其输出的熵可能小于/dev/random的。它可以作为生成较低强度密码的伪随机数生成器,不建议用于生成高强度长期密码。

我们通过查看java.security 文件,(我的java版本是1.8.0_131) 发现依赖的是/dev/random

Tomcat调优总结-LMLPHP

tomcat 启动产生session id 最终依赖的是/dev/random ,/dev/random 又依赖于熵池,

对于熵池,百度百科这样写到:

那么如何查看熵池 的大小,文件 /proc/sys/kernel/random/entropy_avail 保存着 熵池的大小。/proc/sys/kernel/random/poolsize 保存着熵池的最大容量,单位都是bit。

[root@haha cwd]# cat  /proc/sys/kernel/random/entropy_avail
146

总结

tomcat 启动慢的原因是随机数产生遭到阻塞,遭到阻塞的原因是 熵池大小 。

解决方法:

1. 更换产生随机数的源,(也是tomcat的官方文档的启动比较慢的解决办法)
2. 增大熵池 的值

1)更换产生随机数的源

因为/dev/urandom 是非阻塞的随机数产生器,所以我们可以从这边获取,但是生产的随机数的随机性比较低。我们可以在 我们的tomcat启动脚本(catalina.sh)里面添加

JAVA_OPTS="$JAVA_OPTS -Djava.security.egd=file:/dev/./urandom"

或者是更改java的java.security 文件,将securerandom.source=file:/dev/random

securerandom.source=file:/dev/./urandom

2)增大熵池 的值

要增大熵池 的值首先得你的cpu支持DRNG 特性, 如何查看我们的服务器的是否支持DRNG特性?

cat /proc/cpuinfo | grep rdrand

如果不支持的话,那么就只能通过上面的第一种方法来解决了

安装rngd服务(关于rngd服务的介绍)

yum -y install rng-tools
systemctl enable   rngd
systemctl start  rngd

然后我们进行查看我们的熵池 的值,会发现变大了

 cat  /proc/sys/kernel/random/entropy_avail

然后我们启动tomcat 会发现启动速度快很多。

4.1.2 tomcat 需要部署的web应用程序太多

有时候tomcat启动比较慢是因为它需要部署的web应用程序太多,但是其中有些应用程序是不需要的,比如在webapps下的 doc 、example、ROOT 等等,可以将不需要的webapps删除,然后再进行发布,这些不需要的web,不仅会占用资源,还有可能是入侵者的入侵对象。如果想并行启动多个web应用程序,我们可以Host 的属性 startStopThreads 值设置大于1 ,但这也取决于我们的服务器是不是多核的。如果是多核的建议调大 startStopThreads 的值,但不超过内核数。

4.1.3 tomcat启动内存不足

如果是项目比较大的话,使用默认的参数去启动的tomcat是很有可能内存不足的,需要设置JVM,将内存调整,JVM 的最大值和最小值建议是不要相差太大(最好一致.)

在启动脚本catalina.sh加上:

JAVA_OPTS='-server -Xms1024m -Xmx1024m'

具体的内存大小,根据业务调整。

以上就是解决tomcat 启动慢的问题和解决方案,可根据自己的项目情况进行使用。

4.2 Connector 调优

4.2.1 使用arp 连接器

tomcat 可以使用Apache Portable Runtime来提供更高的性能服务。Apache Portable Runtime是一个高度可移植的库,是Apache HTTP Server 2.x的核心。APR有许多用途,包含高级的io功能(sendfile,epoll,Openssl),系统级别功能(产生随机数,系统状态)和进程处理(共享内存,NT管道,unix套接字),它可以让tomcat成为通用的web服务器,让java应用作为一个完整的web服务器更加可行,而不是仅仅作为后端的技术。

总结:从系统级别来解决异步io的问题,提升性能。

apr 连接器需要自己手动安装,需要以下组件

  • APR library (需要手动下载安装) tomcat 8.5 需要 APR 1.2+

  • OpenSSL libraries (需要安装)

  • JNI wrappers for APR used by Tomcat (libtcnative) (tomcat 安装包已经提供)

4.2.1.1 yum安装(建议)

注意apr安装的版本

yum -y install openssl
yum -y install apr
yum -y install apr-util
yum -y install tomcat-native
4.2.1.2 源码安装
4.2.1.2.1 apr源码编译

安装流程:https://www.cnblogs.com/zscc/p/9349908.html

1)安装依赖并下载apr 包

下载地址 : http://archive.apache.org/dist/apr/

yum  -y install make gcc gcc-c++  openssl-devel libtool
cd  /tmp  &&  wget  http://archive.apache.org/dist/apr/apr-1.6.5.tar.gz

2)解压apr 包

tar -xzf  apr-1.6.5.tar.gz &&  cd apr-1.6.5

3)预编译,编译,安装

./configure   --prefix=/usr/local/apr  
make  
make  install   
4.2.1.2.2 native 安装

1)解压native 包 (native 包在tomcat 安装包里面 bin下)

cd  tomcat/bin && tar -xzf tomcat-native.tar.gz

2)预编译,编译,安装

cd tomcat-native*/native &&  ./configure  --with-apr=/usr/local/apr 
make 
make install 

3)添加环境变量

vim  + /etc/profile
# 添加环境变量
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/apr/lib

# 加载变量
source  /etc/profile 
4.2.1.3 配置apr 连接器 
vim  ./conf/server.xml

# 将 Connector 属性protocol 配置成 apr(org.apache.coyote.http11.Http11AprProtocol)
   <Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol"
               connectionTimeout="20000"
               redirectPort="8443" />

默认连接器为nio,共有三大连接器:

  • org.apache.coyote.http11.Http11NioProtocol 非阻塞nio连接器

  • org.apache.coyote.http11.Http11Nio2Protocol 非阻塞nio2连接器

  • org.apache.coyote.http11.Http11AprProtocol APR连接器

重启tomcat

Tomcat调优总结-LMLPHP

4.2.1.4 APR 测试

tomcat,jvm使用默认配置进行测试,使用nio 连接器 共1000次请求100并发

Tomcat调优总结-LMLPHP

在nio连接器时,我多次尝试使用150并发共1000请求去测试,发现一直成功不了,但是当我使用apr连接时,

1)100并发

Tomcat调优总结-LMLPHP

2)150 并发

Tomcat调优总结-LMLPHP

我们可以对比发现,在使用APR连接器在处理高并发请求的时候是有一定的优势的,能够提升web应用的处理请求能力。

4.2.2 Connector 其它属性调优

以上设置只是参考,建议根据自己的业务进行设置,并测试。
有很多参数tomcat 默认就给我设置好了,比如URIEncoding,connectionTimeout,这些值都有默认值的。

4.2.3 Host 属性调优

如果想并行启动多个web应用程序,我们可以Host 的属性 startStopThreads 值设置大于1 ,但这也取决于我们的服务器是不是多核的。如果是多核的建议调大 startStopThreads 的值,但不超过内核数。

4.2.4 tomcat线程关闭不掉调优(代码层)

如果我们使用的是自动发布的形式,也就是替换war包的形式,在tomcat安装文章中介绍了与jenkins 集成实现的,那么容易出现的一个问题就是 上一次的线程没有关闭掉,就启动了新的版本,那么上一个版本的线程还存在,也还在占用着资源,这个问题的原因有可能是代码的问题,我们可以尝试直接使用 catalina.sh 脚本stop ,你会发现stop 掉后,该tomcat 的线程还是存在的,那么出现这种情况很大一部分原因就是 在java代码中有非守护线程,也就是java代码未将线程设置为守护线程,导致了tomcat 进行stop 不掉该线程的原因。那么这种情况有可能会导致两个web应用(新老版本)都在使用,那这这个应用的定时任务可能就会执行两次,就容易导致生产事故。

这种问题,一方面会占用服务器资源。另外一方面还会容易导致生产事故,我们可以用jstack 分析下未停止的线程,并和开发区解决这个线程关闭不掉的问题。

4.2.5 AJP 连接器禁用

AJP协议在tomcat中的作用就是将该服务与其它HTTP服务器集成,一般项目中,没有用到该连接器,所以可以禁用该连接器。

4.3 JVM 设置

在前面启动内存不足就谈到了

JAVA_OPTS='-server -Xms1024m -Xmx1024m'

其他的调优选项,如果有多个connector 的话,那么还可以通过 Executor 的属性进行调优。

04-02 06:03