阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

生产环境 Tomcat 调优实际操作

161次阅读
没有评论

共计 10854 个字符,预计需要花费 28 分钟才能阅读完成。

今天在新环境里部署 tomcat, 刚开始启动很快,关闭之后再启动,却发现启动日志打印到

00:25:14.144 [localhost-startStop-1] INFO  o.s.web.context.ContextLoader – Root WebApplicationContext: initialization completed in 6287 ms

一直 hold 着,tomcat 程序也无法访问,以为是程序哪里配置错了,找了半天,甚至把 spring 的配置加载完全去掉才能启动,why, 程序在开发环境可是刷刷刷就跑起来的

后来一直没管这程序过了几分钟去看日志,发现 tomcat 程序才启动完毕,why? 原来不是卡住,而是慢

用 jstack 观察一下启动线程, 发现 C2 CompilerThread 占用 cpu 很高,  同时 org.apache.catalina.util.SessionIdGeneratorBase.createSecureRandom 这里读文件也产生阻塞,占用 CPU 也很高。

一、问题描述

      在发布或重启某线上某服务时(jetty8 作为服务器),常常发现有些机器的 load 会飙到非常高(高达 70),并持续较长一段时间(5 分钟)后回落(图 1),与此同时响应时间曲线(图 2)也与 load 曲线一致。注:load 飙高的初始时刻是应用服务端口打开,流量打入时(load)。

 

生产环境 Tomcat 调优实际操作

图 1 发布时候 load 飙高

 

生产环境 Tomcat 调优实际操作

图 2 发布时候响应时间飙高

 

二、问题排查方法

     发布时对资源使用情况进行监控。

1)通过 top -H -p 查找 cpu 使用率较高的线程,发现 2129 和 2130 这两个线程 cpu 使用较高。

生产环境 Tomcat 调优实际操作

图 3 查找 cpu 使用率较高的线程

 

2)通过 jstack 打印栈信息,并将线程号 2129 和 2130 转换成 16 进制(printf “%x\n” 2129),分别为 851 和 852,发现这两个线程是编译线程(表 1)。此外当这两个线程 cpu 使用率降低后 load 以及响应时间也马上恢复了正常,时间点非常吻合。

表 1 cpu 使用率较高的两个线程详细信息

“C2 CompilerThread1” daemon prio=10 tid=0x00007fce48125800 nid=0x852 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
– None
“C2 CompilerThread0” daemon prio=10 tid=0x00007fce48123000 nid=0x851 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
– None

三、现象解释

      C2 CompilerThread 线程项目启动初期 cpu 使用率那么高,它在干什么呢? 

      Java 程序在启动的时候所有代码的执行都处于解释执行模式,只有在运行了一段时间后,根据代码方法执行的次数,或代码里循环的执行次数等达到一定的阈值才会编译成机器码,编译成机器码后执行效率会得到大幅提升,而随着执行时间进一步拉长,JVM 的各种更高级的编译优化手段就会逐渐加上,例如 if 条件的执行状况,逃逸分析等。这里的C2 CompilerThread 线程干的就是编译优化的活。

     现在貌似可以解释之前的现象了。

     在程序刚启动的时候,java 还处于解释执行模式,因此服务效率很低,响应时间缓慢,处理得慢了,load 自然也就高了。而当流量持续不断导入时,我们代码的很多方法执行次数不断增多,此时C2 CompilerThread 线程不断收集优化信息,并且开始将一些热点代码优化编译成本地机器码,因此该线程的 cpu 使用率增高。而当 C2 CompilerThread 线程完成初始编译优化过程后,C2 CompilerThread 线程的 cpu 使用率开始下降,与此同时优化后服务的性能大幅提升,服务响应时间也大大缩短,load 也下降。

     现在的症结在于编译优化过程持续时间较长,引起抖动 如何降低编译优化的持续时间呢?

四、解决思路

