点击上方“
朱小厮的博客
”,选择“
设为星标
”
做积极的人,而不是积极废人
来源:http://t.cn/EI9JdBu
处理过线上问题的同学基本上都会遇到系统突然运行缓慢,CPU 100%,以及Full GC次数过多的问题。当然,这些问题的最终导致的直观现象就是系统运行缓慢,并且有大量的报警。本文主要针对系统运行缓慢这一问题,提供该问题的排查思路,从而定位出问题的代码点,进而提供解决该问题的思路。
对于线上系统突然产生的运行缓慢问题,如果该问题导致线上系统不可用,那么首先需要做的就是,导出jstack和内存信息,然后重启系统,尽快保证系统的可用性。这种情况可能的原因主要有两种:
相对来说,这是出现频率最高的两种线上问题,而且它们会直接导致系统不可用。另外有几种情况也会导致某个功能运行缓慢,但是不至于导致系统不可用:
代码某个位置有阻塞性的操作,导致该功能调用整体比较耗时,但出现是比较随机的;
某个线程由于某种原因而进入WAITING状态,此时该功能整体不可用,但是无法复现;
由于锁使用不当,导致多个线程进入死锁状态,从而导致系统整体比较缓慢。
对于这三种情况,通过查看CPU和系统内存情况是无法查看出具体问题的,因为它们相对来说都是具有一定阻塞性操作,CPU和系统内存使用情况都不高,但是功能却很慢。下面我们就通过查看系统日志来一步一步甄别上述几种问题。
1. Full GC次数过多
相对来说,这种情况是最容易出现的,尤其是新功能上线时。对于Full GC较多的情况,其主要有如下两个特征:
首先我们可以使用
top
命令查看系统CPU的占用情况,如下是系统CPU较高的一个示例:
top - 08:31:10 up 30 min , 0 users , load average : 0.73 , 0.58 , 0.34 KiB Mem : 2046460 total , 1923864 used , 122596 free , 14388 buffers KiB Swap : 1048572 total , 0 used , 1048572 free . 1192352 cached Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME
+ COMMAND 9 root 20 0 2557160 288976 15812 S 98.0 14.1 0:42.60 java
可以看到,有一个Java程序此时CPU占用量达到了98.8%,此时我们可以复制该进程id
9
,并且使用如下命令查看呢该进程的各个线程运行情况:
top -Hp 9
该进程下的各个线程运行情况如下:
top - 08:31:16 up 30 min , 0 users , load average : 0.75 , 0.59 , 0.35 Threads : 11 total , 1 running , 10 sleeping , 0 stopped , 0 zombie %Cpu (s ): 3.5 us , 0.6 sy , 0.0 ni , 95.9 id , 0.0 wa , 0.0 hi , 0.0 si , 0.0 st KiB Mem : 2046460 total , 1924856 used , 121604 free , 14396 buffers KiB Swap : 1048572 total , 0 used , 1048572 free . 1192532 cached Mem PID
USER PR NI VIRT RES SHR S %CPU %MEM TIME + COMMAND 10 root 20 0 2557160 289824 15872 R 79.3 14.2 0:41.49 java 11 root 20 0 2557160 289824 15872 S 13.2 14.2 0:06.78 java
可以看到,在进程为
9
的Java程序中各个线程的CPU占用情况,接下来我们可以通过jstack命令查看线程id为
10
的线程为什么耗费CPU最高。需要注意的是,在jsatck命令展示的结果中,线程id都转换成了十六进制形式。可以用如下命令查看转换结果,也可以找一个科学计算器进行转换:
root@a39de7e7934b:/ a
这里打印结果说明该线程在jstack中的展现形式为
0xa
,通过jstack命令我们可以看到如下信息:
"main" #1 prio=5 os_prio=0 tid=0x00007f 8718009800 nid=0xb runnable [0x00007f871f e41000] java.lang.Thread.State: RUNNABLE at com.aibaobei.chapter2.eg2.UserDemo.main(UserDemo.java:9 ) "VM Thread" os_prio=0 tid=0x00007f 871806e000 nid=0xa runnable
这里的VM Thread一行的最后显示
nid=0xa
,这里nid的意思就是操作系统线程id的意思。而VM Thread指的就是垃圾回收的线程。这里我们基本上可以确定,当前系统缓慢的原因主要是垃圾回收过于频繁,导致GC停顿时间较长。我们通过如下命令可以查看GC的情况:
root@8d36124607a0:/ S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0 .00 0 .00 0 .00 75.07 59.09
59.60 3259 0 .919 6517 7.715 8.635 0 .00 0 .00 0 .00 0 .08 59.09 59.60 3306 0 .930 6611 7.822 8.752 0 .00 0 .00 0 .00 0 .08 59.09 59.60 3351 0 .943 6701 7.924 8.867 0 .00 0 .00 0 .00 0 .08 59.09 59.60 3397 0 .955 6793 8.029 8.984
可以看到,这里FGC指的是Full GC数量,这里高达6793,而且还在不断增长。从而进一步证实了是由于内存溢出导致的系统缓慢。那么这里确认了内存溢出,但是如何查看你是哪些对象导致的内存溢出呢,这个可以dump出内存日志,然后通过eclipse的mat工具进行查看,如下是其展示的一个对象树结构:
经过mat工具分析之后,我们基本上就能确定内存中主要是哪个对象比较消耗内存,然后找到该对象的创建位置,进行处理即可。这里的主要是PrintStream最多,但是我们也可以看到,其内存消耗量只有12.2%。也就是说,其还不足以导致大量的Full GC,此时我们需要考虑另外一种情况,就是代码或者第三方依赖的包中有显示的
System.gc()
调用。这种情况我们查看dump内存得到的文件即可判断,因为其会打印GC原因:
[Full GC (System.gc()) [Tenured: 262546 K->262546 K(349568 K), 0.0014879 secs] 262546 K->262546 K(506816 K), [Metaspace: 3109 K->3109 K(1056768 K)], 0.0015151 secs] [Times: user=0.00 sys=0.00
, real=0.01 secs] [GC (Allocation Failure) [DefNew: 2795 K->0 K(157248 K), 0.0001504 secs][Tenured: 262546 K->402 K(349568 K), 0.0012949 secs] 265342 K->402 K(506816 K), [Metaspace: 3109 K->3109 K(1056768 K)], 0.0014699 secs] [Times: user=0.00
比如这里第一次GC是由于
System.gc()
的显示调用导致的,而第二次GC则是JVM主动发起的。总结来说,对于Full GC次数过多,主要有以下两种原因:
2. CPU过高
在前面第一点中,我们讲到,CPU过高可能是系统频繁的进行Full GC,导致系统缓慢。而我们平常也肯能遇到比较耗时的计算,导致CPU过高的情况,此时查看方式其实与上面的非常类似。首先我们通过
top
命令查看当前CPU消耗过高的进程是哪个,从而得到进程id;然后通过
top -Hp
来查看该进程中有哪些线程CPU过高,一般超过80%就是比较高的,80%左右是合理情况。这样我们就能得到CPU消耗比较高的线程id。接着通过该
线程id的十六进制表示
在
jstack
日志中查看当前线程具体的堆栈信息。
在这里我们就可以区分导致CPU过高的原因具体是Full GC次数过多还是代码中有比较耗时的计算了。如果是Full GC次数过多,那么通过
jstack
得到的线程信息会是类似于VM Thread之类的线程,而如果是代码中有比较耗时的计算,那么我们得到的就是一个线程的具体堆栈信息。如下是一个代码中有比较耗时的计算,导致CPU过高的线程信息:
这里可以看到,在请求UserController的时候,由于该Controller进行了一个比较耗时的调用,导致该线程的CPU一直处于100%。我们可以根据堆栈信息,直接定位到UserController的34行,查看代码中具体是什么原因导致计算量如此之高。
3. 不定期出现的接口耗时现象
对于这种情况,比较典型的例子就是,我们某个接口访问经常需要2~3s才能返回。这是比较麻烦的一种情况,因为一般来说,其消耗的CPU不多,而且占用的内存也不高,也就是说,我们通过上述两种方式进行排查是无法解决这种问题的。而且由于这样的接口耗时比较大的问题是不定时出现的,这就导致了我们在通过
jstack
命令即使得到了线程访问的堆栈信息,我们也没法判断具体哪个线程是正在执行比较耗时操作的线程。
对于不定时出现的接口耗时比较严重的问题,我们的定位思路基本如下:首先找到该接口,通过压测工具不断加大访问力度,如果说该接口中有某个位置是比较耗时的,由于我们的访问的频率非常高,那么大多数的线程最终都将阻塞于该阻塞点,这样通过多个线程具有相同的堆栈日志,我们基本上就可以定位到该接口中比较耗时的代码的位置。如下是一个代码中有比较耗时的阻塞操作通过压测工具得到的线程堆栈日志:
"http-nio-8080-exec-2" #29 daemon prio=5 os_prio=31 tid=0x00007fd 08cb26000 nid=0x9603 waiting on condition [0x00007000031d 5000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340 ) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386 ) at com.aibaobei.user.controller.UserController.detail(UserController.java:18 ) "http-nio-8080-exec-3" #30 daemon prio=5 os_prio=31 tid=0x00007fd 08cb27000 nid=0x6203 waiting on condition [0x00007000032d 8000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340 ) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386 ) at com.aibaobei.user.controller.UserController.detail(UserController.java:18 ) "http-nio-8080-exec-4" #31 daemon prio=5 os_prio=31 tid=0x00007fd08d0f a000 nid=0x6403 waiting on condition [0x00007000033d b000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340 ) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386 ) at com.aibaobei.user.controller.UserController.detail(UserController.java:18 )
从上面的日志可以看你出,这里有多个线程都阻塞在了UserController的第18行,说明这是一个阻塞点,也就是导致该接口比较缓慢的原因。
4. 某个线程进入WAITING状态
对于这种情况,这是比较罕见的一种情况,但是也是有可能出现的,而且由于其具有一定的“不可复现性”,因而我们在排查的时候是非常难以发现的。笔者曾经就遇到过类似的这种情况,具体的场景是,在使用CountDownLatch时,由于需要每一个并行的任务都执行完成之后才会唤醒主线程往下执行。而当时我们是通过CountDownLatch控制多个线程连接并导出用户的gmail邮箱数据,这其中有一个线程连接上了用户邮箱,但是连接被服务器挂起了,导致该线程一直在等待服务器的响应。最终导致我们的主线程和其余几个线程都处于WAITING状态。
对于这样的问题,查看过jstack日志的读者应该都知道,正常情况下,线上大多数线程都是处于
TIMED_WAITING
状态,而我们这里出问题的线程所处的状态与其是一模一样的,这就非常容易混淆我们的判断。解决这个问题的思路主要如下: