线上故障主要会包括cpu、磁盘、内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,因此进行排查时候尽可能四个方面依次排查一遍。同时例如jstack、jmap等工具也是不囿于一个方面的问题的,基本上出问题就是df、free、top 三连,而后依次jstack、jmap伺候,具体问题具体分析便可。
java
CPUios
通常来说咱们首先会排查cpu方面的问题。cpu异常每每仍是比较好定位的。缘由包括业务逻辑问题(死循环)、频繁gc以及上下文切换过多。而最多见的每每是业务逻辑(或者框架逻辑)致使的,可使用jstack来分析对应的堆栈状况。缓存
使用jstack分析cpu问题tomcat
咱们先用ps命令找到对应进程的pid(若是你有好几个目标进程,能够先用top看一下哪一个占用比较高)。网络
接着用top -H -p pid来找到cpu使用率比较高的一些线程多线程
而后将占用最高的pid转换为16进制printf '%x\n' pid获得nid并发
接着直接在jstack中找到相应的堆栈信息jstack pid |grep 'nid' -C5 –colorapp
能够看到咱们已经找到了nid为0x42的堆栈信息,接着只要仔细分析一番便可。框架
固然更常见的是咱们对整个jstack文件进行分析,一般咱们会比较关注WAITING和TIMED_WAITING的部分,BLOCKED就不用说了。咱们可使用命令cat jstack.log | grep "java.lang.Thread.State" | sort -nr | uniq -c来对jstack的状态有一个总体的把握,若是WAITING之类的特别多,那么多半是有问题啦。less
频繁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章节的描述。
上下文切换
针对频繁上下文问题,咱们可使用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掉的问题对象来定位问题。
网络
涉及到网络层面的问题通常都比较复杂,场景多,定位难,成为了大多数开发的噩梦,应该是最复杂的了。这里会举一些例子,并从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。