前言
线上故障主要会包括cpu、磁盘、内存以及网络问题,但大多数故障不止是一个层次的问题,所以要依次排查。
CPU
一般来讲我们首先会排查cpu方面的问题。cpu异常往往还是比较好定位的。原因包括业务逻辑问题(死循环)、频繁gc以及上下文切换过多。而最常见的往往是业务逻辑(或者框架逻辑)导致的,可以使用jstack来分析对应的堆栈情况。
使用jstack分析cpu问题
首先找到对应进程的pid
pgrep hello / lsof -i:8089
根据pid找到cpu使用率较高的几个线程
top -H -p pid
然后将占用最高的pid转换为16进制nid
printf ‘%x\n’ pid
直接在jstack中找到相应的堆栈信息
jstack pid |grep ‘nid’ -C5 –color
cat jstack.log | grep “java.lang.Thread.State” | sort -nr | uniq -c
如果WAITING之类的特别多,那么多半是有问题啦。
频繁GC
当然我们还是会使用jstack来分析问题,但有时候我们可以先确定下gc是不是太频繁,对gc分代变化情况进行观察
jstat -gc pid 1000
如果看到gc比较频繁,再针对gc方面做进一步分析,具体可以参考一下gc章节的描述。
频繁上下文切换
针对频繁上下文问题,我们可以使用vmstat命令来进行查看
vmstat 1
cs(context switch)一列则代表了上下文切换的次数。如果要对特定pid进行监控。
pidstat -w pid
cswch和nvcswch表示自愿及非自愿切换。
磁盘
磁盘问题和cpu一样是属于比较基础的。首先是磁盘空间方面,我们来查看文件系统状态
df -hl
更多时候,磁盘问题还是性能上的问题。我们可以通过iostat来进行分析:
iostat -d -k -x
最后一列%util可以看到每块磁盘写入的程度,而rrqpm/s以及wrqm/s分别表示读写速度,一般就能帮助定位到具体哪块磁盘出现问题了。
使用iotop找出使用磁盘繁忙的进程pid
iotop -Po 参数P表示只显示进程,不显示线程,参数o表示只显示正在进行io操作的进程
可以通过lsof命令来确定具体的文件读写情况lsof -p pid
内存
内存问题排查起来相对比CPU麻烦一些,场景也比较多。主要包括OOM、GC问题和堆外内存。一般来讲,我们会先用free命令先来检查一发内存的各种情况。
内存问题大多还都是堆内内存问题。表象上主要分为OOM和StackOverflow。
内存不足报错情况
Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread
这个意思是没有足够的内存空间给线程分配java栈,基本上还是线程池代码写的有问题,比如说忘记shutdown,所以说应该首先从代码层面来寻找问题,使用jstack或者jmap。如果一切都正常,JVM方面可以通过指定Xss来减少单个thread stack的大小。另外也可以在系统层面,可以通过修改/etc/security/limits.confnofile和nproc来增大os对线程的限制
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
这个意思是堆的内存占用已经达到-Xmx设置的最大值,应该是最常见的OOM错误了。解决思路仍然是先应该在代码中找,怀疑存在内存泄漏,通过jstack和jmap去定位问题。如果说一切都正常,才需要通过调整Xmx的值来扩大内存。
Caused by: java.lang.OutOfMemoryError: Meta space
这个意思是元数据区的内存占用已经达到XX:MaxMetaspaceSize设置的最大值,排查思路和上面的一致,参数方面可以通过XX:MaxPermSize来进行调整(这里就不说1.8以前的永久代了)。
Exception in thread “main” java.lang.StackOverflowError
栈内存溢出,表示线程栈需要的内存大于Xss值,同样也是先进行排查,参数方面通过Xss来调整,但调整的太大可能又会引起OOM。
内存问题定位
上述关于OOM和StackOverflow的代码排查方面,我们一般使用JMAPjmap -dump:format=b,file=filename pid来导出dump文件。
通过mat(Eclipse Memory Analysis Tools)导入dump文件进行分析。
gc问题
gc问题除了影响cpu也会影响内存,排查思路也是一致的。一般先使用jstat来查看分代变化情况,比如youngGC或者fullGC次数是不是太多呀;EU、OU等指标增长是不是异常呀等。线程的话太多而且不被及时gc也会引发oom,大部分就是之前说的unable to create new native thread。
除了jstack细细分析dump文件外,我们一般先会看下总体线程,通过pstreee -p pid |wc -l。
堆外内存
如果由于使用Netty导致的,那错误日志里可能会出现OutOfDirectMemoryError错误,如果直接是DirectByteBuffer,那会报OutOfMemoryError: Direct buffer memory。
堆外内存溢出往往是和NIO的使用相关
GC
堆内内存泄漏总是和GC异常相伴。不过GC问题不只是和内存问题相关,还有可能引起CPU负载、网络问题等系列并发症,只是相对来说和内存联系紧密些,所以我们在此单独总结一下GC相关问题。
在启动参数中加上-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps来开启GC日志。
youngGC过频繁youngGC频繁一般是短周期小对象较多,先考虑是不是Eden区/新生代设置的太小了,看能否通过调整-Xmn、-XX:SurvivorRatio等参数设置来解决问题。如果参数正常,但是young gc频率还是太高,就需要使用Jmap和MAT对dump文件进行进一步排查了。
youngGC耗时过长耗时过长问题就要看GC日志里耗时耗在哪一块了。以G1日志为例,可以关注Root Scanning、Object Copy、Ref Proc等阶段。Ref Proc耗时长,就要注意引用相关的对象。Root Scanning耗时长,就要注意线程数、跨代引用。Object Copy则需要关注对象生存周期。而且耗时分析它需要横向比较,就是和其他项目或者正常时间段的耗时比较。比如说图中的Root Scanning和正常时间段比增长较多,那就是起的线程太多了。
触发fullGCG1中更多的还是mixedGC,但mixedGC可以和youngGC思路一样去排查。触发fullGC了一般都会有问题,G1会退化使用Serial收集器来完成垃圾的清理工作,暂停时长达到秒级别,可以说是半跪了。fullGC的原因可能包括以下这些,以及参数调整方面的一些思路:
- 并发阶段失败:在并发标记阶段,MixGC之前老年代就被填满了,那么这时候G1就会放弃标记周期。这种情况,可能就需要增加堆大小,或者调整并发标记线程数-XX:ConcGCThreads。
- 晋升失败:在GC的时候没有足够的内存供存活/晋升对象使用,所以触发了Full GC。这时候可以通过-XX:G1ReservePercent来增加预留内存百分比,减少-XX:InitiatingHeapOccupancyPercent来提前启动标记,-XX:ConcGCThreads来增加标记线程数也是可以的。
- 大对象分配失败:大对象找不到合适的region空间进行分配,就会进行fullGC,这种情况下可以增大内存或者增大-XX:G1HeapRegionSize。
- 程序主动执行System.gc():不要随便写就对了。
我们可以在启动参数中配置-XX:HeapDumpPath=/xxx/dump.hprof来dump fullGC相关的文件。