java排查故障思路

发布于:2024-05-05 ⋅ 阅读:(29) ⋅ 点赞:(0)

一.前言

        好长时间没有写文章了,因为遇见了个倒霉公司,全员降薪30%,我这马上就要做爸爸的人了,没钱可不行。果断告别老东家,拥抱新主子~。新公司业务逻辑比较复杂。持续学习中。。。今天我们来讲一下线上故障的排查思路。话不多说,开启正题

二.概述

 线上的故障无非就是四种,cpu,磁盘,内存,网络。出现的大多数问题都不仅仅在一个层面上面,基本出现问题了就是df,free,top 三连,然后上jstack,jmap.遇见具体问题分析即可

三.CPU

对于cpu飙升问题,一般都是因为死循环啦,频繁GC以及上下文切换过多导致的,而最常见的就是业务逻辑导致了,这时候我们可以使用jstack来分析堆栈情况

首先 ps -ef | grep '有问题的程序' 找到对应的进程pid

接着使用 top -H -p pid 来找到cpu使用率高的线程

然后即将线程id转换为16进制  printf '%x\n'  使用率高的pid

接着直接使用jstack找到对应的堆栈信息

jstack pid | grep 'nid' -C5 -color  这个nid 就是上面转化完16位的pid

这就能看到整个堆栈信息啦

但是通常我们都会对整个jstack文件进行分析,通常我们比较关注waiting和time_waiting部分,brocked就更不用说了,

使用命令 cat jstack.log | grep "java.lang.Thread.State" | sort -nr | uniq -c

什么,还不知道jstack.log怎么生成的?  肯定是  jstack -p pid > 路径/就stack.log

 对于频繁GC

        当然我们还是会使用jstack来分析问题,但有时候我们可以先确定一下到底是不是GC太频繁了,jstat -gc pid 1000    命令对gc粉黛变化情况进行官场 1000 表示采样间隔(ms)

S0C S1C  S0U S1U EC/EU OC/OU MC/MU 分别代表了两个Survivor区  Eden区 老年代,元数据区的容量和使用量。 YGC/YGT   FGC/FGCT GCT 则代表 YoungGC Fullgc的耗时和次数以及总耗时

上下文切换

针对频繁上下文切换 我们可以使用vmstat命令来查看

vmstat 5 10   是 数字5 数字10 嗷   5代表5s一次  10 代表总次数

其中结果的意思如下:

CPU状态的监控指标主要有以下几个参数获得:
r:在运行队列中等待的进程数
b:在等待IO的进程数
cs:每秒的上下文切换的次数
us:用户进程使用的CPU时间(%)
sy:系统进程使用的CPU时间(%)
id:CPU空闲时间(%)
wa:等待IO所消耗的CPU时间(%)

上面的结果发现wa特别高而用户和系统的进程cpu却不高 也就是wa高 us sy不高

同时存在b>0 也就是等待IO的进程特别多

所以目前cpu使用率高是由于IO等待造成的,并不是CPU资源不足,用户应该检查系统中正在进行IO操作的进程

下面这种情况是由大量运算造成的结果

我们可以发现 r=1 代表一直有进程在等待

in  表示每秒的终端数,包括时钟中断,运行队列中等待的进程(看参数r) 中断数in就上俩了

 us表示用户进程使用的cpu时间,随着r=1,用户的cpu占用时间直接达到了100%,id表示cpu的空闲时间,一开始的时候id很高,达到95%,后来程序开始跑,cpu一直处于繁忙状态(看参数r,us的值),id就一直为0,等程序终止,id就是上去了

后续还有详解,我们之后会仔细讲解下其他情况 包括大量系统掉哟个,大量的io操作,以及大量的占用内存。

如果我们希望对特定的pid进行监控 那么我们可以使用 pid stat -w pid 命令 cswch 和nvcswch 表示自愿切换和非自愿切换

磁盘

 磁盘问题和cpu一样都是比较基础的, 首先是磁盘空间方面 我们直接  df -hl 来查看文件系统状态

更多时候磁盘问题还是性能问题 我们可以通过iostat -d -k -x 来进行分析

  1. -d:这个参数用于指定汇报磁盘的使用情况。当使用 -d 参数时,iostat 将显示与磁盘活动相关的统计数据。
  2. -k:该参数表示在显示统计数据时,数值单位为 kilobytes(KB)。也就是说,每秒的数据量将以千字节为单位显示。
  3. -x:通过指定 -x 参数后跟上 device(设备名),可以仅对特定的磁盘设备进行统计,若未指定设备则默认统计所有设备。

最后一列  util 可以看到每块磁盘写入的进度,而rrqpm/s以及wrqm/s分别表示读写速度,一般就能帮助定位到具体哪块磁盘出现问题了。

另外,我们还需要知道哪个进程在进行读写,一般来说开发自己心里有数,或者使用iotop命令给来进行定位文件读写的来源

