推广 热搜: csgo  vue  angelababy  2023  gps  新车  htc  落地  app  p2p 

JAVA 线上故障排查(CPU、磁盘、内存、网络、GC)

   2023-07-10 网络整理佚名850
核心提示:线上故障主要会包括cpu、磁盘、内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍。磁盘问题和cpu一样是属于比较基础的。更多时候,磁盘问题还是性能上的问题。表示线程栈需要的内存大于Xss值,同样也是先进行排查,参数方面通过Xss来调整,但调整的太大可能又会引起OOM。gc问题除了影响cpu也会影响内存,排查思路也是一致的。

线上故障主要包括cpu、磁盘、内存、网络问题,而且大多数故障可能包含不止一级的问题,因此在排查故障时尽量依次检查四个方面。 同时,例如jmap等工具并不局限于解决问题的某一方面。 基本上问题就是连续三个df、free、top,然后jmap依次服务,具体问题可以具体分析。

中央处理器

一般来说,我们首先会排除cpu的问题。 CPU 异常通常可以更好地定位。 原因包括业务逻辑问题(死循环)、GC频繁、上下文切换过多等。 最常见的往往是业务逻辑(或框架逻辑)引起的,可以通过分析相应的堆栈情况。

使用分析cpu问题

我们首先使用 ps 命令找到对应进程的 pid(如果有多个目标进程,可以使用 top 来查看哪一个占用较多)。然后使用 top -H -p pid 找到一些 pid 比较高的线程CPU使用率

然后将占用最高的pid转换为十六进制'%x\n'pid即可得到nid

然后直接找到对应的堆栈信息pid |grep 'nid' -C5 –color

可以看到,我们已经找到了nid为0x42的堆栈信息,接下来我们只需要仔细分析即可。

当然,我们更常见的是分析整个文件,通常我们会更关注总和和部分,所以这是不言而喻的。 我们可以使用命令 cat .log | grep "java.lang..State" | grep "java.lang..State" | grep "java.lang..State" | 排序-nr | uniq -c 对状态有一个整体的把握。 如果这样的太多,那么可能会出现问题。

频繁GC

当然我们还是会用它来分析问题,不过有时候我们可以先判断gc是否过于频繁,使用jstat -gc pid 1000命令观察gc代数的变化,1000表示采样间隔(ms )、S0C/S1C、S0U/S1U、EC/EU、OC/OU、MC/MU分别代表Eden区、老年代、元数据区两个区域的容量和使用情况。 YGC/YGT、FGC/FGCT、GCT分别代表耗时、次数、总耗时。 如果看到gc比较频繁,那么就对gc做进一步的分析。 具体可以参考gc章节的描述。

上下文切换

对于频繁出现的上下文问题,我们可以使用命令查看

cs( ) 列表示上下文切换的次数。 如果我们想监控特定的pid,可以使用-w pid命令,cswch表示自愿和非自愿切换。

磁盘

磁盘问题和cpu一样基本。首先,磁盘空间方面,我们直接使用df -hl 查看文件系统的状态

很多时候,磁盘问题都是性能问题。我们可以使用 -d -k -x 来分析

最后一栏%util中可以看到各个磁盘的写入级别,而rrqpm/s和wrqm/s分别表示读写速度,一般可以帮助定位出现问题的具体磁盘。

另外,我们还需要知道哪个进程在读,哪个进程在写。 一般来说,开发者都心知肚明,或者使用iotop命令来定位文件读写的来源。

