关于tomcat性能优化
前言
关于 Tomcat 性能调优,一直以来就是运维面试的一个重要话题。今天我们就简单聊聊 Tomcat 如何进行性能优化? 首先声明,我不会去说 Tomcat 是什么,内部结构,原理什么的。我不懂......我只是会说一些我在工作当中的一些参数以及自己所了解的方法,主要还是和大家沟通、交流。
一、关于选型
简单说明,关于Openjdk 和 Oracle jdk的选择,我个人比较倾向于使用Oracle jdk,虽然它很流氓但是我觉得东西还是靠谱;其次是在版本上的选择,建议选择最新稳定版进行在生产环境使用,以此来获得更高效的性能;
一、JVM相关参数优化
export JAVA_OPTS=""-Dfile.encoding=UTF-8 -server -Xms1400M -Xmx1400M -Xss512k -XX:+AggressiveOpts -XX:+UseBiasedLocking -XX:PermSize=128M -XX:MaxPermSize=256M -XX:+DisableExplicitGC -XX:MaxTenuringThreshold=30 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:ReservedCodeCacheSize=32m -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -Djava.awt.headless=true"
提示:在java 8中永久代已被移除,如果你是java8的环境,需要你去掉"-XX:PermSize=128M -XX:MaxPermSize=256M"参数,并且"-XX:MaxTenuringThreshold=31"参数值只能设置在0-15,否则会提示相关错误;
配置说明:
- Tomcat 默认是以 java -client 的方式运行,server 意味着是已真正的生产环境来运行,这样可以获得更高的并发、更高效的垃圾回收能力;
- Xms、Xmx表示JVM 最小内存初始值和最大内存初始值,建议设置为相同参数,以减少CPU对内存资源的调度,避免CPU高速运转进行的垃圾回收;
- Xmn设置年轻代大小为512m。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8;
- -Xss是指设定每个线程的堆栈大小。这个就要依据你的程序,看一个线程 大约需要占用多少内存,可能会有多少线程同时运行等。一般不易设置超过1M,要不然容易出现out ofmemory;
- -XX:+AggressiveOpts作用如其名(aggressive),启用这个参数,则每当JDK版本升级时,你的JVM都会使用最新加入的优化技术;
- -XX:+UseBiasedLocking启用一个优化了的线程锁,我们知道在我们的appserver,每个http请求就是一个线程,有的请求短有的请求长,就会有请求排队的现象,甚至还会出现线程阻塞,这个优化了的线程锁使得你的appserver内对线程处理自动进行最优调配;
- -XX:PermSize=128M -XX:MaxPermSize=256M JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;在数据量的很大的文件导出时,一定要把这两个值设置上,否则会出现内存溢出的错误。由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。那么,如果是物理内存4GB,那么64分之一就是64MB,这就是PermSize默认值,也就是永生代内存初始大小;四分之一是1024MB,这就是MaxPermSize默认大小;
- -XX:+DisableExplicitGC在程序代码中不允许有显示的调用”System.gc()”。调用System.gc()付出的代价就是系统响应时间严重降低,就和我在关于Xms,Xmx里的解释的原理一样;
- -XX:+UseParNewGC 对年轻代采用多线程并行回收。
- -XX:+UseConcMarkSweepGC 即CMS gc,这一特性只有jdk1.5即后续版本才具有的功能,它使用的是gc估算触发和heap占用触发。我们知道频频繁的GC会造面JVM的大起大落从而影响到系统的效率,因此使用了CMS GC后可以在GC次数增多的情况下,每次GC的响应时间却很短,比如说使用了CMS GC后经过jprofiler的观察,GC被触发次数非常多,而每次GC耗时仅为几毫秒;
- -XX:+CMSParallelRemarkEnabled在使用UseParNewGC 的情况下, 尽量减少mark的时间;
- -XX:+UseCMSCompactAtFullCollection在使用concurrent gc 的情况下, 防止 memoryfragmention, 对live object 进行整理, 使 memory 碎片减少;
- -XX:LargePageSizeInBytes指定 Java heap的分页页面大小;
- -XX:CMSInitiatingOccupancyFraction=70CMSInitiatingOccupancyFraction,这个参数设置有很大技巧,基本上满足(Xmx-Xmn)*(100- CMSInitiatingOccupancyFraction)/100>=Xmn就 不会出现promotion failed。在我的应用中Xmx是6000,Xmn是512,那么Xmx-Xmn是5488兆,也就是年老代有5488 兆,CMSInitiatingOccupancyFraction=90说明年老代到90%满的时候开始执行对年老代的并发垃圾回收(CMS),这时还 剩10%的空间是5488*10%=548兆,所以即使Xmn(也就是年轻代共512兆)里所有对象都搬到年老代里,548兆的空间也足够了,所以只要满 足上面的公式,就不会出现垃圾回收时的promotion failed,因此这个参数的设置必须与Xmn关联在一起;
Tomcat Connector(Tomcat连接器)有bio、nio、apr三种运行模式,简单介绍(抄百度的,有本事来打我):
- 1. bio(blocking I/O),即阻塞式I/O操作,表示Tomcat使用的是传统的Java I/O操作(即java.io包 及其子包)。Tomcat在默认情况下(敲黑板,主要是在tomcat7或以下的版本),就是以bio模式运行的。遗憾的是,就一般而言,bio模式是三种运行模式中性能最低的一种。
特性:一个线程处理一个请求。缺点:并发量高时,线程数较多,浪费资源。
- 2. nio(new I/O),是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。Java nio是一个基于缓冲区、并能提供非阻塞I/O操作的Java API,因此nio也被看成是non-blocking I/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。nio目前存在nio以及nio2模式,因此到了Tomcat8.5和Tomcat9.0,则去掉了对BIO的支持。tomcat 8默认以 nio 启动,其他版本自己百度怎么修改为 nio,在启动的时候观察日志:
12-Dec-2017 14:17:46.429 信息 [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
12-Dec-2017 14:17:46.445 信息 [main] org.apache.tomcat.util.net.NioSelectorPool.getSharedSelector Using a shared selector for servlet write/read
12-Dec-2017 14:17:46.473 信息 [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["ajp-nio-8009"]
12-Dec-2017 14:17:46.474 信息 [main] org.apache.tomcat.util.net.NioSelectorPool.getSharedSelector Using a shared selector for servlet write/read
12-Dec-2017 14:17:46.476 信息 [main] org.apache.catalina.startup.Catalina.load Initialization processed in 1031 ms
特性:利用Java的异步IO处理,可以通过少量的线程处理大量的请求。
- 3. apr(Apache Portable Runtime/Apache可移植运行时),是Apache HTTP服务器的支持库。在tomcat中,又被称为 tomcat native,从操作系统层面解决io阻塞问题。是一个利用 APR 来提升Tomcat性能的本地API。怎么配置?自己查!生产环境,强烈推荐。
3.2 tomcat server.xml配置优化
线程池参数配置:
<?xml version="1.0" encoding="UTF-8"?>
<Service name="Catalina">
<!--The connectors can use a shared executor, you can define one or more named thread pools-->
<!--
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4"/>
-->
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="1000"
minSpareThreads="100"
maxIdleTime="60000"
prestartminSpareThreads = "true"
maxQueueSize = "100"
className="org.apache.catalina.core.StandardThreadExecutor" />
- URIEncoding="UTF-8" 使得tomcat可以解析含有中文名的文件的url;
- minSpareThreads 最小备用线程数,tomcat启动时的初始化的线程数。
- enableLookups 消除DNS查询对性能的影响我们可以关闭DNS查询。
- connectionTimeout为网络连接超时时间毫秒数。
- maxThreads Tomcat使用线程来处理接收的每个请求。这个值表示Tomcat可创建的最大的线程数,即最大并发数。建议值为500-1000,具体根据服务器性能以及压测数据结果测试;
- acceptCount是当线程数达到maxThreads后,后续请求会被放入一个等待队列,这个acceptCount是这个队列的大小,如果这个队列也满了,就直接refuse connection。
- maxProcessors与minProcessors在 Java中线程是程序运行时的路径,是在一个程序中与其它控制线程无关的、能够独立运行的代码段。它们共享相同的地址空间。多线程帮助程序员写出CPU最 大利用率的高效程序,使空闲时间保持最低,从而接受更多的请求。
提示:网上很多混淆了,异步servlet和非阻塞connector,一个是Executor,一个是connector,两者的工作阶段不同。
连接器配置:
<Connector executor="tomcatThreadPool" URIEncoding="utf-8" port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" enableLookups="false" maxConnections="2000" useURIValidationHack="false" keepAliveTimeout="60000" connectionTimeout="20000"
maxThreads="1000"
minSpareThreads="100"
maxSpareThreads="2000"
minProcessors="100"
maxProcessors="1000" tcpNoDelay="true" redirectPort="8443" />
- maxThreads="X" 表示最多同时处理X个连接;
- minSpareThreads="X" 初始化X个连接;
- maxSpareThreads="X" 表示如果最多可以有X个线程,一旦超过X个,则会关闭不在需要的线程;
- acceptCount="X" 当同时连接的人数达到maxThreads时,还可以排队,队列大小为X.超过X就不处理;
提示:Tomcat 中可以同时接收的连接数为maxConnections+acceptCount 。上面这些参数需要手动开启,默认值配置都较低,无法发挥最佳性能。可以去你们生产环境看看,是不是很多没有做优化呢;
四、关于性能分析
4.1 常见工具
- 1 jmx 端口获取监控信息(比如,zabbix);
- 2 jps/jstat/jmap等命令进行调试;
- 3 java agent方式获取以及代码植入以探针的方式监控(开源PinPoint);
4.2 其他
查看tomcat连接数:
netstat –nat | grep 8080
查看 tomcat 线程数:
# 查看进程ID ps -ef |grep java # 查看线程 ps -o nlwp 3598
提示:通过使用该方式可以查看到该进程有多少线程,但并没有排除处于idle状态的线程。所以,要想获取当前进程running的线程数,还需要执行如下命令。
ps -eLo pid,stat | grep 3598 | grep running | wc -l
其中ps -eLo pid,stat可以找出所有线程,并打印其所在的进程号和线程当前的状态;两个grep命令分别筛选进程号和线程状态;wc统计个数。SL表示空闲状态。