1)预热

      如果在服务接受线上请求之前提前完成编译优化过程,那么将能避免此种抖动情况。一般的做法是预热,有两种方法:

      a)程序主动预热:在启动完成后,程序主动的访问热点的代码,确保主要的热点代码已被编译成机器码后再放入流量,可通过 -XX:+PrintCompilation 来确认。

      b)复制流量预热:通过 tcpcopy 软件拷贝一份线上 nginx 的流量进行预热,完成之后再导入线上流量。

2)启动多个线程进行编译优化

     如果能加快编译优化速度,那也能降低解释执行阶段导致的抖动时间。因此可以多拿几个线程来做编译,加快达到高峰性能的速度。

     可以使用 -XX:CICompilerCount 参数来设置编译线程数目,这个值默认是 2(之前在栈里看到有两个编译线程),我们可以加到 4。

3)采用 多层编译

      编译方式有三种:1)Client 模式;2)Server 模式;3)Tiered 模式。我们服务默认是 Server 模式。

      Server 模式是采用 c2 高级编译的,会比较耗时且要运行一段时间才会触发编译。Server 模式的优点是编译后程序效率较高;

      Client 模式比较轻量也比较快触发(比 Server 模式触发快),编译优化后程序效率不如 Server 模式;

      Tiered 模式是 Client 模式和 Server 模式的折中,一开始会启用 Client 模式,可以在启动后更快的让部分代码先进入编译优化阶段,之后会启动 Server 模式,达到程序效率最大优化的目的。

      Oracle JDK 7 里的 HotSpot VM 已经开始有比较好的 Tiered 编译(tiered compilation)支持,可以设置参数 -XX:+TieredCompilation 来启动 Tiered 模式,java 8 默认就是 Tiered 模式。

      图 4 是截取的不同编译方式的性能比较图,横坐标是时间,纵坐标是性能。可以看出 Tired 模式开始阶段性能与 C1 相当,当到达某一时刻后性能与 C2 相当。

生产环境 Tomcat 调优实际操作

图 4 不同编译模式的性能比较

     

五、结果分析

       简单起见采用方案 2 和方案 3 来进行优化。

       采用方案 2 和 3 之后进行了多次发布,发布时除个别机器 load 达到 10 之外,基本没有过高现象(在 2~4 范围内),并且短时间 (2 分钟) 内,load 都会降到较合理水平(2 左右),较发布时的 load 来看,比优化前要好很多。

      方案 2 和方案 3 只是降低了抖动持续的时间以及抖动强度,并不能完全避免抖动。真正能避免抖动的方案应该是方案 1,通过预热的方式实现平滑发布或重启。

tomcat 启动时 SessionIdGeneratorBase.createSecureRandom 耗时 5 分钟的问题 

通常情况下,tomcat 启动只要 2~3 秒钟,突然有一天,tomcat 启动非常慢,要花 5~6 分钟,查了很久,终于在这篇文章找到了解决方案,博主牛人啊。

Tomcat 8 启动很慢,且日志上无任何错误,在日志中查看到如下信息:
Log4j:[2015-10-29 15:47:11]  INFO ReadProperty:172 – Loading properties file from class path resource [resources/jdbc.properties]
Log4j:[2015-10-29 15:47:11]  INFO ReadProperty:172 – Loading properties file from class path resource [resources/common.properties]
29-Oct-2015 15:52:53.587 INFO [localhost-startStop-1] org.apache.catalina.util.SessionIdGeneratorBase.createSecureRandom Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [342,445] milliseconds.

原因

Tomcat 7/ 8 都使用 org.apache.catalina.util.SessionIdGeneratorBase.createSecureRandom 类产生安全随机类 SecureRandom 的实例作为会话 ID,这里花去了 342 秒,也即接近 6 分钟。

SHA1PRNG 算法是基于 SHA- 1 算法实现且保密性较强的伪随机数生成器。

在 SHA1PRNG 中,有一个种子产生器,它根据配置执行各种操作。

