线上JAVA进程CPU高处理过程

Posted by mingo on 2018-12-09 14:50

前几日,收到线程服务器CPU超过50%的报警, 赶紧秒开VPN登上服务器

top

首先通过top命令, 查看服务器的各项指标, 比如CPU使用情况, 内存使用情况, 服务器的负载;

top

由于当时情况紧急,没空截图, 当时的情况是负载在7~10之间, CPU使用率在400~600%, 内存率正常

找到进程ID, 比如3233, 再找到进程内负载比较高的线程ID

top -Hp 3233

top

可以看出CPU占用高的线程ID, 此处是20981, 20974, 20975, 20976, 20977, 20978, 20979, 20980

1
2
3
4
5
6
7
8
9
10
  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                                                                                  
20981 tomcat    20   0 14.139g 8.988g  14200 R 99.9 28.8  79:53.47 java                                                                                     
20974 tomcat    20   0 14.139g 8.988g  14200 R 99.9 28.8  79:56.24 java                                                                                     
20975 tomcat    20   0 14.139g 8.988g  14200 R 99.9 28.8  79:53.03 java                                                                                     
20976 tomcat    20   0 14.139g 8.988g  14200 R 99.9 28.8  79:53.09 java                                                                                     
20977 tomcat    20   0 14.139g 8.988g  14200 S 99.9 28.8  79:54.50 java                                                                                     
20978 tomcat    20   0 14.139g 8.988g  14200 R 99.9 28.8  79:53.72 java                                                                                     
20979 tomcat    20   0 14.139g 8.988g  14200 R 99.9 28.8  79:53.73 java                                                                                     
20980 tomcat    20   0 14.139g 8.988g  14200 R 99.9 28.8  79:53.85 java                                                                                     
20946 tomcat    20   0 14.139g 8.988g  14200 S  0.0 28.8   0:00.00 java  

jstack

CPU高, 一般是有进程在进行密集计算, 比如无限循环, 频繁进行JSON序列化反序列化, 或者图形计算;

这些情况都是不正常的, 99.99999%都是我们的代码有问题, 那我们怎么定位到问题代码呢

我们自然的想法是如果我们能获取此时是哪个线程, 最好能具体到哪个类, 哪个方法, 这样排查的范围就可以大大缩小了

jstack命令就能把java进程内部的线程信息以调用栈的形式dump出来

jstack 3233 > /tmp/3233.log

jstack

从中我们可以看出此时java进程里的所有线程, 每个线程的状态, 线程ID(截图里的nid)

此处有一点小小问题, jstack里线程ID是16进制, 而top显示的是10进制, 我们需要把top的线程ID转成16进制; 转换方法有很多, 我喜欢用python的print方法

假设3233内的有问题线程ID是30417

python print

然后我们在3233.log文件里搜索76d1一下, 幸运的话能看到具体的类和方法名了

Unable to open socket file: target process not responding or HotSpot VM not loaded

在实际环境中, 为了保证服务器安全, 服务器上的每个进程或实例都是用单独的用户权限管理, 比如nginx就是用nginx组的nginx用户启动停止, tomcat就是用tomcat组的tomcat用户管理 而我们开发登录线上服务器一般是以自己名字命令的用户, 这个时候, 如果我们直接使用 jstack {tomcat_pid}, 会提示

1
2
{tomcat_pid}: Unable to open socket file: target process not responding or HotSpot VM not loaded
The -F option can be used when the target process is not responding

如果这个时候, 我们按提示,简单粗暴的使用

jstack -F {tomcat_pid}

也能导出数据, 但是导出的线程信息里是没打印nid信息的, 这样还是不方便我们定位问题

解决方法有2个:

  • 使用进程用户来执行jstack;

    而一般像nginx, tomcat这样的用户都是没有分配登录权限的, 也就是没有分配shell, 我们还需要去修改/etc/passwd里的配置;

    并且切换帐户也需要权限, 所以最好还是以下面的方式

  • 使用超级管理员权限

su - tomcat -c “jstack {tomcat_pid} > /tmp/{tomcat_pid}.log”

sudo -u tomcat jstack {tomcat_pid} > /tmp/{tomcat_pid}.log

每一步都不舒坦啊, 搞程序的就是这样的, 遇到困难不要慌, 这是学习的好机会

纸上得来终觉浅, 绝知此事需躬行

jstat

当然我就没那么幸运了, 我的线程ID在jstack文件里找到的是GC进程的ID, 也就是说JVM一直在执行GC导致CPU占用高, 那怎么查看当前进程的gc情况呢

-gc

jstat -gc 工具可以打印出JVM里堆空间各个区分配的内存大小, 已使用的内存大小, GC的次数, 累计耗时信息

我们通过各个区的大小和增长速度, 大致可以判断出是有内存泄漏还是区分配大小是否合理

1
jstat -gc {pid} {interval}

jstat-gc

各列含义说明:

  • S0C: Survivor0区的容量 #C是Capacity, 容量的意思, 单位:字节, 下同
  • S1C: Survivor1区的容量
  • S0U: Survivor0区的使用量 # U是used, 使用量的意思, 单位:字节, 下同
  • S1U: Survivor1区的使用量
  • EC: Eden区的容量
  • EU: Eden区的使用量
  • OC: Old区的容量
  • OU: Old区的使用量
  • MC: Method区的容量
  • MU: Method区的使用量
  • CCSC: 类区的容量
  • CCSU: 类区的使用量
  • YGC: 从进程启动到现在累计Young GC次数; 如果两次打印之间, 数字增加了, 表明刚刚发生了一次YGC
  • YGCT: 从进程启动到现在累计Yong GC耗时, 单位:秒
  • FGC: 从进程启动到现在累计Full GC次数; 如果两次打印之间, 数字增加了, 表明刚刚发生了一次FGC
  • FGCT: 从进程启动到现在累计Full GC耗时
  • GCT: 从进程启动到现在累计总GC耗时