这里面是TID ,但是我们需要转化为PID 使用命令

 readlink -f /proc/*/task/tid/../..

其中 18500为tid 26065为pid 

找到pid之后就可以看进程具体的读写情况

cat /proc/pid/io

我们还可以通过lsof 命令来确定具体的文件读写情况

lsof -p pid  

内存

内存问题排查起来比CPU麻烦 场景也比较多 包括OOM GC 对外内存

一般我们会先使用free命令检查一下

堆内内存 主要是oom和stackOverflow

OOM

oom主要分以下几种

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

这代表没有足够的内存空间给县城分配java栈 基本上还是线程池代码写的有问题,比如忘记shutdown 使用kstack或者jmap 如果一切都正常 jvm方面可以通过指定Xss来减少单个thread stack的大小,另外也可以在系统层面 可以通过修改 /etc/security/limits.conf nofile和nproc来增大对线程的限制

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

这个意思是对的内存占用已经达到了-Xmx设置的最大值了,应该是最常见的OOM错误了,解决思路仍然是先从代码中找  可以使用jstack  jsmap去定位问题,如果说一切都正常 才需要通过调整Xmx的值来扩大内存

Caused by: java.lang.OutOfMemoryError: Meta space

这个错误代表元数据去的内存占用已经达到XX:MaxMetaspaceSize 设置的最大值,排场思路和上述一直,参数方面可以通过修改XX:MaxPermSize来进行调整

stackOverflow

Exception in thread "main" java.lang.StackOverflowError

栈溢出也很常见的错误

表示线程需要的内存大于Xss值了  先和上面一样排查,如果没有问题 调整参数Xss来调整 但是调整的太大 又会导致OMM问题

使用JMAP定位代码内存泄露问题

我们使用jmap -dump:format=b,file= filename pid 来导出dump文件

通过mat(Eclipse Memory Analysis Tools)导入dump文件进行分析,内存泄漏问题一般我们直接选Leak Suspects即可,mat给出了内存泄漏的建议。另外也可以选择Top Consumers来查看最大对象报告。和线程相关的问题可以选择thread overview进行分析。除此之外就是选择Histogram类概览来自己慢慢分析,大家可以搜搜mat的相关教程。

日常开发中,代码产生内存泄漏是比较常见的事,并且比较隐蔽,需要开发者更加关注细节。比如说每次请求都new对象,导致大量重复创建对象;进行文件流操作但未正确关闭;手动不当触发gc;ByteBuffer缓存分配不合理等都会造成代码OOM。

另一方面,我们可以在启动参数中指定-XX:+HeapDumpOnOutOfMemoryError来保存OOM时的dump文件。

gc问题和线程

gc问题除了影响cpu也会影响内存,排查思路也是一致的。一般先使用jstat来查看分代变化情况,比如youngGC或者fullGC次数是不是太多呀;EU、OU等指标增长是不是异常呀等。线程的话太多而且不被及时gc也会引发oom,大部分就是之前说的unable to create new native thread。除了jstack细细分析dump文件外,我们一般先会看下总体线程,通过pstreee -p pid |wc -l

或者直接通过查看/proc/pid/task的数量即为线程数量。 

堆外内存溢出

这个问题可太麻烦了

首先堆外内存溢出表现就是物理常驻内存增长快,报错的话视使用方式都不确定,如果由于使用Netty导致的,那错误日志里可能会出现OutOfDirectMemoryError错误,如果直接是DirectByteBuffer,那会报OutOfMemoryError: Direct buffer memory

堆外内存溢出往往是和NIO的使用相关,一般我们先通过pmap来查看下进程占用的内存情况

pmap -x pid | sort -rn -k3 | head -30,这段意思是查看对应pid倒序前30大的内存段。这边可以再一段时间后再跑一次命令看看内存增长情况,或者和正常机器比较可疑的内存段在哪里。

我们如果确定有可疑的内存端,需要通过gdb来分析gdb --batch --pid {pid} -ex "dump memory filename.dump {内存起始地址} {内存起始地址+内存块大小}" 

获取dump文件后可用heaxdump进行查看hexdump -C filename | less,不过大多数看到的都是二进制乱码。

NMT是Java7U40引入的HotSpot新特性,配合jcmd命令我们就可以看到具体内存组成了。需要在启动参数中加入 -XX:NativeMemoryTracking=summary 或者 -XX:NativeMemoryTracking=detail,会有略微性能损耗。

一般对于堆外内存缓慢增长直到爆炸的情况来说,可以先设一个基线jcmd pid VM.native_memory baseline

然后等放一段时间后再去看看内存增长的情况,通过jcmd pid VM.native_memory detail.diff(summary.diff)做一下summary或者detail级别的diff。

 

可以看到jcmd分析出来的内存十分详细,包括堆内、线程以及gc(所以上述其他内存异常其实都可以用nmt来分析),这边堆外内存我们重点关注Internal的内存增长,如果增长十分明显的话那就是有问题了。detail级别的话还会有具体内存段的增长情况, 

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和正常时间段比增长较多,那就是起的线程太多了。

触发fullGC

G1中更多的还是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():不要随便写就对了。

结尾

        好啦,大概就这么写东东啦,以后想起来再补充


网站公告

今日签到

点亮在社区的每一天
去签到