1)如果 Java.security.egd 属性或 securerandom.source 属性指定的是”file:/dev/random”或”file:/dev/urandom”,那么 JVM 会使用本地种子产生器 NativeSeedGenerator,它会调用 super()方法,即调用 SeedGenerator.URLSeedGenerator(/dev/random)方法进行初始化。

2)如果 java.security.egd 属性或 securerandom.source 属性指定的是其它已存在的 URL,那么会调用 SeedGenerator.URLSeedGenerator(url)方法进行初始化。

这就是为什么我们设置值为”file:///dev/urandom”或者值为”file:/./dev/random”都会起作用的原因。

在这个实现中,产生器会评估熵池(entropy pool)中的噪声数量。随机数是从熵池中进行创建的。当读操作时,/dev/random 设备会只返回熵池中噪声的随机字节。/dev/random 非 常适合那些需要非常高质量随机性的场景,比如一次性的支付或生成密钥的场景。

当熵池为空时,来自 /dev/random 的读操作将被阻塞,直到熵池收集到足够的环境噪声数据。这么做的目的是成为一个密码安全的伪随机数发生器,熵池要有尽可能大的输出。对于生成高质量的加密密钥或者是需要长期保护的场景,一定要这么做。

那么什么是环境噪声?

随机数产生器会手��来自设备驱动器和其它源的环境噪声数据,并放入熵池中。产生器会评估熵池中的噪声数据的数量。当熵池为空时,这个噪声数据的收集是比较花时间的。这就意味着,Tomcat 在生产环境中使用熵池时,会被阻塞较长的时间。

解决

有两种解决办法:

1)在 Tomcat 环境中解决

可以通过配置 JRE 使用非阻塞的 Entropy Source。

在 catalina.sh 中加入这么一行:-Djava.security.egd=file:/dev/./urandom 即可。

加入后再启动 Tomcat,整个启动耗时下降到 Server startup in 2912 ms。

2)在 JVM 环境中解决

打开 $JAVA_PATH/jre/lib/security/java.security 这个文件,找到下面的内容:

securerandom.source=file:/dev/urandom

替换成
securerandom.source=file:/dev/./urandom

tomcat 的缺省配置是不能长期稳定的运行的,也就是不适合生产环境,会出现死机的情况,让他不断的重启。对于操作系统的优化来说,是尽可能的提高内存容量,提高 cpu 的频率,保证文件系统的读写速率。

tomcat 的优化主要有三方面,分为系统优化,tomcat 自身优化,java 虚拟机(jvm)调优,此处主要讨论后两种。

一、tomcat 本身优化

1 工作方式选择

为了提升性能,首先就要对代码进行动静分离,让 Tomcat 只负责 jsp 文件的解析工作。如采用 Apache 和 Tomcat 的整合方式,他们之间的连接方案有三种选择,JK、http_proxy 和 ajp_proxy。相对于 JK 的连接方式,后两种在配置上比较简单的,灵活性方面也一点都不逊色。但就稳定性而言不像 JK 这样久经考验,所以建议采用 JK 的连接方式。

2 connector 连接器的工作方式

Tomcat 连接器的三种方式:bio、nio 和 apr,三种方式性能差别很大,apr 的性能最优,bio 的性能最差。而 Tomcat 7 使用的 Connector  默认就启用的 Apr 协议,但需要系统安装 Apr 库,否则就会使用 bio 方式。

3 配置文件优化

(1) 线程池

tomcat 为每个 connector 绑定一个线程池(默认最大线程数为 200)。

