系统性能问题定位分析思路

系统性能问题定位分析思路可以从代码相关、cpu、内存、磁盘I/O网络I/O、GC等分析

CPU相关

CPU 利用率高&&平均负载高

排查高 CPU 占用的一般思路通过 jstack 多次(> 5次)打印线程栈,一般可以定位到消耗 CPU 较多的线程堆栈。或者通过 Profiling 的方式(基于事件采样或者埋点),得到应用在一段时间内的 on-CPU 火焰图,也能较快定位问题。

还有一种可能的情况,此时应用存在频繁的 GC (包括 Young GC、Old GC、Full GC),这也会导致 CPU 利用率和负载都升高。排查思路:使用 jstat -gcutil 持续输出当前应用的 GC 统计次数和时间。频繁 GC 导致的负载升高,一般还伴随着可用内存不足,可用 free 或者 top 等命令查看下当前机器的可用内存大小。

CPU 利用率低&&平均负载高

排查&&验证思路使用 vmstat 1 定时输出系统资源使用,观察 %wa(iowait) 列的值,该列标识了磁盘 I/O 等待时间在 CPU 时间片中的百分比,如果这个值超过30%,说明磁盘 I/O 等待严重,这可能是大量的磁盘随机访问或直接的磁盘访问(没有使用系统缓存)造成的,也可能磁盘本身存在瓶颈,可以结合 iostat 或 dstat 的输出加以验证,如 %wa(iowait) 升高同时观察到磁盘的读请求很大,说明可能是磁盘读导致的问题。

 

CPU 上下文切换次数变高

先用 vmstat 查看系统的上下文切换次数,然后通过 pidstat 观察进程的自愿上下文切换(cswch)和非自愿上下文切换(nvcswch)情况

 

内存相关

系统内存不足

一般系统内存不足,大概率是由 Java 应用引起的。使用 top 命令时,我们可以看到 Java 应用进程的实际内存占用,其中 RES 表示进程的常驻内存使用,VIRT 表示进程的虚拟内存占用,内存大小的关系为:VIRT > RES > Java 应用实际使用的堆大小除了堆内存,Java 进程整体的内存占用,还有方法区/元空间、JIT 缓存等

Java 进程的内存占用,可以使用 jstat -gc 命令查看,输出的指标中可以得到当前堆内存各分区、元空间的使用情况。

堆外内存的统计和使用情况,可以利用 NMT(Native Memory Tracking,HotSpot VM Java8 引入)获取。

 

Java 内存溢出

1)java.lang.OutOfMemoryError: Java heap space。原因:堆中(新生代和老年代)无法继续分配对象了、某些对象的引用长期被持有没有被释放,垃圾回收器无法回收、使用了大量的 Finalizer 对象,这些对象并不在 GC 的回收周期内等。一般堆溢出都是由于内存泄漏引起的,如果确认没有内存泄漏,可以适当通过增大堆内存。

2)java.lang.OutOfMemoryError:GC overhead limit exceeded。原因:垃圾回收器超过98%的时间用来垃圾回收,但回收不到2%的堆内存,一般是因为存在内存泄漏或堆空间过小。

3)java.lang.OutOfMemoryError: Metaspace或java.lang.OutOfMemoryError: PermGen space。排查思路:检查是否有动态的类加载但没有及时卸载,是否有大量的字符串常量池化,永久代/元空间是否设置过小等。

4)java.lang.OutOfMemoryError : unable to create new native Thread。原因:虚拟机在拓展栈空间时,无法申请到足够的内存空间。可适当降低每个线程栈的大小以及应用整体的线程个数。此外,系统里总体的进程/线程创建总数也受到系统空闲内存和操作系统的限制,请仔细检查。注:这种栈溢出,和 StackOverflowError 不同,后者是由于方法调用层次太深,分配的栈内存不够新建栈帧导致。

另外还有 Swap 分区溢出、本地方法栈溢出、数组分配溢出等 OutOfMemoryError 类型

Java 内存泄漏排查思路

1.通过 jmap 定期输出堆内对象统计,定位数量和大小持续增长的对象;

2.使用 Profiler 工具对应用进行 Profiling,寻找内存分配热点。

堆内存持续增长时,建议 dump 一份堆内存的快照,后面可以基于快照做一些分析

 

GC相关

1)选择合适的 GC 回收器