-gcutil

-gc显示的每个区的大小, -gcutil可以看到每个区的使用比例, 可以从另外一个维度来看每个区的使用情况

1
jstat -gcutil {pid} {interval}

jstat-gcutil

各列含义说明:

  • S0: Survivor0区使用率
  • S1: Survivor1区使用率, Survivor0区与Survivor1区总有一个是空的
  • E: Eden区使用率
  • O: Old区使用率
  • M: Method区使用率
  • CCS: 类区使用率
  • YGC: 从进程启动到现在累计Young GC次数; 如果两次打印之间, 数字增加了, 表明刚刚发生了一次YGC
  • YGCT: 从进程启动到现在累计Yong GC耗时, 单位:秒
  • FGC: 从进程启动到现在累计Full GC次数; 如果两次打印之间, 数字增加了, 表明刚刚发生了一次FGC
  • FGCT: 从进程启动到现在累计Full GC耗时
  • GCT: 从进程启动到现在累计总GC耗时

-gccause

gccause可以查看最近2次gc发生的原因

1
jstat -gccause {pid} {interval}

jstat-gccause

各列含义说明(S0,S1,E,O,M,CCS,YGC,YGCT,FGC,FGCT,GCT跟gcutil的含义相同):

  • LGCC: 上一次gc发生的原因
  • GCC: 本次gc发生的原因

一般gc原因的解释, 参考:

  • No GC: 没有发生GC
  • _java_lang_system_gc: 通过代码显示调用System.gc()触发; 还有一种情况是,在visualvm等软件上通过JMX监控时有点击触发SystemGC的按钮
  • _jvmti_force_gc: JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口; 通过jvmti方式触发的GC
  • _gc_locker: 当最后一个位于jni临界区内的线程退出临界区时,发起一次CGCause为_gc_locker的GC
  • _heap_inspection: 这个类型主要是jmap -hisot:live命令时会触发
  • _heap_dump:
  • _wb_young_gc:
  • _allocation_failure: 这个就是常见的内存分配失败触发的GC。比如在new 对象时
  • _metadata_GC_threshold: 这个用于在metaspace区域分配时分配不下,从而触发的GC
  • _cms_initial_mark, _cms_final_remark: 这2个就是对于设置的CMS回收器时,有一个background式的回收时的初始标记和最终标记阶段
  • _cms_concurrent_mark: cms的background式GC
  • _adaptive_size_policy: 这个在ps中动态调整堆以及各个区大小时用到
  • _g1_inc_collection_pause: 这个是设置的G1回收器时,若分配不下触发的GC的cause
  • _g1_humongous_allocation: 这个用于分配超大对象失败时触发GC
  • _last_ditch_collection: 这个也是用于在metaspace区域分配不下时,最后的一次回收

FGC会Stop-the-World, 如果频繁的发生FGC会让大部分的线程都处于WAIT状态, 时间一长整个服务器的负载开始上升, 整个Java进程的TPS下降, 而外部的请求数却没有减少并响应时长开始增加甚至出现超时现象

而我当时看到的FGC每2秒就执行一次, 而且FGC之后, Old区的使用率并没有下降多少, gc的原因都是Allocation Failure

一般频繁发生FGC要么堆空间配置不合理, 要么代码有内存有泄漏, old区被打满且FGC后, 内存无法释放

首先怀疑代码有无内存泄漏, 现在只能出最后的大招了, 把java内存快照dump出来分析分析

jmap

1
jmap -dump:format=b,file=filname {pid}

结果dump出来的文件足足有11个G, 压缩后也有2G, 下载速度300+K/s, 需要1个半小时, 头疼!!!

没办法, 吹出去的牛跪着也要实现, 慢慢下载吧, 期间网络中断2次, 花了5,6个小时终于download了下来

jhat, jvisualvm, jprofiler

你以为这就完了吗? 打开一看就知道问题在哪里了吗? No

因为dump文件太大, 我用了jhat, jvisualvm都多次把我本机打成了OOM; 我只能把我本机的16G内存分配14个G给jvisualvm, 等了半个小时才打开

jvisualvm -J-xmx14g

你以为这就完了吗? no, 每个按钮点一下又要等半小时, 实在受不了, 求教同行, 得知有个神器叫jprofiler, 赶紧下载之, 又是贼慢; 但好歹是唯一的希望, 只要让我看到dump里的对象是啥, 我就能定位这个内存泄漏的原因

好不容易等到jprofiler下载完成, 又过了半小时终于打开了dump文件, 现在的问题只剩下怎么从打开的面板里分析出哪些对象是异常的

你以为打开之后, 就一目了然了看出哪里内存泄漏了吗? too young ~~

一般是从对象大小, 对象个数维度来分析;

  • 如果有个对象是自定义类, 且数量很多, 应该是重点分析目标;
  • 如果有Map对象很大, 也是重点考查目标
  • char[], String很多, 就比较麻烦了, 因为这个基础类型, 数量多也可能是正常的情况, 就只能仔细分析了

最后, 即便我们有这么多工具, 还是未必能找到BUG, 现实就是这么令人沮丧

唯有努力学习, 夯实基本功, 才能在下一次与BUG的交手中, 一击必杀, God Luck!

参考: