Tomcat参数优化
从这个图中可以得出,限制Tomcat请求数量的因素四个方面。
当前服务器系统资源
我想可能大家遇到过类似“Socket/File:Can't open so many files”的异常,这个就是表示Linux系统中的文件句柄限制。
在Linux中,每一个TCP连接会占用一个文件描述符(fd),一旦文件描述符超过Linux系统当前的限制,就会提示这个错误。
我们可以通过下面这条命令来查看一个进程可以打开的文件数量
ulimit -a 或者 ulimit -n
open files (-n) 1024 是linux操作系统对一个进程打开的文件句柄数量的限制(也包含打开的套接字数量)
这里只是对用户级别的限制,其实还有个是对系统的总限制,查看系统总线制:
cat /proc/sys/fs/file-max
file-max是设置系统所有进程一共可以打开的文件数量 。同时一些程序可以通过setrlimit
调用,设置每个进程的限制。如果得到大量使用完文件句柄的错误信息,是应该增加这个值。
当出现上述异常时,我们可以通过下面的方式来进行修改(针对单个进程的打开数量限制)
vi /etc/security/limits.conf
root soft nofile 65535
root hard nofile 65535
* soft nofile 65535
* hard nofile 65535
*
代表所有用户、root
表示root用户。- noproc 表示最大进程数量
- nofile代表最大文件打开数量。
- soft/hard,前者当达到阈值时,制作警告,后者会报错。
另外还要注意,要确保针对进程级别的文件打开数量反问是小于或者等于系统的总限制,否则,我们需要修改系统的总限制。
vi /proc/sys/fs/file-max
TCP连接对于系统资源最大的开销就是内存。
因为tcp连接归根结底需要双方接收和发送数据,那么就需要一个读缓冲区和写缓冲区,这两个buffer在linux下最小为4096字节,可通过cat /proc/sys/net/ipv4/tcp_rmem和cat /proc/sys/net/ipv4/tcp_wmem来查看。
所以,一个tcp连接最小占用内存为4096+4096 = 8k,那么对于一个8G内存的机器,在不考虑其他限制下,最多支持的并发量为:810241024/8 约等于100万。此数字为纯理论上限数值,在实际中,由于linux kernel对一些资源的限制,加上程序的业务处理,所以,8G内存是很难达到100万连接的,当然,我们也可以通过增加内存的方式增加并发量。
Tomcat依赖的JVM的配置
我们知道Tomcat是Java程序,运行在JVM上,因此我们还需要对JVM做优化,才能更好的提升Tomcat的性能,简单带大家了解一下JVM,如下图所示。
在JVM中,内存划分为堆、程序计数器、本地方发栈、方法区(元空间)、虚拟机栈。
堆空间说明
其中,堆内存是JVM内存中最大的一块区域,几乎所有的对象和数组都会被分配到堆内存中,它被所有线程共享。 堆空间被划分为新生代和老年代,新生代进一步划分为Eden和Surivor区,如下图所示。
新生代和老年代的比例是1:2,也就是新生代会占1/3的堆空间,老年代会占2/3的堆空间。 另外,在新生代中,空间占比为Eden:Surivor0:Surivor1=8:1:1 。 举个例子来说,如果eden区内存大小是40M,那么两个Survivor区分别是占5M,整个新生代就是50M,然后计算出老年代的内存大小是100M,也就是说堆空间的总内存大小是150M。
可以通过 java -XX:PrintFlagsFinal -version查看默认参数
uintx InitialSurvivorRatio = 8 uintx NewRatio = 2
InitialSurvivorRatio: 新生代Eden/Survivor空间的初始比例
NewRatio : Old区/Young区的内存比例
堆内存的具体工作原理是:
- 绝大部分的对象被创建之后,会保存在Eden区,当Eden区满了的时候,就会触发YGC(Young GC),大部分对象会被回收掉,如果还有活着的对象,就拷贝到Survivor0,这时Eden区被清空。
- 如果后续再次触发YGC,活着的对象Eden+Survivor0中的对象拷贝到Survivor1区, 这时Eden和Survivor0都会被清空
- 接着再触发YGC,Eden+Survivor1中的对象会被拷贝到Survivor0区,一直这么循环,直到对象的年龄达到阈值,则放入到老年代。(之所以这么设计,是因为Eden区的大部分对象会被回收)
- Survivor区装不下的对象会直接进入到老年代
- 老年代满了,会触发Full GC。
GC标记-清除算法 在执行过程中暂停其他线程??
程序计数器
程序计数器是用来记录各个线程执行的字节码地址等,当线程发生上下文切换时,需要依靠这个来记住当前执行的位置,当下次恢复执行后要沿着上一次执行的位置继续执行。
方法区
方法区是逻辑上的概念,在HotSpot虚拟机的1.8版本中,它的具体实现就是元空间。
方法区主要用来存放已经被虚拟机加载的类相关信息,包括类元信息、运行时常量池、字符串常量池,类信息又包括类的版本、字段、方法、接口和父类信息等。
方法区和堆空间类似,它是一个共享内存区域,所以方法区是属于线程共享的。
本地方发栈和虚拟机栈
Java虚拟机栈是线程私有的内存空间,当创建一个线程时,会在虚拟机中申请一个线程栈,用来保存方法的局部变量、操作数栈、动态链接方法等信息。每一个方法的调用都伴随这栈帧的入栈操作,当一个方法返回之后,就是栈帧的出栈操作。
本地方法栈和虚拟机栈类似,本地方法栈是用来管理本地方法的调用,也就是native方法。
JVM内存应该怎么设置
了解了上述基本信息之后,那么JVM中内存应该如何设置呢?有哪些参数来设置?
而在JVM中,要配置的几个核心参数无非是。
-
-Xms
,Java堆内存大小 -
-Xmx
,Java最大堆内存大小 -
-Xmn
,Java堆内存中的新生代大小,扣除新生代剩下的就是老年代内存新生代内存设置过小会频繁触发Minor GC,频繁触发GC会影响系统的稳定性
-
-XX:MetaspaceSize
,元空间大小, 128M -
-XX:MaxMetaspaceSize
,最大云空间大小 (如果没有指定这两个参数,元空间会在运行时根据需要动态调整。) 256M一个新系统的元空间,基本上没办法有一个测算的方法,一般设置几百兆就够用,因为这里面主要存放一些类信息。
-
-Xss
,线程栈内存大小,这个基本上不需要预估,设置512KB到1M就行,因为值越小,能够分配的线程数越多。
JVM内存的大小,取决于机器的配置,比如一个2核4G的服务器,能够分配给JVM进程也就2G左右,因为机器本身也需要内存,而且机器上还运行了其他的进程也需要占内存。而这2G还得分配给栈内存、堆内存、元空间,那堆内存能够得到的也就1G左右,然后堆内存还要分新生代、老年代。
Tomcat本身的配置
The maximum number of request processing threads to be created by this Connector, which therefore determines the maximum number of simultaneous requests that can be handled. If not specified, this attribute is set to 200. If an executor is associated with this connector, this attribute is ignored as the connector will execute tasks using the executor rather than an internal thread pool. Note that if an executor is configured any value set for this attribute will be recorded correctly but it will be reported (e.g. via JMX) as
-1
to make clear that it is not used.
server:
tomcat:
uri-encoding: UTF-8
#最大工作线程数,默认200, 4核8g内存,线程数经验值800
#操作系统做线程之间的切换调度是有系统开销的,所以不是越多越好。
max-threads: 1000
# 等待队列长度,默认100,
accept-count: 1000
max-connections: 20000
# 最小工作空闲线程数,默认10, 适当增大一些,以便应对突然增长的访问量
min-spare-threads: 100
-
accept-count: 最大等待数,当调用HTTP请求数达到tomcat的最大线程数时,还有新的HTTP请求到来,这时tomcat会将该请求放在等待队列中,这个acceptCount就是指能够接受的最大等待数,默认100。如果等待队列也被放满了,这个时候再来新的请求就会被tomcat拒绝(connection refused)
-
maxThreads:最大线程数,每一次HTTP请求到达Web服务,tomcat都会创建一个线程来处理该请求,那么最大线程数决定了Web服务容器可以同时处理多少个请求。maxThreads默认200,肯定建议增加。但是,增加线程是有成本的,更多的线程,不仅仅会带来更多的线程上下文切换成本,而且意味着带来更多的内存消耗。JVM中默认情况下在创建新线程时会分配大小为1M的线程栈,所以,更多的线程异味着需要更多的内存。线程数的经验值为:1核2g内存为200,线程数经验值200;4核8g内存,线程数经验值800。
-
maxConnections,最大连接数,这个参数是指在同一时间,tomcat能够接受的最大连接数。对于Java的阻塞式BIO,默认值是maxthreads的值;如果在BIO模式使用定制的Executor执行器,默认值将是执行器中maxthreads的值。对于Java 新的NIO模式,maxConnections 默认值是10000。对于windows上APR/native IO模式,maxConnections默认值为8192
如果设置为-1,则禁用maxconnections功能,表示不限制tomcat容器的连接数。
maxConnections和accept-count的关系为:当连接数达到最大值maxConnections后,系统会继续接收连接,但不会超过acceptCount的值。