Tomcat 性能分析 60s 与常见异常特征分析思路

业务复杂度催生了了企业软件的复杂度,可观测性越来越多的得到重视,系统异常一定不是毫无征兆的,及时监测并干预可以保证系统稳定性。

这里,我们就来分析下 Tomcat 的关键性能监控指标,以及指标异常的典型特征,从而总结出常见异常分析思路,这些指标就可以得到量化,成为性能分析检查表或仪表盘。

一、Tomcat 性能分析 60s

这里选用 Linux 操作系统或 JDK 集成的 CLI 工具,一方面是保证监测手段可执行,在项目没有完备的监控系统或碍于严格的线上运维工具要求也可以完成监测任务,另一方面避免监控系统的可视化应用占用过多系统资源,在系统性能达到瓶颈时无响应。

通过这份清单可以快速完成对 Tomcat 运行状况的监测,也就是性能分析里常用的 60s 法则,快速找到性能瓶颈。监测内容涵盖 JVM、CPU、IO、内存、网络等。

Tomcat 监控指标包括吞吐量、响应时间、错误数、线程池、CPU 以及 JVM。

| 工具名称 | 工具能力 | 常用指令 |
|---------|----------------------------|-------------------------------------------|------|
| jps | 显示所有 JVM 进程,包括主类名和进程号 | jps |
| jstat | 收集 JVM 运行数据,如垃圾回收信息 | jstat -gc/gcutil/gccause |
| jmap | 生成内存转储快照文件(heapdump) | jmap -dump:format=b,file=./dumpfile.hprof |
| jstack | 生成 JVM 线程快照(threaddump) | jstack |
| cat | 查看 tomcat 进程状态 | cat /proc//status |
| top | 查看 tomcat 进程 CPU 和内存资源占用情况 | top -H -p |
| netstat | 查看 tomcat 网络连接 | netstat -na | grep |
| ifstat | 查看 tomcat 网络负载 | ifstat |
| vmstat | 查看 IO 资源占用 | vmstat |

二、JVM 调优举例

和 Web 应用程序一样,Tomcat 作为一个 Java 程序也跑在 JVM 中,对于 JVM 调优主要是对 JVM 垃圾收集的优化。观察到 Tomcat 进程的 CPU 使用率高,并且在 GC 日志中发现 GC 次数频繁、GC 停顿时间长,就表明需要对 GC 进行优化了。

下面以目前仍占主流的 Java 8 和 G1 作为举例对象。

1、开启 GC 日志打印

系统运维强调事故现场有迹可循,打印 GC 日志对于 JVM 调优来说就是必不可少的。常用的日志参数有这些,建议应用上线启用:

ruby 复制代码
# 开启 GC 日志细节打印
-XX:+PrintGCDetails
# 打印 GC 时间戳
-XX:+PrintGCDateStamps
# GC 日志输出的文件路径,通过时间戳区分
-Xloggc:/opt/app/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation 
# 最多分割 20 个文件,超过之后循环从头文件开始写入
-XX:NumberOfGCLogFiles=20
# 单个文件大小上限 10M,超过就触发分割
-XX:GCLogFileSize=10M

2、观察 CPU 使用率

top 命令可以观察到 CPU 使用率已经超过了 100%:

sql 复制代码
top - 04:06:14 up 1 day,  2:11,  2 users,  load average: 2.43, 1.73, 1.45
Tasks: 135 total,   2 running, 133 sleeping,   0 stopped,   0 zombie
%Cpu(s): 34.5 us,  1.0 sy,  0.0 ni, 64.4 id,  0.0 wa,  0.0 hi,  0.1 si,  0.0 st
KiB Mem :  3879588 total,   221376 free,  3313652 used,   344560 buff/cache
KiB Swap:  8257532 total,  8257532 free,        0 used.   286964 avail Mem 
​
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                         
 6022 root      20   0 3635792 184024  13416 S 140.0  4.7   1:38.76 java      

3、关注 FGC 与老年代占用率

连续运行 Full GC 就可能导致间歇性内存不足错误(OutOfMemoryErrors)、响应时间下降或 CPU 负载过高,甚至使应用程序完全无法响应。

使用 jstat 工具观察垃圾收集统计数据:

yaml 复制代码
jstat -gcutil 6022

得到了如下结果:

yaml 复制代码
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00   0.00   0.00  97.78  92.71  88.85   1183    7.185  1131   53.537   60.722

触发了大量 FGC 并且 YGC 与 FGC 数量大致相当,老年代占用率逼近 100%,总 GC 时间超过了 60s。可以给出两种优化思路:

  • 堆内存大小调整,减少 YGC 次数
  • 观察内存回收效率,考虑内存泄漏

最后,结合业务特点再进行选择优化方向。

三、CPU 调优举例

CPU 作为重要的系统资源往往容易成为性能瓶颈,常见的分析思路就是抓出 CPU 占用率异常高的线程优化掉,或者整体占用率高但是没有某个线程占用率特别高,就要考虑大量线程频繁切换消耗的原因了

1、观察 CPU 使用率

top 查看 CPU 占用率排行,这里很容易发现第一个 java 进程几乎耗尽了 CPU 资源:

sql 复制代码
top - 17:16:53 up 1 day, 15:21,  2 users,  load average: 9.84, 9.23, 9.32
Tasks: 134 total,   1 running, 133 sleeping,   0 stopped,   0 zombie
%Cpu(s): 46.4 us, 32.1 sy,  0.0 ni, 21.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  3879588 total,   470052 free,  3180356 used,   229180 buff/cache
KiB Swap:  8257532 total,  8136444 free,   121088 used.   476584 avail Mem 
​
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                                                         
16208 root      20   0 4820976 222448  13392 S 369.4  5.7 281:09.76 java                                                                                                            
    1 root      20   0  128812   3904   2304 S   0.0  0.1   0:06.56 systemd                                                                                                         
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.08 kthreadd 

进一步查看这个进程中各线程的 CPU 占用率:

arduino 复制代码
top - 15:04:14 up 1 day, 13:09,  2 users,  load average: 7.80, 3.39, 1.47
Threads: 577 total, 129 running, 448 sleeping,   0 stopped,   0 zombie
%Cpu(s): 45.6 us, 32.8 sy,  0.0 ni, 21.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  3879588 total,   504768 free,  3246524 used,   128296 buff/cache
KiB Swap:  8257532 total,  8131324 free,   126208 used.   451976 avail Mem 
​
  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                                                                                                          
14472 root      20   0 4811332 207504  13356 R 98.7  5.3   2:09.29 scheduling-1                                                                                                     
14548 root      20   0 4811332 207504  13356 S 12.6  5.3   0:00.97 pool-1-thread-7                                                                                                  
15017 root      20   0 4811332 207504  13356 R 11.0  5.3   0:00.62 pool-1-thread-5                                                                                                  
14757 root      20   0 4811332 207504  13356 R  9.6  5.3   0:01.25 pool-1-thread-2                                                                                                  
15024 root      20   0 4811332 207504  13356 R  9.3  5.3   0:00.46 pool-1-thread-5                                                                                                  
14499 root      20   0 4811332 207504  13356 R  7.3  5.3   0:01.38 pool-1-thread-2                                                                                                  
14572 root      20   0 4811332 207504  13356 R  7.0  5.3   0:00.65 pool-1-thread-1                                                                                                  
14846 root      20   0 4811332 207504  13356 S  7.0  5.3   0:01.38 pool-1-thread-3                                                                                                  
14536 root      20   0 4811332 207504  13356 S  5.6  5.3   0:00.94 pool-1-thread-6                                                                                                  
14620 root      20   0 4811332 207504  13356 S  5.6  5.3   0:00.68 pool-1-thread-1                                                                                                  
14784 root      20   0 4811332 207504  13356 R  5.6  5.3   0:01.07 pool-1-thread-3 

除了第一个线程占用了大量 CPU 外,其余线程似乎占用率都不高,那 369.4% 都耗费在哪里呢,很有可能因为大量线程切换消耗掉了。

2、分析 thread dump

jstack 打印线程转储日志,也就是 thread dump,逐个分析线程状态。

首先查看占用 CPU 最高的线程 scheduling-1:

php 复制代码
"scheduling-1" #29 prio=5 os_prio=0 tid=0x00007f903cefc800 nid=0x3f78 runnable [0x00007f8fe1832000]
   java.lang.Thread.State: RUNNABLE
    at sun.misc.Unsafe.unpark(Native Method)
    at java.util.concurrent.locks.LockSupport.unpark(LockSupport.java:141)
    at java.util.concurrent.SynchronousQueue$TransferStack$SNode.tryMatch(SynchronousQueue.java:265)
    at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:383)
    at java.util.concurrent.SynchronousQueue.offer(SynchronousQueue.java:913)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
    at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
    at com.example.demo.controller.HighCpuController.lambda$lockContention$0(HighCpuController.java:35)
    at com.example.demo.controller.HighCpuController$$Lambda$577/1498344437.accept(Unknown Source)
    at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110)
    at java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:581)
    at com.example.demo.controller.HighCpuController.lockContention(HighCpuController.java:35)
    at sun.reflect.GeneratedMethodAccessor4.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84)
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:750)