配置方式如下:

    <Executor name=”tomcatThreadPool” namePrefix=”catalina-exec-“

        maxThreads=”500″ minSpareThreads=”20″ maxSpareThreads=”50″ maxIdleTime=”60000″/>

    <Connector executor=”tomcatThreadPool”
              port=”8080″ protocol=”HTTP/1.1″

              URIEncoding=”UTF-8″

              connectionTimeout=”30000″

              enableLookups=”false”

              disableUploadTimeout=”false”

              connectionUploadTimeout=”150000″

              acceptCount=”300″

              keepAliveTimeout=”120000″

              maxKeepAliveRequests=”1″

              compression=”on”

              compressionMinSize=”2048″

              compressableMimeType=”text/html,text/xml,text/javascript,text/css,text/plain,image/gif,image/jpg,image/png”

              redirectPort=”8443″ />

maxThreads:tomcat 使用线程来处理请求,这个值表示 tomcat 可以创建的最大线程数,默认值为 200。

minSpareThreads:最小空闲线程数,tomcat 启动时的初始化线程数,表示即便没有请求,也要开启这么多的线程等待,默认值是 10。

maxSpareThreads:最大空闲线程数,一旦空闲的线程数超过这个值,tomcat 就会关闭不在需要的 socket 线程。

maxThreads 的值越大就会越消耗内存和 CPU, 因为 CPU 疲于处理线程上下文切换,就没有精力处理请求了。具体的值要取决于系统参数及实际应用场景。线程池可以配置在 tomcatTheadPool 中,也可以直接配置在 connector 中,但不可以重复配置。

(2)URIEncoding:指定 Tomcat 容器的 URL 编码格式,语言编码格式这块倒不如其它 WEB 服务器软件配置方便,需要分别指定。

(3)connnectionTimeout:网络连接超时,单位:毫秒,设置为 0 表示永不超时,这样设置有隐患的。通常可设置为 30000 毫秒,可根据检测实际情况,适当修改。

(4)enableLookups:是否反查域名,以返回远程主机的主机名,取值为:true 或 false,如果设置为 false,则直接返回 IP 地址,为了提高处理能力,应设置为 false。

(5)disableUploadTimeout:上传时是否使用超时机制。

(6)connectionUploadTimeout:上传超时时间,毕竟文件上传可能需要消耗更多的时间,这个根据你自己的业务需要自己调,以使 Servlet 有较长的时间来完成它的执行,需要与上一个参数一起配合使用才会生效。

(7)acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可传入连接请求的最大队列长度,超过这个数的请求将不予处理,默认为 100 个。

(8)keepAliveTimeout:长连接最大保持时间(毫秒),表示在下次请求过来之前,Tomcat 保持该连接多久,默认是使用 connectionTimeout 时间,-1 为不限制超时。

(9)maxKeepAliveRequests:表示在服务器关闭之前,该连接最大支持的请求数。超过该请求数的连接也将被关闭,1 表示禁用,- 1 表示不限制个数,默认 100 个,一般设置在 100~200 之间。

(10)compression:是否对响应的数据进行 GZIP 压缩,off:表示禁止压缩;on:表示允许压缩(文本将被压缩)、force:表示所有情况下都进行压缩,默认值为 off,压缩数据后可以有效的减少页面的大小,一般可以减小 1 / 3 左右,节省带宽。

(11)compressionMinSize:表示压缩响应的最小值,只有当响应报文大小大于这个值的时候才会对报文进行压缩,如果开启了压缩功能,默认值就是 2048。

(12)compressableMimeType:压缩类型,指定对哪些类型的文件进行数据压缩。

(13)noCompressionUserAgents=”gozilla, traviata”:对于以下的浏览器,不启用压缩。

如果已经对代码进行了动静分离,静态页面和图片等数据就不需要 Tomcat 处理了,那么也就不需要配置在 Tomcat 中配置压缩了。

以上是一些常用的配置参数属性,当然还有好多其它的参数设置,还可以继续深入的优化,HTTP Connector 与 AJP Connector 的参数属性值,可以参考官方文档的详细说明:

https://tomcat.apache.org/tomcat-7.0-doc/config/http.html

https://tomcat.apache.org/tomcat-7.0-doc/config/ajp.html

二、JVM 优化

JVM 常见配置

堆设置

-Xms: 初始堆大小

