专栏名称: 石杉的架构笔记
专注原创、用心雕琢!十余年BAT一线大厂架构经验倾囊相授
目录
相关文章推荐
51好读  ›  专栏  ›  石杉的架构笔记

一整套线上故障排查技巧,爱了!

石杉的架构笔记  · 公众号  ·  · 2020-10-16 08:30

正文


我的新课 《C2C 电商系统微服务架构120天实战训练营》 在公众号 儒猿技术窝 上线了,感兴趣的同学,可以长按扫描下方二维码了解课程详情:

课程大纲请参见文末


本文来源:
https://fredal.xin/java-error-check

线上故障主要会包括 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 方面做进一步分析,具体可以参考一下 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 和 Stack Overflow。


①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 和 Stack Overflow 的代码排查方面,我们一般使用 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 手动触发 Full GC 来看看堆外内存有没有被回收。


如果被回收了,那么大概率是堆外内存本身分配的太小了,通过 -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 与 Full GC 是否过于频繁或者耗时过长,从而对症下药。


我们下面将对 G1 垃圾收集器来做分析,这边也建议大家使用 G1-XX:+UseG1GC。


①youngGC 过频繁


youngGC 频繁一般是短周期小对象较多,先考虑是不是 Eden 区/新生代设置的太小了,看能否通过调整 -Xmn、-XX:SurvivorRatio 等参数设置来解决问题。


如果参数正常,但是 youngGC 频率还是太高,就需要使用 Jmap 和 MAT 对 dump 文件进行进一步排查了。


②youngGC 耗时过长


耗时过长问题就要看 GC 日志里耗时耗在哪一块了。以 G1 日志为例,可以关注 Root Scanning、Object Copy、Ref Proc 等阶段。


Ref Proc 耗时长,就要注意引用相关的对象。Root Scanning 耗时长,就要注意线程数、跨代引用。


Object Copy 则需要关注对象生存周期。而且耗时分析它需要横向比较,就是和其他项目或者正常时间段的耗时比较。


比如说图中的 Root Scanning 和正常时间段比增长较多,那就是起的线程太多了。

触发 Full GC


G1 中更多的还是 mixedGC,但 mixedGC 可以和 youngGC 思路一样去排查。


触发 Full GC 了一般都会有问题,G1 会退化使用 Serial 收集器来完成垃圾的清理工作,暂停时长达到秒级别,可以说是半跪了。


FullGC 的原因可能包括以下这些,以及参数调整方面的一些思路:

  • 并发阶段失败: 在并发标记阶段,MixGC 之前老年代就被填满了,那么这时候 G1 就会放弃标记周期。

    这种情况,可能就需要增加堆大小,或者调整并发标记线程数 -XX:ConcGCThreads。

  • 晋升失败: 在 GC 的时候没有足够的内存供存活/晋升对象使用,所以触发了 Full GC。

    这时候可以通过 -XX:G1ReservePercent 来增加预留内存百分比,减少 -XX:InitiatingHeapOccupancyPercent 来提前启动标记,-XX:ConcGCThreads 来增加标记线程数也是可以的。

  • 大对象分配失败: 大对象找不到合适的 Region 空间进行分配,就会进行 Full GC,这种情况下可以增大内存或者增大 -XX:G1HeapRegionSize。

  • 程序主动执行 System.gc(): 不要随便写就对了。


另外,我们可以在启动参数中配置 -XX:HeapDumpPath=/xxx/dump.hprof 来 dump fullGC 相关的文件,并通过 jinfo 来进行 GC 前后的 dump:

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


这样得到两份 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。






请到「今天看啥」查看全文


推荐文章
冲蒌老伍  ·  如何老牛吃嫩草,笑到胃抽筋
8 年前
分布式实验室  ·  使用NGINX和NGINX Plus进行Docker Swarm负载均衡
8 年前
柴森物理  ·  柴森物理—初三春季招生简章
8 年前