可以查到 HighCpuController 类中提交了周期性线程任务,AbstractExecutorService#submit 不断向线程池中提交任务从而消耗了大量 CPU,因此优化方向就具体化为对 HighCpuController 第 35 行附近代码的斟酌。

3、线程切换验证

佐证上面的猜测,可以进一步查看其他相同命名前缀(pool-1-thread)的线程状态,比如 pool-1-thread-3:

php 复制代码
"pool-1-thread-3" #32 prio=5 os_prio=0 tid=0x00007f8fb4006000 nid=0x3f7b runnable [0x00007f8fe176f000]
   java.lang.Thread.State: TIMED_WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000000c0c00000> (a java.util.concurrent.SynchronousQueue$TransferStack)
    at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
    at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:460)
    at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362)
    at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:941)
    at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1073)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:750)

线程有时间限制的等待在 ThreadPoolExecutor#getTask 方法上,尝试从线程池队列中拿到任务但是队列为空,于是通过 LockSupport#parkNanos 调用进入到了 TIMED_WAITING (parking) 状态。

统计下这种状态的线程数量,共有 536 个:

bash 复制代码
grep -o 'pool-1-thread' threaddump.log | wc -l

进一步的,还可以通过 vmstat 验证:

css 复制代码
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
217  0 126208 462160     44 127672    2    3     4     4   35   92  3  0 97  0  0
23  0 126208 461996     44 127672    0    0     0     0 51544 118594  7 30 63  0  0
 9  0 126208 461872     44 127672    0    0     0     0 50658 120680  9 30 61  0  0
 3  0 126208 461872     44 127672    0    0     0     0 51316 116810  7 30 63  0  0
 4  0 126208 461872     44 127672    0    0     0     0 48867 115963  9 29 62  0  0
17  0 126208 461856     44 127676    0    0     0     0 50131 123902  8 30 62  0  0

发现 in 和 cs 的值异常高,表明 CPU 资源大量耗费在了线程切换上,再一次确认上面定位到的代码行就是重点分析对象了。比如使用 Integer 最大值作为线程数上限的 Executors#newCachedThreadPool 来创建线程池。

四、内存调优举例

内存作为重要的系统资源也往往容易成为性能瓶颈,如果发现系统频繁的触发 FGC,并且老年代占用率一直居高不下,那就可以大胆猜测发生了内存泄漏,需要作内存优化了。

1、开启 heap dump 打印

系统运维强调事故现场有迹可循,打印 OOM 时的转储日志对于分析系统崩溃原因来说就是必不可少的。常用的 heap dump 参数有这些,建议应用上线启用:

ruby 复制代码
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./heapdump.hprof

触发一些 OOM 就会打印如下日志,并生成 hprof 文件:

makefile 复制代码
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid25615.hprof ...

2、关注 FGC 与老年代占用率

与 JVM 调优思路类似,关注 FGC 与老年代占用率。必要时主动触发 heap dump 打印:

ini 复制代码
jmap -dump:format=b,file=./dumpfile.hprof 25615

3、分析内存溢出点

结合 OOM 日志以及 heap dump 中分析内存溢出点,这要深入代码实现,并借助 MAT(Eclipse MemoryAnalyzer)等工具辅助完成。

比如解决 java.lang.OutOfMemoryError: Requested array size exceeds VM limit 思路是考虑存在申请超大数组的地方;解决 java.lang.OutOfMemoryError: Java heap space 思路是内存泄漏导致 GC 一直无法回收内存,或者 JVM 参数配置不合适需要调整;解决 java.lang.OutOfMemoryError: GC overhead limit exceeded 思路是内存泄漏导致 GC 回收效率低。

五、IO 与线程池调优举例

1、选择合适的 IO 模型

Tomcat 支持的 IO 模型有 NIO、NIO.2、APR,默认为 NIO。

Tomcat 如果运行在 Windows 上并且访问量比较大,建议选择 NIO.2,因为 Windows 实现了异步 IO;如果运行在 Linux 上,则还是选择 NIO,因为 Linux 并没有真正实现异步 IO,只是通过 epoll 模拟了异步 IO。

如果 Web 应用需要 TLS 加密传输并且对性能要求很高,可以考虑使用 APR,它通过 OpenSSL 实现了高性能加解密。

2、线程池参数配置

线程池调优为了使 Tomcat 能够合理响应请求又不会给系统造成过大负担,经常调整的参数有这些。

maxThreads 是线程池最大线程数,默认值 200。maxThreads 设置过大将会导致线程资源空闲,以及浪费切换线程开销,设置过小就会导致 Tomcat 吞吐量过低。

