Java线上故障排查

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java线上故障排查相关的知识,希望对你有一定的参考价值。

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

      内存泄漏、内存溢出、CPU飙升三者之间的关系:内存泄露可能会导致内存溢出。 内存溢出会抛出异常,内存泄露不会抛出异常,大多数时候程序看起来是正常运行的。 内存泄露的程序,JVM频繁进行FullGC尝试释放内存空间,进而会导致CPU飙升,内存泄露过多,造成可回收内存不足,程序申请内存失败,结果就是内存溢出。

一、CPU问题

CPU 异常往往还是比较好定位的。原因包括业务逻辑问题(死循环)、频繁 gc 以及上下文切换过多。而最常见的往往是业务逻辑(或者框架逻辑)导致的,可以使用 jstack 来分析对应的堆栈情况,具体步骤是:

  1. 查看当前占用cpu最高的进程pid:top
  2. 获取当前进程中所有线程占CPU的情况:top -Hp pid
  3. 将占用最高的pid转换为16进制得到nid:printf %x\\n pid
  4. 在 jstack 中找到相应的堆栈信息:jstack pid |grep nid -C5 –color
  5. 保存线程栈现场到指定文件里分析:jstack pid > jstack.log

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

gc问题:

查看堆的各个部分的详细的使用情况:

  1. jstat -gc pid [1000 10]

查看gc情况,每1秒打印一次总共打印10次(可选),可以查看各个带的使用总大小和使用大小对于jvm的优化就是要去优化它的FullGC次数,FullGC越少越好,最好控制在FullGC几个小时甚至几天一次,具体看业务的情况。

jstat参数说明:

S0C:第一个幸存区的大小(From Survivor区),以下几个容量的单位都是KB 
S1C:第二个幸存区的大小 (To Survivor区)
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小 (Eden区)
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小(元空间)
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间,单位s
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间,单位s
GCT:垃圾回收消耗总时间,单位s

根据jstat查看出来的gc情况,我们可能需要以下几个主要指标:

各内存区域大小是否合理;
观察Eden区的对象增长,如每秒有多少对象创建;
每次YoungGC后有多少对象存活下来、有多少对象进入了老年代;
YoungGC的耗时;
FullGC触发频率及耗时;

二、内存问题

内存问题排查起来相对比 CPU 麻烦一些,场景也比较多。主要包括 OOM、GC 问题和堆外内存。

2.1 内存溢出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 对线程的限制

  • 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来进行调整。

2.2 栈内存溢出Stack Overflow

  • Exception in thread "main" java.lang.StackOverflowError

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

2.3 内存溢出的检查方法

  1. 在启动参数中指定-XX:+HeapDumpOnOutOfMemoryError来保存 OOM 时的 dump 文件
  2. 导出 dump 文件:jmap -dump:format=b,file=filename pid
  3. 通过 mat(Eclipse Memory Analysis Tools)导入 dump 文件进行分析,内存泄漏问题一般我们直接选 Leak Suspects 即可,mat 给出了内存泄漏的建议;另外也可以选择 Top Consumers 来查看最大对象报告;和线程相关的问题可以选择 thread overview 进行分析;除此之外就是选择 Histogram 类概览来自己慢慢分析。
  4. 线程的话太多而且不被及时 gc 也会引发 oom,大部分就是之前说的unable to create new native thread。除了 jstack 细细分析 dump 文件外,我们一般先会看下总体线程,通过pstreee -p pid |wc -l

三、网络问题

涉及到网络层面的问题一般都比较复杂,场景多,从 tcp 层、应用层以及工具的使用等方面进行阐述。

3.1 超时

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

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

我们在设置各种超时时间中,需要确认的是尽量保持客户端的超时小于服务端的超时,以保证连接正常结束。

3.2 TCP队列溢出

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

overflowed 表示全连接队列溢出的次数,sockets dropped 表示半连接队列溢出的次数。

  • ss 命令,执行 ss -lnt

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

3.3 RST 异常

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

在实际开发中,我们往往会看到connection reset / connection reset by peer错误,这种情况就是 RST 包导致的。

3.4 端口不存在

如果像不存在的端口发出建立连接 SYN 请求,那么服务端发现自己并没有这个端口则会直接返回一个 RST 报文,用于中断连接。

3.5 主动代替 FIN 终止连接

一般来说,正常的连接关闭都是需要通过 FIN 报文实现,然而我们也可以用 RST 报文来代替 FIN,表示直接终止连接。实际开发中,可设置 SO_LINGER 数值来控制,这种往往是故意的,来跳过 TIMED_WAIT,提供交互效率,不闲就慎用。

3.6 TIME_WAIT 和 CLOSE_WAIT

ss -ant | awk ++S[$1] END for(a in S) print a, S[a]

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 往往都是因为应用程序写的有问题,没有在 ACK 后再次发起 FIN 报文。close_wait 出现的概率甚至比 time_wait 要更高,后果也更严重。往往是由于某个地方阻塞住了,没有正常关闭连接,从而渐渐地消耗完所有的线程。想要定位这类问题,最好是通过 jstack 来分析线程堆栈来排查问题。

以上是关于Java线上故障排查的主要内容,如果未能解决你的问题,请参考以下文章

Java: 线上故障如何快速排查?来看这套技巧大全(高德地图的总结)

深入理解JVM虚拟机15:Java线上故障排查全套路总结

Java线上故障排查不会怎么办,p8大佬总结的套路清单带你轻松玩转!

JAVA 线上故障排查完整套路,从 CPU磁盘内存网络GC 一条龙!

线上故障排查全套路,运维小哥可自查~

线上故障排查全套路盘点,运维一片就够