服务端性能问题排查及优化---CPU高问题分析
CPU高是常见的性能问题,但CPU高并不一定都是有问题,有可能你的业务就是CPU密集型的业务。
如果CPU资源有限的情况下,本文可以提供一下优化CPU使用的方法,可以尽可能的优化,使CPU在可能的范围内降到最低。
可能造成CPU高的情况 :
- 代码Bug
- 意外的死循环
- 线程数太多,频繁线程切换
- 频繁的FGC
- 不正常的使用某些类
可能导致CPU高的情况
- 代码Bug
可能性太多… - 意外的死循环
手抖写的死循环或者计算失误导致死循环 - 线程数太多(线程数量是否比预期的异常的高)
大量的线程切换:线程数太多可能会导致频繁的线程上下文的切换,浪费CPU资源。
频繁创建销毁线程:线程的创建和销毁对系统的资源消耗比较大,如果一直在频繁的创建销毁临时线程导致资源占用,可能需要考虑下是否有其他更好的方案了。
线程池不正常的使用:比如Cache线程池初始化比较少、最小和最大的数量相差比较大、业务并发突然增大可能导致同时去创建线程。 - 频繁的FGC
导致频繁的FGC也会导致CPU高,比如由于JVM参数设置的不合理,年轻代和老年代的比例比例不合理导致频繁的FGC等。 - 不正常的使用某些类
比如Map的初始大小给的太小,而后续的使用中存的东西太多,可能会频繁的resize。
比如大数据量List的频繁查找,clone等。
比如可重复利用资源的频繁初始化操作。
分析过程
用到的工具和命令
jvisualvm
jstack,jstat,jmap,ps,top
CPU高的集中情况
-
情况一:单核CPU使用率一直100%
大多情况为死循环或者某个线程一直在执行大量运算操作 -
情况二:单核CPU使用率不定时100%
应用本身有周期性的任务
非人为的周期性操作,比如FGC,大量数据的copy(list,map) -
情况三:每核CPU使用率都高
需要结合堆栈、代码、资源占用、内存情况等,全面的考虑和分析。
分析方法
-
方法1
Java Visualvm直接连接进程分析,能够比较直观的看到每个方法所用的CPU时间,更容易定位问题。大多数情况下生产环境无法使用。适用于被测对象问题能够复现,且配置的远程rmi监控的情况。生产环境不会配置远程监控。java服务启动时需配置启动参数:
-Dcom.sun.management.jmxremote.port=8999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=192.168.143.136使用方法见以下演示。
-
方法2
在CPU高的时候抓线程堆栈,适用于CPU相对比较高的情况,如果条件允许可以把服务压力加大到尽量大,使CPU尽量高后去分析问题。抓取线程执行堆栈
jstack pid > /tmp/jstack.txt
找到占用cpu时间比较多的线程id
ps -mp pid -o THREAD,tid,time | awk '{printf $2" "$8"\n" }' | sort
把线程id转换为10进制,8进制?
printf "%x\n" tid
输出线程堆栈
jstack pid | grep –A 20 tid
连续抓取几次堆栈信息,分析正在运行的线程,分析当前操作是否耗费CPU,这么多运行的是否正常,业务堆栈是否正常,该业务可能出现的问题等等。
案例分析
案例介绍
测试时发现使用的的测试客户端CPU高,表现为运行一段时间后,CPU突然100%,出现该情况后不能恢复正常。
分析过程
- 首先抓取客户端的线程堆栈,看看能否发现什么可以的地方。
- 分析堆栈发现线程有1000多,大部分为BLOCKED状态,ACTIVE状态基本看到的都是nio的,暂时没看到问题。
- 继续搜索本地package的以下名称,看看有没有在执行自己代码的地方,正好发现一些类似以下的信息。
Thread 1134: (state = IN_NATIVE)
- java.net.NetworkInterface.getAll() @bci=0 (Compiled frame; information may be imprecise)
- java.net.NetworkInterface.getNetworkInterfaces() @bci=0, line=334 (Compiled frame)
- com.alibaba.rocketmq.remoting.common.RemotingUtil.getLocalAddress() @bci=0, line=112 (Compiled frame)
- com.alibaba.rocketmq.client.ClientConfig.<init>() @bci=19, line=32 (Compiled frame)
- com.alibaba.rocketmq.client.producer.DefaultMQProducer.<init>(java.lang.String, com.alibaba.rocketmq.remoting.RPCHook) @bci=1, line=95 (Compiled frame)
- com.alibaba.rocketmq.client.producer.DefaultMQProducer.<init>(java.lang.String) @bci=3, line=86 (Compiled frame)
- ********************MQProducer.<init>(java.lang.String, java.lang.String) @bci=71, line=62 (Compiled frame)
- ********************.RocketMQ.sendMessage() @bci=76,line=119 (Compiled frame) // 119为源代码行号
- ********************.RocketMQ$1$1.safeRun() @bci=7, line=53 (Compiled frame)
- ********************.SafeRunnable.run() @bci=1, line=13 (Compiled frame)
- java.util.concurrent.ThreadPoolExecutor.runWorker(java.util.concurrent.ThreadPoolExecutor$Worker) @bci=95, line=1145 (Compiled frame)
- java.util.concurrent.ThreadPoolExecutor$Worker.run() @bci=5, line=615 (Interpreted frame)
- java.lang.Thread.run() @bci=11, line=745 (Interpreted frame)
发现这些线程都在执行java.net.NetworkInterface.getAll() ,此方法比较耗费CPU,之前遇到过类似案例。接着分析为什么这几个线程会卡到这。
- 根据代码分析都在执行这个操作的原因。
public void sendMessage() {
try {
// 略…
Message msg = new Message("Performace", msgContent.getBytes("UTF-8"));
if (producer == null) {
producer = new MQProducer("Performace", "192.168.143.135:9876"); (1)
producer.start();
rst = producer.product(msg);
} else {
rst = producer.product(msg);
}
// 略…
} catch (Exception e) {
if (producer != null) {
producer.shutdown();
producer = null;
}
}
}
sendMessage方法会被随机的注册到一个timer线程池上,有可能会在同一时间点或者很近时间点同时执行该方法。
producer.product(msg);为给远端发送信息,如果因为网络原因或者其他未知原因导致Exception,会把producer赋值为null,当再次执行sendMessage会重新初始化producer,如果恰好有多线程并发执行sendMessage,可能会导致重复初始化以及其他并发问题,导致恶性循环,恰好这个过程对CPU消耗比较多。
最后
以上是一个工作上简单案例的分析过程,实际工作中遇到的问题可能会复杂的多,过程可能会更曲折,需要从更多的方面去了解被测对象,甚至需要比开发自己更了解整个系统的架构,才能从多个方面去考虑问题,查找问题的真正原因。
本文由郭军英提供
2019年连续五十三天修心 土司于北京