一般设置原则是先根据系统业务特点设置一个极小值,高并发低耗时设置少量线程数,低并发高耗时设置较多线程数,高并发高耗时设置多线程数。再进行压测,当错误数显著增加或者响应时间大幅增加意味着达到系统极限,再逐阶加大线程数,直至 TPS 出现下降拐点后,就可以认为这是最佳线程数了。

minSpareThreads 是线程池最小线程数,默认值 25,线程空闲后将会被回收直至最小线程数。根据系统波谷分析最小线程使用量来调整它,设置过大也会导致线程资源浪费,设置过小就会造成频繁创建与销毁线程的开销。

3、分析 tcp 连接数

CPU 与内存占用没有明显增长,但是系统响应缓慢,优化思路转向线程状态与连接数。

统计 ESTABLISHED 连接数,共有 2000 个:

perl 复制代码
netstat -na | grep 9999 | grep ESTAB | wc -l

线程打满考虑网络拥塞或者下游服务响应缓慢等异常。

六、网络调优举例

1、查找网络异常

从 Tomcat 日志中容易发现 Socket 异常,Connection timed out: connect,Connection reset,Too many open files 等,这些都表明需要从网络层面优化 Tomcat。

比如分析 Connection timed out: connect 异常是发生在所有网络请求中,还是某些服务上。结合 CPU 与内存占用分析上并没有明显增长,那就要考虑下游服务异常或者网络连接参数需要调整了。

2、网络连接参数配置

maxConnections 是指 Tomcat 在给定时间内接受和处理的最大连接数,NIO 默认值是 10000。达到此数量后,Tomcat 将继续根据 acceptCount 值接受但不处理新的连接。

acceptCount 是可在请求队列中排队的连接数,也就是超过 maxThreads 值后在队列中等待处理,默认值 100。当所有可能的请求处理线程都在使用时,接收到的连接请求的最大队列长度。队列已满时收到的任何请求都将被拒绝。同样需要根据压测来调整,在系统吞吐量以及系统负载之间平衡。

对于上面的例子就可以通过调大 maxConnections 来解决。

七、Tomcat 常见性能异常分析思路

1、异常数据来源

在建设完备的可观测系统之前,原始日志仍然是分析问题的首选。

Tomcat 作为运行在 JVM 上的进程,JVM 的异常分析同样适用于 Tomcat,比如 JVM 崩溃日志 hs_err,部分 OOM 发生时的 heap dump,以及线程转储日志 thread dump,他们是 JVM 调优的重要分析来源。

其次,运行在 Tomcat 上的组件日志也可以提供分析思路,比如网络 IO 异常、内存溢出异常等都可以从组件日志中捕获。

2、分析手段

上文提到的基本都是借助 JDK 或 Linux 自带的命令行工具完成的,一方面保证工具的可靠性,部署环境如何变化都能及时准确的获取系统运行现状,为性能分析拿到第一手资料;另一方面是稳定性,在可视化分析工具在系统资源达到瓶颈不可用时也能持续工作。这些基本工具使用,可以纳入研发和运维同学的武器库。

当然,分析手段远不止于此,随着 DevOps 深入各行各业的 arthas,云原生运维场景下的 datadog,以及 AI 加持的 gceasy 等产品都有着用武之地。

3、最后的最后

Tomcat 性能分析的本质还是对计算机操作系统的全方位掌握,能够从 CPU、内存、磁盘、网络等系统资源上找出性能瓶颈,并给出解决思路,也就是用性能工程的角度看待性能瓶颈。

相关推荐
人工智能培训咨询叶梓2 小时前
探索开放资源上指令微调语言模型的现状
人工智能·语言模型·自然语言处理·性能优化·调优·大模型微调·指令微调
CodeToGym4 小时前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
无尽的大道4 小时前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
superman超哥4 小时前
04 深入 Oracle 并发世界:MVCC、锁、闩锁、事务隔离与并发性能优化的探索
数据库·oracle·性能优化·dba
前端青山14 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
尢词16 小时前
SpringMVC
java·spring·java-ee·tomcat·maven
清风百草16 小时前
【04】【Maven项目热部署】将Maven项目热部署到远程tomcat服务器上
tomcat·maven项目热部署
WZF-Sang18 小时前
Linux—进程学习-01
linux·服务器·数据库·学习·操作系统·vim·进程
青云交19 小时前
大数据新视界 -- 大数据大厂之 Impala 性能优化:应对海量复杂数据的挑战(上)(7/30)
大数据·性能优化·impala·数据分区·查询优化·海量复杂数据·经典案例
chusheng18401 天前
Python 爬取大量数据如何并发抓取与性能优化
开发语言·python·性能优化