JAVA 线上故障排查全套路

线上故障主要会包括 CPU、磁盘、内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,因此进行排查时候尽可能四个方面依次排查一遍。同时例如 jstack、jmap 等工具也是不囿于一个方面的问题的,基本上出问题就是 df、free、top 三连,而后依次 jstack、jmap 伺候,具体问题具体分析便可。java

CPUios

通常来说咱们首先会排查 CPU 方面的问题。CPU 异常每每仍是比较好定位的。缘由包括业务逻辑问题(死循环)、频繁 gc 以及上下文切换过多。而最多见的每每是业务逻辑(或者框架逻辑)致使的,可使用 jstack 来分析对应的堆栈状况。程序员

使用 jstack 分析 CPU 问题面试

咱们先用 ps 命令找到对应进程的 pid(若是你有好几个目标进程,能够先用 top 看一下哪一个占用比较高)。segmentfault

接着用top -H -p pid来找到 CPU 使用率比较高的一些线程缓存

file

而后将占用最高的 pid 转换为 16 进制printf '%xn' pid获得 nid网络

file

接着直接在 jstack 中找到相应的堆栈信息jstack pid |grep 'nid' -C5 –color多线程

file

能够看到咱们已经找到了 nid 为 0x42 的堆栈信息,接着只要仔细分析一番便可。架构

固然更常见的是咱们对整个 jstack 文件进行分析,一般咱们会比较关注 WAITING 和 TIMED_WAITING 的部分,BLOCKED 就不用说了。咱们可使用命令cat jstack.log | grep "java.lang.Thread.State" | sort -nr | uniq -c来对 jstack 的状态有一个总体的把握,若是 WAITING 之类的特别多,那么多半是有问题啦。并发

file

频繁 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 的耗时和次数以及总耗时。若是看到 gc 比较频繁,再针对 gc 方面作进一步分析,具体能够参考一下 gc 章节的描述。

file

上下文切换

针对频繁上下文问题,咱们可使用vmstat命令来进行查看

file

cs(context switch)一列则表明了上下文切换的次数。

若是咱们但愿对特定的 pid 进行监控那么可使用 pidstat -w pid命令,cswch 和 nvcswch 表示自愿及非自愿切换。

file

磁盘

磁盘问题和 CPU 同样是属于比较基础的。首先是磁盘空间方面,咱们直接使用df -hl来查看文件系统状态

file

更多时候,磁盘问题仍是性能上的问题。咱们能够经过 iostatiostat -d -k -x来进行分析

file

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

另外咱们还须要知道是哪一个进程在进行读写,通常来讲开发本身内心有数,或者用 iotop 命令来进行定位文件读写的来源。

file