-Xmx: 最大堆大小

-XX:NewSize=n: 设置年轻代大小

-XX:NewRatio=n: 设置年轻代和年老代的比值。如: 为 3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代年老代和的 1 /4

-XX:SurvivorRatio=n: 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。如:3,表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的 1 /5

-XX:MaxPermSize=n: 设置持久代大小

收集器设置

-XX:+UseSerialGC: 设置串行收集器

-XX:+UseParallelGC: 设置并行收集器

-XX:+UseParalledlOldGC: 设置并行年老代收集器

-XX:+UseConcMarkSweepGC: 设置并发收集器

垃圾回收统计信息

-XX:+PrintGC

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-Xloggc:filename

并行收集器设置

-XX:ParallelGCThreads=n: 设置并行收集器收集时使用的 CPU 数。并行收集线程数。

-XX:MaxGCPauseMillis=n: 设置并行收集最大暂停时间

-XX:GCTimeRatio=n: 设置垃圾回收时间占程序运行时间的百分比。公式为 1 /(1+n)

并发收集器设置

-XX:+CMSIncrementalMode: 设置为增量模式。适用于单 CPU 情况。

-XX:ParallelGCThreads=n: 设置并发收集器年轻代收集方式为并行收集时,使用的 CPU 数。并行收集线程数。

常见的内存溢出有以下两种:

Java.lang.OutOfMemoryError: PermGen space

java.lang.OutOfMemoryError: Java heap space

这里以 tomcat 环境为例

一、java.lang.OutOfMemoryError: PermGen space

PermGen space 的全称是 Permanent Generation space, 是指内存的永久保存区域,

这块内存主要是被 JVM 存放 Class 和 Meta 信息的,Class 在被 Loader 时就会被放到 PermGen space 中, 它和存放类实例 (Instance) 的 Heap 区域不同,GC(Garbage Collection)不会在主程序运行期对 PermGen space 进行清理,所以如果你的应用中有很多 CLASS 的话, 就很可能出现 PermGen space 错误, 这种错误常见在 web 服务器对 JSP 进行 pre compile 的时候。如果你的 WEB APP 下都用了大量的第三方 jar, 其大小超过了 jvm 默认的大小 (4M) 那么就会产生此错误信息了。
解决方法:手动设置 MaxPermSize 大小
建议:将相同的第三方 jar 文件移置到 tomcat/shared/lib 目录下,这样可以达到减少 jar 文档重复占用内存的目的。

二、java.lang.OutOfMemoryError: Java heap space
JVM 堆的设置是指 java 程序运行过程中 JVM 可以调配使用的内存空间的设置.JVM 在启动的时候会自动设置 Heap size 的值,其初始空间 (即 -Xms) 是物理内存的 1 /64,最大空间 (-Xmx) 是物理内存的 1 /4。可以利用 JVM 提供的 -Xmn -Xms -Xmx 等选项可进行设置。Heap size 的大小是 Young Generation 和 Tenured Generaion 之和。
提示:在 JVM 中如果 98%的时间是用于 GC 且可用的 Heap size 不足 2%的时候将抛出此异常信息。
提示:Heap Size 最大不要超过可用物理内存的 80%,一般的要将 -Xms 和 -Xmx 选项设置为相同,而 -Xmn 为 1 / 4 的 -Xmx 值。
解决方法:手动设置 Heap size

Linux 下修改 JVM 内存大小:

要添加在 tomcat 的 bin 下 catalina.sh 里,位置 cygwin=false 前。

# OS specific support. $var _must_ be set to either true or false.
JAVA_OPTS=”-Xms256m -Xmx512m -Xss1024K -XX:PermSize=128m -XX:MaxPermSize=256m”
cygwin=false

Windows 下修改 JVM 内存大小:

情况一: 解压版本的 Tomcat, 要通过 startup.bat 启动 tomcat 才能加载配置

要添加在 tomcat 的 bin 下 catalina.bat 里

