线上故障次要会包含cpu、磁盘、内存以及网络问题,而大多数故障可能会蕴含不止一个层面的问题,所以进行排查时候尽量四个方面顺次排查一遍。

同时例如jstack、jmap等工具也是不囿于一个方面的问题的,基本上出问题就是df、free、top 三连,而后顺次jstack、jmap服侍,具体问题具体分析即可。

CPU

一般来讲咱们首先会排查cpu方面的问题。cpu异样往往还是比拟好定位的。起因包含业务逻辑问题(死循环)、频繁gc以及上下文切换过多。而最常见的往往是业务逻辑(或者框架逻辑)导致的,能够应用jstack来剖析对应的堆栈状况。

应用jstack剖析cpu问题

咱们先用ps命令找到对应过程的pid(如果你有好几个指标过程,能够先用top看一下哪个占用比拟高)。

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

而后将占用最高的pid转换为16进制printf '%x\n' pid失去nid

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

能够看到咱们曾经找到了nid为0x42的堆栈信息,接着只有仔细分析一番即可。

当然更常见的是咱们对整个jstack文件进行剖析,通常咱们会比拟关注WAITING和TIMED_WAITING的局部,BLOCKED就不用说了。咱们能够应用命令cat jstack.log | grep "java.lang.Thread.State" | sort -nr | uniq -c来对jstack的状态有一个整体的把握,如果WAITING之类的特地多,那么多半是有问题啦。

频繁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方面做进一步剖析。

上下文切换

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

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

如果咱们心愿对特定的pid进行监控那么能够应用 pidstat -w pid命令,cswch和nvcswch示意被迫及非被迫切换。

磁盘

磁盘问题和cpu一样是属于比拟根底的。首先是磁盘空间方面,咱们间接应用df -hl来查看文件系统状态

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

最初一列%util能够看到每块磁盘写入的水平,而rrqpm/s以及wrqm/s别离示意读写速度,个别就能帮忙定位到具体哪块磁盘呈现问题了。

另外咱们还须要晓得是哪个过程在进行读写,一般来说开发本人心里有数,或者用iotop命令来进行定位文件读写的起源。

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

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

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

内存

内存问题排查起来绝对比CPU麻烦一些,场景也比拟多。次要包含OOM、GC问题和堆外内存。一般来讲,咱们会先用free命令先来查看一发内存的各种状况。

堆内内存

内存问题大多还都是堆内内存问题。表象上次要分为OOM和StackOverflow。

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

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和StackOverflow的代码排查方面,咱们个别应用JMAPjmap -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级别的话还会有具体内存段的增长状况,如下图。

此外在零碎层面,咱们还能够应用strace命令来监控内存调配 strace -f -e "brk,mmap,munmap" -p pid

这边内存调配信息次要包含了pid和内存地址。

不过其实下面那些操作也很难定位到具体的问题点,要害还是要看谬误日志栈,找到可疑的对象,搞清楚它的回收机制,而后去剖析对应的对象。比方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和失常时间段比增长较多,那就是起的线程太多了。

触发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

这样失去2份dump文件,比照后次要关注被gc掉的问题对象来定位问题。

搜寻Java知音,回复“后端面试”,送你一份面试宝典.pdf

网络

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

超时

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

  • 读写超时。readTimeout/writeTimeout,有些框架叫做so_timeout或者socketTimeout,均指的是数据读写超时。留神这边的超时大部分是指逻辑上的超时。soa的超时指的也是读超时。读写超时个别都只针对客户端设置。
  • 连贯超时。connectionTimeout,客户端通常指与服务端建设连贯的最大工夫。服务端这边connectionTimeout就有些形形色色了,jetty中示意闲暇连贯清理工夫,tomcat则示意连贯维持的最大工夫。
  • 其余。包含连贯获取超时connectionAcquireTimeout和闲暇连贯清理超时idleConnectionTimeout。多用于应用连接池或队列的客户端或服务端框架。

咱们在设置各种超时工夫中,须要确认的是尽量放弃客户端的超时小于服务端的超时,以保障连贯失常完结。

在理论开发中,咱们关怀最多的应该是接口的读写超时了。

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

服务端接口明明rt升高,但客户端依然始终超时又是另一个问题。这个问题其实很简略,客户端到服务端的链路包含网络传输、排队以及服务解决等,每一个环节都可能是耗时的起因。

TCP队列溢出

tcp队列溢出是个绝对底层的谬误,它可能会造成超时、rst等更表层的谬误。因而谬误也更荫蔽,所以咱们独自说一说。

如上图所示,这里有两个队列: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"

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

ss命令,执行ss -lnt

下面看到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示意监听的网卡。

接下来咱们通过wireshark关上抓到的包,可能就能看到如下图所示,红色的就示意RST包了。

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

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

当然咱们不要遗记在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。