不过这边拿到的是 tid,咱们要转换成 pid,能够经过 readlink 来找到 pidreadlink -f /proc/*/task/tid/../..。

file

找到 pid 以后就能够看这个进程具体的读写状况cat /proc/pid/io

file

咱们还能够经过 lsof 命令来肯定具体的文件读写状况lsof -p pid

file

内存

内存问题排查起来相对比 CPU 麻烦一些,场景也比较多。主要包括 OOM、GC 问题和堆外内存。通常来说,咱们会先用free命令先来检查一发内存的各类状况。

file

堆内内存

内存问题大多还都是堆内内存问题。表象上主要分为 OOM 和 Stack Overflo。

OOM

JMV 中的内存不足,OOM 大体能够分为如下几种:

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 对线程的限制

file

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 之前的永久代了)。

Stack Overflow

栈内存溢出,这个你们见到也比较多。

Exception in thread "main" java.lang.StackOverflowError

表示线程栈须要的内存大于 Xss 值,一样也是先进行排查,参数方面经过Xss来调整,但调整的太大可能又会引发 OOM。

使用 JMAP 定位代码内存泄漏

上述关于 OOM 和 Stack Overflo 的代码排查方面,咱们通常使用 JMAPjmap -dump:format=b,file=filename pid来导出 dump 文件

file

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

file

平常开发中,代码产生内存泄漏是比较常见的事,而且比较隐蔽,须要开发者更加关注细节。好比说每次请求都 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。

file

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

file

堆外内存

若是碰到堆外内存溢出,那可真是太不幸了。首先堆外内存溢出表现就是物理常驻内存增加快,报错的话视使用方式都不肯定,若是因为使用 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 {内存起始地址} {内存起始地址+内存块大小}"

file

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

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

通常对于堆外内存缓慢增加直到爆炸的状况来讲,能够先设一个基线jcmd pid VM.native_memory baseline。

file

而后等放一段时间后再去看看内存增加的状况,经过jcmd pid VM.native_memory detail.diff(summary.diff)作一下 summary 或者 detail 级别的 diff。

file

file

能够看到 jcmd 分析出来的内存十分详细,包括堆内、线程以及 gc(因此上述其余内存异常其实均可以用 nmt 来分析),这边堆外内存咱们重点关注 Internal 的内存增加,若是增加十分明显的话那就是有问题了。

detail 级别的话还会有具体内存段的增加状况,以下图。

file

此外在系统层面,咱们还可使用 strace 命令来监控内存分配 strace -f -e "brk,mmap,munmap" -p pid

这边内存分配信息主要包括了 pid 和内存地址。

file

不过其实上面那些操做也很难定位到具体的问题点,关键仍是要看错误日志栈,找到可疑的对象,搞清楚它的回收机制,而后去分析对应的对象。好比 DirectByteBuffer 分配内存的话,是须要 full GC 或者手动 system.gc 来进行回收的(因此最好不要使用-XX:+DisableExplicitGC)。那么其实咱们能够跟踪一下 DirectByteBuffer 对象的内存状况,经过jmap -histo:live pid手动触发 fullGC 来看看堆外内存有没有被回收。若是被回收了,那么大几率是堆外内存自己分配的过小了,经过-XX:MaxDirectMemorySize进行调整。若是没有什么变化,那就要使用 jmap 去分析那些不能被 gc 的对象,以及和 DirectByteBuffer 之间的引用关系了。

GC 问题

堆内内存泄漏老是和 GC 异常相伴。不过 GC 问题不仅是和内存问题相关,还有可能引发 CPU 负载、网络问题等系列并发症,只是相对来讲和内存联系紧密些,因此咱们在此单独总结一下 GC 相关问题。

咱们在 CPU 章介绍了使用 jstat 来获取当前 GC 分代变化信息。而更多时候,咱们是经过 GC 日志来排查问题的,在启动参数中加上-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps来开启 GC 日志。

常见的 Young GC、Full GC 日志含义在此就不作赘述了。

针对 gc 日志,咱们就能大体推断出 youngGC 与 fullGC 是否过于频繁或者耗时过长,从而对症下药。咱们下面将对 G1 垃圾收集器来作分析,这边也建议你们使用 G1-XX:+UseG1GC。

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

file

触发 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():不要随便写就对了。

另外,咱们能够在启动参数中配置-XX:HeapDumpPath=/xxx/dump.hprof来 dump fullGC 相关的文件,并经过 jinfo 来进行 gc 先后的 dump

jinfo -flag +HeapDumpBeforeFullGC pid

jinfo -flag +HeapDumpAfterFullGC pid

jinfo -flag +HeapDumpBeforeFullGC pid

jinfo -flag +HeapDumpAfterFullGC pid

这样获得 2 份 dump 文件,对比后主要关注被 gc 掉的问题对象来定位问题。

网络

涉及到网络层面的问题通常都比较复杂,场景多,定位难,成为了大多数开发的噩梦,应该是最复杂的了。这里会举一些例子,并从 tcp 层、应用层以及工具的使用等方面进行阐述。

超时

超时错误大部分处在应用层面,因此这块着重理解概念。超时大致能够分为链接超时和读写超时,某些使用链接池的客户端框架还会存在获取链接超时和空闲链接清理超时。

  • 读写超时。readTimeout/writeTimeout,有些框架叫作 so_timeout 或者 socketTimeout,均指的是数据读写超时。注意这边的超时大部分是指逻辑上的超时。soa 的超时指的也是读超时。读写超时通常都只针对客户端设置。
  • 链接超时。connectionTimeout,客户端一般指与服务端创建链接的最大时间。服务端这边 connectionTimeout 就有些五花八门了,Jetty 中表示空闲链接清理时间,Tomcat 则表示链接维持的最大时间。
  • 其余。包括链接获取超时 connectionAcquireTimeout 和空闲链接清理超时 idleConnectionTimeout。多用于使用链接池或队列的客户端或服务端框架。

咱们在设置各类超时时间中,须要确认的是尽可能保持客户端的超时小于服务端的超时,以保证链接正常结束。

在实际开发中,咱们关心最多的应该是接口的读写超时了。

如何设置合理的接口超时是一个问题。若是接口超时设置的过长,那么有可能会过多地占用服务端的 tcp 链接。而若是接口设置的太短,那么接口超时就会很是频繁。

服务端接口明明 rt 下降,但客户端仍然一直超时又是另外一个问题。这个问题其实很简单,客户端到服务端的链路包括网络传输、排队以及服务处理等,每个环节均可能是耗时的缘由。

TCP 队列溢出

tcp 队列溢出是个相对底层的错误,它可能会形成超时、rst 等更表层的错误。所以错误也更隐蔽,因此咱们单独说一说。

file

如上图所示,这里有两个队列:syns queue(半链接队列)、accept queue(全链接队列)。三次握手,在 server 收到 client 的 syn 后,把消息放到 syns queue,回复 syn+ack 给 client,server 收到 client 的 ack,若是这时 accept queue 没满,那就从 syns queue 拿出暂存的信息放入 accept queue 中,不然按 tcp_abort_on_overflow 指示的执行。

tcp_abort_on_overflow 0 表示若是三次握手第三步的时候 accept queue 满了那么 server 扔掉 client 发过来的 ack。tcp_abort_on_overflow 1 则表示第三步的时候若是全链接队列满了,server 发送一个 rst 包给 client,表示废掉这个握手过程和这个链接,意味着日志里可能会有不少connection reset / connection reset by peer。

那么在实际开发中,咱们怎么能快速定位到 tcp 队列溢出呢?

netstat 命令,执行 netstat -s | egrep "listen|LISTEN"

file

如上图所示,overflowed 表示全链接队列溢出的次数,sockets dropped 表示半链接队列溢出的次数。

ss 命令,执行 ss -lnt

file

上面看到 Send-Q 表示第三列的 listen 端口上的全链接队列最大为 5,第一列 Recv-Q 为全链接队列当前使用了多少。

接着咱们看看怎么设置全链接、半链接队列大小吧:

全链接队列的大小取决于 min(backlog, somaxconn)。backlog 是在 socket 建立的时候传入的,somaxconn 是一个 os 级别的系统参数。而半链接队列的大小取决于 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。

在平常开发中,咱们每每使用 servlet 容器做为服务端,因此咱们有时候也须要关注容器的链接队列大小。在 Tomcat 中 backlog 叫作acceptCount,在 Jetty 里面则是acceptQueueSize。

RST 异常

RST 包表示链接重置,用于关闭一些无用的链接,一般表示异常关闭,区别于四次挥手。

在实际开发中,咱们每每会看到connection reset / connection reset by peer错误,这种状况就是 RST 包致使的。

端口不存在

若是像不存在的端口发出创建链接 SYN 请求,那么服务端发现本身并无这个端口则会直接返回一个 RST 报文,用于中断链接。

主动代替 FIN 终止链接

通常来讲,正常的链接关闭都是须要经过 FIN 报文实现,然而咱们也能够用 RST 报文来代替 FIN,表示直接终止链接。实际开发中,可设置 SO_LINGER 数值来控制,这种每每是故意的,来跳过 TIMED_WAIT,提供交互效率,不闲就慎用。

客户端或服务端有一边发生了异常,该方向对端发送 RST 以告知关闭链接

咱们上面讲的 tcp 队列溢出发送 RST 包其实也是属于这一种。这种每每是因为某些缘由,一方没法再能正常处理请求链接了(好比程序崩了,队列满了),从而告知另外一方关闭链接。

接收到的 TCP 报文不在已知的 TCP 链接内

好比,一方机器因为网络实在太差 TCP 报文失踪了,另外一方关闭了该链接,而后过了许久收到了以前失踪的 TCP 报文,但因为对应的 TCP 链接已不存在,那么会直接发一个 RST 包以便开启新的链接。

一方长期未收到另外一方的确认报文,在必定时间或重传次数后发出 RST 报文

这种大多也和网络环境相关了,网络环境差可能会致使更多的 RST 报文。

以前说过 RST 报文多会致使程序报错,在一个已关闭的链接上读操做会报connection reset,而在一个已关闭的链接上写操做则会报connection reset by peer。一般咱们可能还会看到broken pipe错误,这是管道层面的错误,表示对已关闭的管道进行读写,每每是在收到 RST,报出connection reset错后继续读写数据报的错,这个在 glibc 源码注释中也有介绍。

咱们在排查故障时候怎么肯定有 RST 包的存在呢?固然是使用 tcpdump 命令进行抓包,并使用 wireshark 进行简单分析了。tcpdump -i en0 tcp -w xxx.cap,en0 表示监听的网卡。

file

接下来咱们经过 wireshark 打开抓到的包,可能就能看到以下图所示,红色的就表示 RST 包了。

file

TIME_WAIT 和 CLOSE_WAIT

TIME_WAIT 和 CLOSE_WAIT 是啥意思相信你们都知道。

在线上时,咱们能够直接用命令netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'来查看 time-wait 和 close_wait 的数量

用 ss 命令会更快ss -ant | awk '{++S[$1]} END {for(a in S) print a, S[a]}'

file

TIME_WAIT

time_wait 的存在一是为了丢失的数据包被后面链接复用,二是为了在 2MSL 的时间范围内正常关闭链接。它的存在其实会大大减小 RST 包的出现。

过多的 time_wait 在短链接频繁的场景比较容易出现。这种状况能够在服务端作一些内核参数调优:

表示开启重用。容许将TIME-WAIT sockets从新用于新的TCP链接,默认为0,表示关闭

net.ipv4.tcp_tw_reuse = 1

表示开启TCP链接中TIME-WAIT sockets的快速回收,默认为0,表示关闭

net.ipv4.tcp_tw_recycle = 1

表示开启重用。容许将TIME-WAIT sockets从新用于新的TCP链接,默认为0,表示关闭

net.ipv4.tcp_tw_reuse = 1

表示开启TCP链接中TIME-WAIT sockets的快速回收,默认为0,表示关闭

net.ipv4.tcp_tw_recycle = 1

固然咱们不要忘记在 NAT 环境下由于时间戳错乱致使数据包被拒绝的坑了,另外的办法就是改小tcp_max_tw_buckets,超过这个数的 time_wait 都会被干掉,不过这也会致使报time wait bucket table overflow的错。

CLOSE_WAIT

close_wait 每每都是由于应用程序写的有问题,没有在 ACK 后再次发起 FIN 报文。close_wait 出现的几率甚至比 time_wait 要更高,后果也更严重。每每是因为某个地方阻塞住了,没有正常关闭链接,从而渐渐地消耗完全部的线程。

想要定位这类问题,最好是经过 jstack 来分析线程堆栈来排查问题,具体可参考上述章节。这里仅举一个例子。

开发同窗说应用上线后 CLOSE_WAIT 就一直增多,直到挂掉为止,jstack 后找到比较可疑的堆栈是大部分线程都卡在了countdownlatch.await方法,找开发同窗了解后得知使用了多线程可是确没有 catch 异常,修改后发现异常仅仅是最简单的升级 sdk 后常出现的class not found。

今天就分享这么多,欢迎各位朋友在留言区评论,对于有价值的留言,我都会一一回复的。若是以为文章对你有一丢丢帮助,请给我点个赞吧,让更多人看到该文章。另外,小编最近将收集的Java程序员进阶架构师和面试的资料作了一些整理,免费分享给每一位学习Java的朋友,须要的能够进群:751827870,欢迎你们进群和我一块儿交流。

相关文章
相关标签/搜索