JAVA线上问题排查及常用命令

前言

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

CPU问题

CPU 异常往往还是比较好定位的。原因包括业务逻辑问题(死循环)、频繁 gc 以及上下文切换过多.

业务逻辑

  1.  先用 top 看一下哪个占用比较高,找到对应进程的pid

  2. 我们先用 ps 命令,查看此进程的启动命令,了解进程的作用
  3. 接着用top -H -p pid来找到 CPU 使用率比较高的一些线程JAVA 线上故障排查全套路
  4. 然后将占用最高的线程 pid 转换为 16 进制得到 nid
  5. 接着直接在 jstack 中找到相应的堆栈信息jstack pid |grep 'nid' -C5 –color

    JAVA 线上故障排查全套路

  6. 可以看到我们已经找到了 nid 为 0x42 的堆栈信息,接着结合代码仔细分析一番即可。

频繁 gc

     参考《JVMGC优化

上下文切换

针对频繁上下文问题,我们可以使用vmstat 1命令来每1s进行查看一次

JAVA 线上故障排查全套路

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

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

JAVA 线上故障排查全套路

内存

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

JAVA 线上故障排查全套路

堆内内存

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

OOM

JMV 中的内存不足,OOM 大致可以分为以下几种:

1. 没有足够的内存空间给线程分配 Java 栈

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

  • 使用 jstack 或者 jmap,从代码层面来寻找问题,比如说线程池忘记 shutdown,
  • 如果一切都正常,JVM 方面可以通过指定Xss来减少单个 thread stack 的大小。
  • 另外也可以在系统层面,可以通过修改/etc/security/limits.confnofile 和 nproc 来增大 os 对线程的限制

JAVA 线上故障排查全套路

2. 堆的内存占用已经达到-Xmx 设置的最大值(无法回收),应该是最常见的 OOM 错误了

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

  • 通过 jstack和 jmap去定位辅助定位代码内存泄漏
  • 如果说一切都正常,才需要通过调整Xmx的值来扩大内存

3.  元数据区的内存占用已经达到XX:MaxMetaspaceSize设置的最大值

Caused by: java.lang.OutOfMemoryError: Meta space

  • 主要原因, 是加载到内存中的 class 数量太多或者体积太大
  • 通过Jstat,jmap查看没必要的class类加载
  • 如果一切正常加大XX:MaxMetaspaceSize值

Stack Overflow

栈内存溢出,表示线程栈需要的内存大于 Xss 值

Exception in thread "main" java.lang.StackOverflowError

  • 通过jstack进行排查,比如递归问题引起
  • 如果一切正常加大Xss值,但调整的太大可能又会引起 OOM

使用 JMAP 定位代码内存泄漏

上述关于 jmap帮助的代码排查方的主要步聚如下:

  • 使用 JMAP jmap -dump:format=b,file=filename pid来导出 dump 文件JAVA 线上故障排查全套路
  • 通过 mat(Eclipse Memory Analysis Tools)导入 dump 文件进行分析,内存泄漏问题一般我们直接选 Leak Suspects 即可,mat 给出了内存泄漏的建议。另外也可以选择 Top Consumers 来查看最大对象报告。和线程相关的问题可以选择 thread overview 进行分析。除此之外就是选择 Histogram 类概览来自己慢慢分析,大家可以搜搜 mat 的相关教程。JAVA 线上故障排查全套路

日常开发中,代码产生内存泄漏是比较常见的事,并且比较隐蔽,需要开发者更加关注细节。

  1. 比如说每次请求都 new 对象,导致大量重复创建对象;
  2. 进行文件流操作但未正确关闭;
  3. 手动不当触发 gc;
  4. ByteBuffer 缓存分配不合理。

另一方面,我们可以在启动参数中指定-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 查询线程数。JAVA 线上故障排查全套路

堆外内存

如果碰到堆外内存溢出,那可真是太不幸了。

首先堆外内存溢出表现就是物理常驻内存增长快,报错的话视使用方式都不确定,

  1. 如果由于使用 Netty 导致的,那错误日志里可能会出现OutOfDirectMemoryError错误,
  2. 如果直接是 DirectByteBuffer,那会报OutOfMemoryError: Direct buffer memory。

堆外内存溢出往往是和 NIO 的使用相关

  1. 一般我们先通过 pmap 来查看下进程占用的内存情况pmap -x pid | sort -rn -k3 | head -30,这段意思是查看对应 pid 倒序前 30 大的内存段。JAVA 线上故障排查全套路
  2. 再一段时间后,再跑一次1的命令看看内存增长情况,或者和正常机器比较可疑的内存段在哪里
  3. NMT 是 Java7U40 引入的 HotSpot 新特性,配合 jcmd 命令我们就可以看到具体内存组成了。需要在启动参数中加入 -XX:NativeMemoryTracking=summary 或者 -XX:NativeMemoryTracking=detail,会有略微性能损耗。
  4. 一般对于堆外内存缓慢增长直到爆炸的情况来说,可以先设一个基线jcmd pid VM.native_memory baseline。然后等放一段时间后再去看看内存增长的情况,通过jcmd pid VM.native_memory detail.diff(summary.diff)做一下 summary 或者 detail 级别的 diff。JAVA 线上故障排查全套路

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

不过其实上面那些操作也很难定位到具体的问题点,关键还是要看错误日志栈,找到可疑的对象,搞清楚它的回收机制,然后去分析对应的对象。

比如 DirectByteBuffer 分配内存的话,是需要 full GC 或者手动 system.gc 来进行回收的(所以最好不要使用-XX:+DisableExplicitGC)。那么其实我们可以跟踪一下 DirectByteBuffer 对象的内存情况

  • 通过jmap -histo:live pid手动触发 fullGC 来看看堆外内存有没有被回收。
  • 如果被回收了,那么大概率是堆外内存本身分配的太小了,通过-XX:MaxDirectMemorySize进行调整。
  • 如果没有什么变化,那就要使用 jmap 去分析那些不能被 gc 的对象,以及和 DirectByteBuffer 之间的引用关系了。

 磁盘

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

JAVA 线上故障排查全套路

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

JAVA 线上故障排查全套路

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

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

JAVA 线上故障排查全套路

不过这边拿到的是 tid(线程id),我们要转换成 pid,可以通过 readlink 来找到 pidreadlink -f /proc/*/task/tid/../..。

JAVA 线上故障排查全套路

找到 pid 之后就可以看这个进程具体的读写情况cat /proc/pid/io

JAVA 线上故障排查全套路

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

JAVA 线上故障排查全套路

网络

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

超时

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

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

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

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

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

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

TCP 队列溢出

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

JAVA 线上故障排查全套路

如上图所示,这里有两个队列:syns queue(半连接队列)、accept queue(全连接队列)。三次握手

  1. 在 server 收到 client 的 syn 后,把消息放到 syns queue,回复 syn+ack 给 client,
  2. server 收到 client 的 ack,如果这时 accept queue 没满,那就从 syns queue 拿出暂存的信息放入 accept queue 中,否则按 tcp_abort_on_overflow 指示的执行
  3. tcp_abort_on_overflow 0 表示如果三次握手第三步的时候 accept queue 满了那么 server 扔掉 client 发过来的 ack。
  4. tcp_abort_on_overflow 1 则表示第三步的时候如果全连接队列满了,server 发送一个 rst 包给 client,表示废掉这个握手过程和这个连接,意味着日志里可能会有很多connection reset / connection reset by peer。

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

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

JAVA 线上故障排查全套路

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

ss 命令,执行 ss -lnt

JAVA 线上故障排查全套路

上面看到 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 源码注释中也有介绍。

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]}'

JAVA 线上故障排查全套路

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 要更高,后果也更严重。往往是由于某个地方阻塞住了,没有正常关闭连接,从而渐渐地消耗完所有的线程。

主要参考

JAVA 线上故障排查全套路

sysstat Linux 性能监控工具

jdk工具

聊聊HotSpot VM的Native Memory Tracking

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值