rem Guess CATALINA_HOME if not defined
set CURRENT_DIR=%cd% 后面添加, 红色的为新添加的.

set JAVA_OPTS=-Xms256m -Xmx512m -XX:PermSize=128M -XX:MaxNewSize=256m -XX:MaxPermSize=256m -Djava.awt.headless=true

情况二: 安装版的 Tomcat 下没有 catalina.bat

windows 服务执行的是 bin/tomcat.exe. 他读取注册表中的值, 而不是 catalina.bat 的设置.

修改注册表 HKEY_LOCAL_MACHINE/SOFTWARE/Apache Software Foundation/Tomcat Service Manager/Tomcat5/Parameters/JavaOptions
原值为
-Dcatalina.home=”C:/ApacheGroup/Tomcat 5.0″
-Djava.endorsed.dirs=”C:/ApacheGroup/Tomcat 5.0/common/endorsed”
-Xrs

加入 -Xms300m -Xmx350m
重起 tomcat 服务, 设置生效

如何设置 Tomcat 的 JVM 虚拟机内存大小

可以给 Java 虚拟机设置使用的内存,但是如果你的选择不对的话,虚拟机不会补偿。可通过命令行的方式改变虚拟机使用内存的大小。如下表所示有两个参数用来设置虚拟机使用内存的大小。

-Xms
JVM 初始化堆的大小

-Xmx
JVM 堆的最大值

这两个值的大小一般根据需要进行设置。初始化堆的大小执行了虚拟机在启动时向系统申请的内存的大小。一般而言,这个参数不重要。但是有的应用程序在大负载的 情况下会急剧地占用更多的内存,此时这个参数就是显得非常重要,如果虚拟机启动时设置使用的内存比较小而在这种情况下有许多对象进行初始化,虚拟机就必须 重复地增加内存来满足使用。由于这种原因,我们一般把 -Xms 和 -Xmx 设为一样大,而堆的最大值受限于系统使用的物理内存。一般使用数据量较大的应用程 序会使用持久对象,内存使用有可能迅速地增长。当应用程序需要的内存超出堆的最大值时虚拟机就会提示内存溢出,并且导致应用服务崩溃。因此一般建议堆的最大值设置为可用内存的最大值的 80%。

Tomcat 默认可以使用的内存为 128MB,在较大型的应用项目中,这点内存是不够的,需要调大。
Windows 下,在文件 /bin/catalina.bat,Unix 下,在文件 /bin/catalina.sh 的前面,增加如下设置:
JAVA_OPTS=’-Xms【初始化内存大小】-Xmx【可以使用的最大内存】’
需要把这个两个参数值调大。例如:
JAVA_OPTS=’-Xms256m -Xmx512m’
表示初始化内存为 256MB,可以使用的最大内存为 512MB。
另 外需要考虑的是 Java 提供的垃圾回收机制。虚拟机的堆大小决定了虚拟机花费在收集垃圾上的时间和频度。收集垃圾可以接受的速度与应用有关,应该通过分析 实际的垃圾收集的时间和频率来调整。如果堆的大小很大,那么完全垃圾收集就会很慢,但是频度会降低。如果你把堆的大小和内存的需要一致,完全收集就很快,但是会更加频繁。调整堆大小的的目的是最小化垃圾收集的时间,以在特定的时间内最大化处理客户的请求。在基准测试的时候,为保证最好的性能,要把堆的大小 设大,保证垃圾收集不在整个基准测试的过程中出现。

如果系统花费很多的时间收集垃圾,请减小堆大小。一次完全的垃圾收集应该不超过 3-5 秒。如果垃圾收集成为瓶颈,那么需要指定代的大小,检查垃圾收集的详细输出,研究 垃圾收集参数对性能的影响。一般说来,你应该使用物理内存的 80% 作为堆大小。当增加处理器时,记得增加内存,因为分配可以并行进行,而垃圾收集不是并行的。

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-21发表,共计10854字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中