但是我们这里得到的是tid,我们要把它转换成pid,我们可以通过 -f /proc/*/task/tid/../.. 找到它

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

我们还可以使用lsof命令来判断具体的文件读写情况 lsof -p pid

记忆

排查内存问题比CPU更麻烦,而且场景也更多。 主要包括OOM、GC问题和堆外内存。 一般来说,我们会首先使用free命令来检查内存的各种情况。

堆内存

大多数内存问题也是堆内存问题。 外观主要分为OOM和.

OOM

JMV、OOM中内存不足大致可以分为以下几类:

in "main" java.lang.: to new 这意味着没有足够的内存空间来分配java堆栈给线程。 基本上都是线程池代码有问题,比如忘记了,所以我们首先应该从代码层面寻找问题,使用或者jmap。 如果一切正常,JVM可以通过指定Xss来减小单个堆栈的大小。另外,在系统层面,可以通过修改/etc//来增加os对线程的限制。 和nproc

在“main”java.lang.:Java堆空间中表示堆的内存使用量已经达到了-Xmx设置的最大值,这应该是最常见的OOM错误。 解决办法还是先在代码中查找,怀疑有内存泄漏,使用jmap来定位问题。 如果一切正常,则需要通过调整Xmx的值来扩大内存。

by: java.lang.: meta space 表示元数据区域的内存占用已经达到了XX:设置的最大值,排查思路与上面一致。 可以通过XX来调整参数:(更不用说永久代的1.8之前的了)。

堆栈内存溢出,你已经见过很多这种情况了。 在“主”java.lang中。 表示线程堆栈需要的内存大于Xss值。 也是先检查一下。 通过Xss调整参数,但如果调整过大,可能会导致OOM。

使用JMAP定位代码内存泄漏

对于上述OOM和代码排查,我们一般使用-dump:=b,file=pid来导出dump文件

通过mat(工具)导入dump文件进行分析。 一般我们可以直接选择Leak进行内存泄漏。 mat 给出了内存泄漏的建议。 或者,选择“顶部”以查看最大的对象报告。 可以选择分析与线程相关的问题。 另外,选课概述自己慢慢分析。 你可以在mat上搜索相关教程。

在日常开发中,代码内存泄漏是比较常见且隐蔽的,需要开发者多关注细节。 例如,每次请求都会创建新的对象,导致对象大量重复创建; 文件流操作未正确关闭; 手动不当触发GC; 不合理的缓存分配会导致代码OOM。

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

gc 问题和线程

gc问题除了影响CPU之外,还会影响内存,排查思路是一样的。 一般先用jstat查看代次变化,比如次数是否过多; EU、OU等指标增长是否异常等。线程过多,不及时gc也会造成oom,大部分就是前面提到的to new。 除了详细分析dump文件之外,我们一般先看线程整体,通过-p pid |wc -l。

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

堆外内存

如果遇到堆外内存溢出确实很不幸。 首先,堆外内存溢出的表现是物理常驻内存快速增长。 如果报错,则取决于使用方法。 如果是使用Netty导致的,错误日志中可能会出现or的错误。 如果是直接的话,会报: 。

堆外内存溢出往往与NIO的使用有关。 一般我们首先通过pmap查看进程占用的内存 pmap -x pid | 排序-rn -k3 | head -30,表示逆序查看pid对应的前30大内存段。 这里可以在一段时间后再次运行该命令,查看内存增长情况,或者与正常机器相比可疑内存段在哪里。

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

获取转储文件后,可以使用-C | 较少查看,但看到的文件大部分都是二进制乱码。

NMT是引入的新功能。 通过jcmd命令,我们可以看到具体的内存构成。 需要在启动参数中添加-XX:=或-XX:=,会有轻微的性能损失。

一般对于堆外内存增长缓慢直至爆炸的情况,可以先设置一个基线jcmd pid VM。

然后等待一段时间检查内存增长情况,并使用 jcmd pid VM..diff(.diff) 做一些或级别 diff。

可以看到jcmd分析的内存非常详细,包括堆、线程、gc(所以上面提到的其他内存异常其实都可以通过nmt来分析)。 这里重点关注堆外内存的内存增长情况,如果增长非常明显的话。 那么有一个问题。 对于关卡来说,还会有特定内存段的增长,如下图所示。

另外,在系统层面,我们还可以通过命令来监控内存分配 -f -e "brk,mmap," -p pid 这里的内存分配信息主要包括pid和内存地址。

但事实上,上述操作很难定位具体问题。 关键是看错误日志堆栈,找到可疑对象,弄清楚其恢复机制,然后分析对应的对象。 例如,如果分配了内存,则需要full GC或手动.gc进行回收(所以最好不要使用-XX:+)。 所以其实我们可以跟踪对象的内存,通过jmap -histo:live pid手动触发,看看堆外的内存是否被回收了。 如果是回收的话,那么很大概率是堆外内存本身分配太小,可以通过-XX:来调整。 如果没有变化,那么使用jmap来分析那些不能被gc的对象,以及它们之间的引用关系。

气相色谱问题

堆内内存泄漏总是伴随着GC异常。 不过GC问题不仅仅和内存问题有关,还可能会引起CPU负载、网络问题等一系列并发症,但又和内存关系比较密切,所以这里单独总结一下GC相关的问题。

我们在cpu章节中介绍过使用jstat获取当前GC分代变化信息。 更多时候,我们使用GC日志来排查问题,在启动参数中添加-:gc -XX:+ -XX:+ -XX:+来开启GC日志。 常见的Young GC和Full GC日志的含义这里不再赘述。

根据gc日志,我们可以大致推断是否是连接过于频繁或者耗时过长,从而对症下药。 下面我们将分析G1垃圾收集器,这里也推荐大家使用G1-XX:+。

太频繁一般是因为短时间内小物体较多。 首先考虑是否Eden区/新生代设置太小,看看是否可以通过调整-Xmn、-XX:等参数设置来解决问题。 如果参数正常,但young gc的频率仍然过高,则需要使用Jmap和MAT进一步检查转储文件。

花费时间过长的问题取决于GC日志中时间花费在哪里。 以G1日志为例,可以重点关注Root、Copy、Ref Proc等阶段。 Ref Proc耗时较长,所以一定要注意引用相关对象。 root需要的时间比较长,所以要注意线程数和跨代引用。 Copy需要注意对象的生命周期。 而且,耗时的分析需要横向比较,也就是与其他项目或者正常时间段进行耗时的比较。 例如,如果图中的Root增长超过正常时间段,则意味着启动了太多线程。

还有更多的触发因素,但可以用与思考相同的方式来检查它们。 触发时通常会出现问题。 G1会退化并使用收集器完成垃圾清理工作。 停顿时间达到了二级,可以说是半跪了。 原因可能有以下几个,以及参数调整的一些思路:

另外,我们可以在启动参数中配置-XX:=/xxx/dump.hprof来dump相关文件,并在gc前后使用jinfo进行dump

jinfo -flag +HeapDumpBeforeFullGC pid
jinfo -flag +HeapDumpAfterFullGC pid

这样就得到了两个dump文件。 经过对比,我们主要针对gc丢弃的问题对象来定位问题。

网络

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

暂停

大多数超时错误都发生在应用程序级别,因此本文重点是理解这个概念。 超时大致可以分为连接超时和读写超时。 一些使用连接池的客户端框架也有连接超时和空闲连接清理超时。

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

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

如何设置合理的接口超时是一个问题。 如果接口超时设置过长,可能会占用服务器过多的tcp连接。 而如果接口设置得太短,那么接口会经常超时。

还有一个问题就是服务端接口已经明显降低了rt,但是客户端仍然不断超时。 这个问题其实很简单。 从客户端到服务器的链路包括网络传输、排队、业务处理等。 每个环节都可能很耗时。

TCP队列溢出

tcp队列溢出是一种比较低级的错误,可能会导致、rst等更表面的错误。 因此,错误也比较微妙,所以我们单独说一下。

如上图所示,有两个队列:syns队列(半连接队列)和queue(全连接队列)。 三次握手,收到syn后,将消息放入syns队列中,回复syn+ack,并接收ack,如果此时队列未满,则从syns队列中取出临时信息并将其放入队列,否则按照w指示执行。

w 0 表示如果三次握手的第三步队列已满,则丢弃发送的ack。 w 1 表示如果第三步中满连接队列已满,则向其发送第一个数据包,这意味着握手过程和连接将被废除,这意味着在第三步中可能存在多次reset/reset by peer日志。

那么在实际开发中,如何快速定位tcp队列溢出呢?

命令,执行-s | egrep“|”

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

ss命令,执行ss -lnt

如上所示,第三列Send-Q表示该端口上的全连接队列最大数量为5,第一列Recv-Q表示当前使用了多少个全连接队列。

那么我们看看如何设置全连接和半连接队列大小:

全连接队列的大小取决于min(, )。 它是在创建时传入的,是操作系统级别的系统参数。 半连接队列的大小取决于 max(64, /proc/sys/net/ipv4/)。

在日常开发中,我们经常使用容器作为服务器,所以有时需要关注容器的连接队列的大小。 它被称为 in ,它在码头。

RST异常

RST包的意思是连接重置,用于关闭一些无用的连接。 通常表示异常关机,与四次挥手不同。

在实际开发中,我们经常会看到reset/reset by peer错误,这些错误都是由RST包引起的。

端口不存在

如果向一个不存在的端口发送建立连接的SYN请求,服务器发现自己没有这个端口,会直接返回RST报文来终止连接。

主动终止连接而不是FIN

一般来说,正常的连接关闭需要通过FIN报文来实现,但是我们也可以使用RST报文来代替FIN,表示直接终止连接。 实际开发中,可以设置一个值来控制,往往是有意的,跳过,以提高交互效率,不闲着的时候慎用。

客户端或服务端一侧发生异常,方向发送RST给另一端,通知关闭连接

上面我们提到的tcp队列溢出发送RST包其实就属于这一类。 这往往是由于某些原因,一方无法再正常处理请求连接(例如程序崩溃、队列已满),从而告诉另一方关闭连接。

收到的 TCP 数据包不在已知的 TCP 连接中

例如,如果一台机器由于网络状况不好而丢失了一条TCP报文,对方关闭了连接,过了很长一段时间才收到丢失的TCP报文,但由于对应的TCP连接已经不存在了,所以会直接发送一个RST 数据包打开一个新连接。

一方长时间没有收到对方的确认报文,在一定时间或重传次数后发送RST报文

其中大部分也与网络环境有关,网络环境不好可能会导致RST包较多。

之前说过RST包太多会导致程序报错。 对已关闭连接的读取操作将报告重置,对已关闭连接的写入操作将报告对等点的重置。 通常我们也可能会看到管道错误,这是管道级别的错误,表示关闭的管道读写,而且往往是收到RST后继续读写数据报报复位错误的错误。 这是glibc中的一个错误,在源码注释中也有介绍。

故障排除时如何判断是否存在RST包? 当然,使用命令抓包并用于简单分析。 -i en0 tcp -w xxx.cap,en0表示监控网卡。

接下来,打开抓到的包,我们或许可以看到下图,红色的就是RST包。

相信大家都知道是什么意思。在线时,我们可以直接使用命令 -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' 查看等待时间和数量

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

第一个的存在是为了让丢失的数据包能够被后续的连接复用,第二个是为了在2MSL的时间范围内正常关闭连接。 它的存在实际上会大大减少RST包的出现。

过多的情况更容易出现在短连接频繁的场景中。 这种情况下,可以在服务器端进行一些内核参数调整:

#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1

当然,我们不要忘记,在NAT环境下,数据包会因为错误的时间戳而被拒绝。 另一种方法是将数字更改为较小的值。 超过这个数量的会被杀死,但这也会导致时间等待表错误。

往往是因为应用程序写入有问题,ACK后没有再次发送FIN报文。 发生的概率甚至更高,后果也更严重。 往往是因为某个地方被阻塞,连接没有正常关闭,从而逐渐消耗掉所有的线程。

要定位此类问题,最好通过分析线程堆栈来排查问题。 详细内容请参考以上章节。 这里仅举一个例子。

开发同学表示,应用上线后,不断增加,直至挂掉。 找到比较可疑的堆栈后,大部分线程都卡在了.await方法中。 问开发同学了解后得知,使用了多线程但是没有catch异常。 修改后发现异常只是升级sdk后经常出现的最简单的类未找到。

 
反对 0举报 0 收藏 0 打赏 0评论 0
 
更多>同类资讯
推荐图文
推荐资讯
点击排行
网站首页  |  关于我们  |  联系方式  |  使用协议  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报
Powered By DESTOON