根据应用对延迟、吞吐的要求,结合各垃圾回收器的特点,合理选用。推荐使用 G1 替换 CMS 垃圾回收器,G1 的性能是在逐步优化的,在 8GB 内存及以下的机器上,其各方面的表现也在赶上甚至有超越之势。G1 调参较方便,而 CMS 垃圾回收器参数太过复杂、容易造成空间碎片化、对 CPU 消耗较高等弊端,也使其目前处于废弃状态。Java 11 里新引入的 ZGC 垃圾回收器,基本可用做到全阶段并发标记和回收

2)合理的堆内存大小设置

堆大小不要设置过大,建议不要超过系统内存的 75%,避免出现系统内存耗尽。最大堆大小和初始化堆的大小保持一致,避免堆震荡。新生代的大小设置比较关键,我们调整 GC 的频率和耗时,很多时候就是在调整新生代的大小,包括新生代和老年代的占比、新生代中 Eden 区和 Survivor 区的比例等,这些比例的设置还需要考虑各代中对象的晋升年龄,整个过程需要考虑的东西还是比较多的。如果使用 G1 垃圾回收器,新生代大小这一块需要考虑的东西就少很多了,自适应的策略会决定每一次的回收集合(CSet)。新生代的调整是 GC 调优的核心,非常依赖经验,但是一般来说,Young GC 频率高,意味着新生代太小(或 Eden 区和 Survivor 配置不合理),Young GC 时间长,意味着新生代过大,这两个方向大体不差。

3)降低 Full GC 的频率

如果出现了频繁的 Full GC 或者 老年代 GC,很有可能是存在内存泄漏,导致对象被长期持有,通过 dump 内存快照进行分析,一般能较快地定位问题。除此之外,新生代和老年代的比例不合适,导致对象频频被直接分配到老年代,也有可能会造成 Full GC,这个时候需要结合业务代码和内存快照综合分析。

此外,通过配置 GC 参数,可以帮助我们获取很多 GC 调优所需的关键信息,如配置-XX:+PrintGCApplicationStoppedTime-XX:+PrintSafepointStatistics-XX:+PrintTenuringDistribution,分别可以获取 GC Pause 分布、安全点耗时统计、对象晋升年龄分布的信息,加上 -XX:+PrintFlagsFinal 可以让我们了解最终生效的 GC 参数等。

另外定位性能问题的命令:

1)查看系统当前网络连接数

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

2)查看堆内对象的分布 Top 50(定位内存泄漏)

jmap –histo:live $pid | sort-n -r -k2 | head-n 50

3)按照 CPU/内存的使用情况列出前10 的进程

#内存ps axo %mem,pid,euser,cmd | sort -nr | head -10#CPUps -aeo pcpu,user,pid,cmd | sort -nr | head -10

4)显示系统整体的 CPU利用率和闲置率

grep "cpu " /proc/stat | awk -F ' ' '{total = $2 + $3 + $4 + $5} END {print "idle \t used\n" $5*100/total "% " $2*100/total "%"}'

5)按线程状态统计线程数

jstack $pid | grep java.lang.Thread.State:|sort|uniq -c | awk '{sum+=$1; split($0,a,":");gsub(/^[ \t]+|[ \t]+$/, "", a[2]);printf "%s: %s\n", a[2], $1}; END {printf "TOTAL: %s",sum}';

6)查看最消耗 CPU 的 Top10 线程机器堆栈信息

推荐大家使用 show-busy-java-threads 脚本,该脚本可用于快速排查 Java 的 CPU 性能问题(top us值过高),自动查出运行的 Java 进程中消耗 CPU 多的线程,并打印出其线程栈,从而确定导致性能问题的方法调用,该脚本已经用于阿里线上运维环境。链接地址:https://github.com/oldratlee/useful-scripts/。

7)JVM 内存使用及垃圾回收状态统计

#显示最后一次或当前正在发生的垃圾收集的诱发原因jstat -gccause $pid
#显示各个代的容量及使用情况jstat -gccapacity $pid
#显示新生代容量及使用情况jstat -gcnewcapacity $pid
#显示老年代容量jstat -gcoldcapacity $pid
#显示垃圾收集信息(间隔1秒持续输出)jstat -gcutil $pid 1000
# 快速杀死所有的 java 进程ps aux | grep java | awk '{ print $2 }' | xargs kill -9

你可能感兴趣的:(架构)