前言
大部分的开发都没有接触到线上故障排查的经历, 本篇通过对生产事故的复盘做下记录分析。
公司是做IM机器人的。 某次, 售后反馈, 大量客户的机器人出现延迟对话的情况, 此时, 看业务日志发现业务未出现问题, 而数据入口处MQ无日志埋点, 无法判断是哪个环节出现了延迟。
现象
- load average偏高
- top查看gc进程占用了大量的cpu
- 内存free空间还比较多
- gc日志显示大量的
Full GC
理论
要排查问题, 首先需要了解各个指标的具体含义
-
load average
表示的是CPU的负载. 一般业界能够被接受的值是, load average <= CPU核数 *0.7。
-
Full GC
会导致 STW :top一the一World,简称STW,指的是Gc事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW
-
线程池
队列是一种消息堆积策略, 比如下边的线程池, 最大线程100, 队列长度10000, 在高并发下, queue可能被堆积满。假设每个任务的执行耗时为200ms, 则该场景下任务需要 10000/100*200ms=20000ms=20s
能够执行完。则 TPS=10000/20=500
。所以, 理论上单点最大并发500。
1
| new ThreadPoolExecutor(50, 100, 30L, TimeUnit.SECONDS, new ArrayBlockingQueue(10000), new AbortPolicy())
|
gc日志
所示, MinorGC
已无法回收内存, 只能触发 Full GC
。 出现这种情况说明: 1. 内存还在使用中, 无法释放(可能内存分配太小, 正常内存溢出; 可能有IO、三方调用等耗时操作, 任务执行效率低, 导致内存长时间占用); 2. 内存泄露
1 2 3 4 5
| [644572.936s][info][gc] GC(288066) To-space exhausted [644572.936s][info][gc] GC(288066) Pause Young (Concurrent Start) (G1 Evacuation Pause) 497M->497M(512M) 59.255ms [644572.936s][info][gc] GC(288068) Concurrent Cycle [644573.994s][info][gc] GC(288067) Pause Full (G1 Evacuation Pause) 497M->369M(512M) 1058.025ms [644573.995s][info][gc] GC(288068) Concurrent Cycle 1058.250ms
|
分析
通过上边的现象分析
- 任务延时, 说明阻塞率比较高, 可能是业务阻塞(业务日志可以排查耗时), 也可能是得到的cpu执行单元降低
- load average高, 说明无法再扩大线程池最大线程数
- GC Thread占用大量cpu, 需要排查gc日志, 查看gc日志可知, Full GC频繁, 说明需要分析内存(看哪些对象占用内存高)。同时 Full GC会阻塞业务线程(有效内存得不到释放), 且会造成线程池中Queue进一步堆积, 加剧内存的消耗, 降低线程执行效率。内存分析过程下边再详细阐述。到这里基本可以确认, 线上的512M内存需要扩容了。
内存分析模拟
导出线程快照: jstack -l pid > xxx.log
导出内存快照: jmap -dump:live,format=b,file=/data/xxxx.hprof pid
1
| live表示只dump活跃对象,format=b表示二进制格式,file表示保存文件的路径,pid表示进程号。
|
详细的JVM在线指令可以参考 https://idcent.cn/#/note/JVM调优指令
- 查看我们应用的pid
- 利用top命令找到CPU占用高的进程pid
显示pid: 381179
1
| 381179 root 20 0 6222740 996436 9272 S 0.7 12.5 133:34.36 java
|
- 利用top找到该pid 381179下所有的线程数据
可以看到占用cpu资源最高的为385444
1
| 385444 root 20 0 6222740 996436 9272 S 0.3 12.5 22:06.56 dubbo-future-ti
|
- 占用率最高的线程 ID 为385444,将其转换为 16 进制形式 (因为 java native 线程以 16 进制形式输出)
得到线程ID: 5e1a4
- 利用 jstack 打印出 java 线程调用栈信息
1 2 3 4 5 6
| # 1. 在线查询 jstack 381179 | grep '5e1a4' -A 50 --color
# 2. 导出线程快照查询 jstack -l 381179 > ./381179.stack cat 381179.stack | grep '5e1a4' -C 50
|
模拟故障
- 堆内存溢出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package com.example.demo;
import java.util.ArrayList; import java.util.List;
public class HeapOOm {
public static void main(String[] args) throws InterruptedException { List<byte[]> list = new ArrayList<>(); int i=0; while(true){ list.add(new byte[5*1024*1024]); System.out.println("分配次数:"+(++i)); } } }
|
- 栈内存溢出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package com.example.demo;
public class StackOverflowErrorDemo {
int depth = 0;
public void sofMethod(){ depth ++ ; sofMethod(); }
public static void main(String[] args) { StackOverflowErrorDemo test = null; try { test = new StackOverflowErrorDemo(); test.sofMethod(); } finally { System.out.println("递归次数:"+test.depth); } } }
|
- 永久代-方法区溢出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package com.example.demo;
import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class MethodAreaOOMTest {
public static void main(String[] args) { int i=0; try { while(true){ Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); i++; } } finally{ System.out.println("运行次数:"+i); } }
static class OOMObject{
} }
|
- 元空间溢出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package com.example.demo;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class DirectMemoryOOMTest {
public static void main(String[] args) { int i=0; try { Field field = Unsafe.class.getDeclaredFields()[0]; field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); while(true){ unsafe.allocateMemory(1024*1024); i++; } } catch (Exception e) { e.printStackTrace(); }finally { System.out.println("分配次数:"+i); } } }
|
- Young GC 模拟
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package com.example.demo;
import java.util.ArrayList; import java.util.List;
public class TestYGC {
public static void main(String[] args) { List<Object> list = new ArrayList<>();
for (int i = 0; i < 4; i++) { byte[] b = new byte[1024 * 1024]; list.add(b); } } }
|
- Full GC 模拟
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package com.example.demo;
import java.util.ArrayList; import java.util.List;
public class TestFullGC {
public static void main(String[] args) { List<Object> list = new ArrayList<>();
for (int i = 0; i < 8; i++) { byte[] b = new byte[1024 * 1024]; list.add(b); } } }
|
哪些对象触发了Full GC, 可以通过如下查看
1
| jmap -histo:live pid | head -10
|
参考 http://static.kancloud.cn/alex_wsc/javajvm/1844985
- 死锁模拟
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package com.javaedge.concurrency.example.deadLock;
public class DeadLockDemo { private static Object resource1 = new Object(); private static Object resource2 = new Object();
public static void main(String[] args) { new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "线程 1").start();
new Thread(() -> { synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource1"); synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); } } }, "线程 2").start(); } }
|
最后
简单总结下: CPU看线程, 内存看GC
本文到此结束,感谢阅读。如果您觉得不错,请关注公众号【当我遇上你】,您的支持是我写作的最大动力。