CPU飙高的现象很常见,但其实发现和解决起来并不是特别复杂,此处列举一些常见的CPU飙高案例,并给出解决方案和相关故障排查解决过程。
分析之前,复习几个知识点:
CPU性能指标:
load average:负载,linux查看的时候,通常显示如下:
代表了系统1分钟,5分钟,15分钟平均负载。
形象的类比可参考Understanding Linux CPU Load - when should you be worried? | Scout APM Blog
另一个形象地比喻:CPU的load和使用率傻傻分不清 - 昀溪 - 博客园 (cnblogs.com)
上图1个电话亭可以理解为一个CPU核心。从上图的过程中可以看到load的概念,而使用率始终100%。
使用率
%Cpu(s):用户空间占用CPU百分比
%CPU:上次更新到现在的CPU时间占用百分比
查看
cat /proc/cpuinfo
grep -c ‘model name’ /proc/cpuinfo
实验1:观察CPU使用率
public static void main(String[] args) {
try {
//模拟CPU占用率
cpu50(Integer.parseInt(args[0]));
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
public static void cpu50(int num) {
for (int i = 0; i < num; i++) {
new Thread(() -> {
while (true) {
long time_start;
int fulltime = 100;
int runtime = 50;
while (true) {
time_start = System.currentTimeMillis();
while ((System.currentTimeMillis() - time_start) < runtime) {
}
try {
Thread.sleep(fulltime - runtime);
} catch (InterruptedException e) {
return;
}
}
}
}).start();
}
}
启动:java -jar 2_cpu-0.0.1-SNAPSHOT.jar 8 > log.file 2>&1 &
实验2:定位CPU标高
方法1:
1-启动:java -jar
2_cpu-0.0.1-SNAPSHOT.jar 8 > log.file 2>&1 & 2-一般来说,应用服务器通常只部署了java应用,可以top一下先确认,是否是java应用导致的:命令:top
3-如果是,查看java进场ID,命令:jps -l
4-找出该进程内最好非CPU的线程,命令:top -Hp pid 25128
5-将线程ID转化为16进制,命令:printf "%x\n" 线程ID 623c 25148
6-导出java堆栈信息,根据上一步的线程ID查找结果:命令: jstack 11976 >stack.txt grep 2ed7 stack.txt -A 20
方法2:
在线工具:https://gceasy.io/ft-index.jsp
1-方法1中导出的对快照文件,上传到该网站即可
1.发现问题
7.8日 17:09监控大盘发现xxx-web的CPU快跑满,导致机器不断扩容增加机器。
平均负载图如下:
CPU利用率:
2.分析阶段
根据对线程dump文件进行分析,主要是否存在死锁、阻塞现象,以及CPU线程的占用情况,分析
工具采用在线fastThread工具。地址:https://gceasy.io/ft-index.jsp
(1)查看线程数情况
数据图示可知,创建的线程等待线程有3000,提示高线程数可能导致内存泄露异常,从而可能影响后面任务的创建线程。
(2)查看当前CPU线程使用情况
根据CPU线程情况,查询CPU正在执行的线程堆栈列表,可以发现大部分日志都是类似于:catalina-exec-879
(3)定位出现问题的线程堆栈
查看是新版头像圈的一段代码逻辑,其中有个步骤需要深拷贝对象,以便以后逻辑更改使用。
3.问题恢复
从而猜测可能是跟8号开放一批白名单规则用户有关,所以临时采用更改config的白名单策略配置,降低灰度用户范围。通过配置推送后,CPU利用率恢复正常情况。
调整后机器CPU利用率:
服务池平均CPU利用率情况如下:
4.相关代码:
public Map<String, TopHeadInfoV3> getTopHeadInfoGroupByLiveIdCache(){
Map<String, TopHeadInfoV3> topHeadInfoGroupByLiveIdMap =
topHeadInfoGroupByLiveIdCache.getUnchecked("top_head_info_group_by_live_id");
if (MapUtils.isEmpty(topHeadInfoGroupByLiveIdMap)) {
return Collections.emptyMap();
}
// guava cache 对象对外不可⻅,防⽌上游对cache⾥对象进⾏修改,并发场景下出现问题
Map<String, TopHeadInfoV3> topHeadInfoGroupByLiveIdMapDeepCopy =
Maps.newHashMapWithExpectedSize(topHeadInfoGroupByLiveIdMap.size());
for (Map.Entry<String, TopHeadInfoV3> entry : topHeadInfoGroupByLiveIdMap.entrySet()) {
topHeadInfoGroupByLiveIdMapDeepCopy.put(entry.getKey(),
SerializationUtils.clone(entry.getValue()));
}
return topHeadInfoGroupByLiveIdMapDeepCopy;
}
其中影响性能的问题方法是apache commongs工具包提供的对象克隆工具:
SerializationUtils.clone(entry.getValue())),基本操作就是利用ObjectInputStream和ObjectOutputSTream进行,先序列化再发序列化。频繁克隆且耗时较长,导致占用其他任务的执行。
public static <T extends Serializable> T clone(final T object) {
if (object == null) {
return null;
}
final byte[] objectData = serialize(object);
final ByteArrayInputStream bais = new ByteArrayInputStream(objectData);
try (ClassLoaderAwareObjectInputStream in = new ClassLoaderAwareObjectInputStream(bais,
object.getClass().getClassLoader())) {
/*
* when we serialize and deserialize an object,
* it is reasonable to assume the deserialized object
* is of the same type as the original serialized object
*/
@SuppressWarnings("unchecked") // see above
final T readObject = (T) in.readObject();
return readObject;
} catch (final ClassNotFoundException ex) {
throw new SerializationException("ClassNotFoundException while reading cloned object data", ex);
} catch (final IOException ex) {
throw new SerializationException("IOException while reading or closing cloned object data", ex);
}
}
源代码注释:
使用序列化来深度克隆一个对象。 这比在你的对象图中的所有对象上手工编写克隆方法要慢很多倍。然而,对于复杂的对象图,或者那些不支持深度克隆的对象,这可以是一个简单的替代实现。当然,所有的对象必须是可序列化的。
5.优化方案
经过讨论临时采用创建对象和属性设置的方式进行对象复制,先不采用对象序列化工具。实现java.lang.Cloneable接口并实现clone方法。
主要对象拷贝代码如下:
@Override
public TopHeadInfoV3 clone() {
Object object = null;
try {
object = super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
TopHeadInfoV3 topHeadInfoV3 = (TopHeadInfoV3) object;
topHeadInfoV3.playEnums = Sets.newHashSet(topHeadInfoV3.playEnums);
topHeadInfoV3.recallTypeEnums = Sets.newHashSet(topHeadInfoV3.recallTypeEnums);
topHeadInfoV3.behaviorEnums = Sets.newHashSet(topHeadInfoV3.behaviorEnums);
topHeadInfoV3.micLinkUserList =
topHeadInfoV3.micLinkUserList.stream().map(MicLinkUser::clone).collect(Collectors.toList());
return topHeadInfoV3;
}
6.上线情况
优化方案上线,13号下午两点全量后,看监控大盘cpu利用率为正常情况,cpu利用率如下:
7.问题总结
针对相对大流量的接口可以提前做好压测分析;
上线后定期通过监控大盘查看线上运行情况,留意机器监控告警便于及时发现问题;
若告警或大盘发现问题CPU或内存使用情况异常,其中可以打印线程堆栈日志,通过堆栈分析工具帮助分析线程使用的情况。
序列化参考:Home · eishay/jvm-serializers Wiki · GitHub
报表worker-CPU使用率过高【618备战】
一、排查过程
1:查看机器监控,初步判断可能有耗CPU的线程
2:导出jstat信息,发现JVM老年代占用过高(达到97%),Full-GC频率超高,FULL-GC总共占用了36小时。初步定位是频繁FULL-GC导致CPU负载过高。
3:使用jmap –histo导出堆概要信息,发现有个超大的HashMap。
4:使用jmap –dump导出堆。得出hashMap中的KEY是运单号
二、总结
1:使用缓存时要做容量估算,并考虑数据增长率
2:缓存要有过期时间。
场景问题案例解读:
1、该应用上线弹性数据库后,调用端通过接口查询历史库应用服务时,出现大面积
RpcException,如下图所示
2、观察该应用,full gc 情况,如下图所示,会出现高频full gc 情况
3、观察应用,young gc情况,如下图所示
4、查看jvm配置参数时,配置内容如下(可以通过ump、应用配置、堡垒机打印应用信息等方式查看)
-Xss512k
-Xmn2048m
-XX:+UseConcMarkSweepGC
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+CMSParallelRemarkEnabled
-XX:+CMSClassUnloadingEnabled
-XX:CMSInitiatingOccupancyFraction=60
-XX:CMSInitiatingPermOccupancyFraction=60
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintClassHistogram
-Xloggc:/export/Logs/jvm/gc.log
5、通过jstat命令打印内存情况如下图所示
命令:jstat -gcutil pid
S0 S1 E O P YGC YGCT FGC FGCT GCT
4.26 0.00 71.92 28.36 59.99 3374 427.474 599 1071.941 1499.415
4.26 0.00 72.17 28.36 59.99 3374 427.474 599 1071.941 1499.415
4.26 0.00 72.43 28.36 59.99 3374 427.474 599 1071.941 1499.415
4.26 0.00 73.08 28.36 59.99 3374 427.474 599 1071.941 1499.415
4.26 0.00 73.09 28.36 59.99 3374 427.474 599 1071.941 1499.415
6、之前我们已经配置了jvm参数来打印gc日志,如下所示
66289.394: [GC66289.394: [ParNew: 1685902K->8995K(1887488K), 0.1182020 secs] 2289213K-
>612400K(3984640K), 0.1188790 secs] [Times: user=0.45 sys=0.01, real=0.12 secs]
66312.916: [GC66312.916: [ParNew: 1686819K->8220K(1887488K), 0.1287130 secs] 2290224K-
>611674K(3984640K), 0.1296130 secs] [Times: user=0.48 sys=0.02, real=0.13 secs]
66317.050: [GC [1 CMS-initial-mark: 603454K(2097152K)] 884210K(3984640K), 0.1249350 secs] [Times:
user=0.13 sys=0.01, real=0.12 secs]
66317.176: [CMS-concurrent-mark-start]
66317.567: [CMS-concurrent-mark: 0.391/0.391 secs] [Times: user=1.45 sys=0.06, real=0.40 secs]
66317.567: [CMS-concurrent-preclean-start]
66317.586: [CMS-concurrent-preclean: 0.017/0.018 secs] [Times: user=0.02 sys=0.00, real=0.01
secs]
66317.586: [CMS-concurrent-abortable-preclean-start]
CMS: abort preclean due to time 66322.639: [CMS-concurrent-abortable-preclean: 3.043/5.053 secs]
[Times: user=3.36 sys=0.32, real=5.06 secs]
66322.643: [GC[YG occupancy: 525674 K (1887488 K)]66322.643: [Rescan (parallel) , 2.3838210
secs]66325.027: [weak refs processing, 0.0014400 secs]66325.029: [class unloading, 0.0305350
secs]66325.059: [scrub symbol table, 0.0141910 secs]66325.074: [scrub string table, 0.0032960
secs] [1 CMS-remark: 603454K(2097152K)] 1129128K(3984640K), 2.4410070 secs] [Times: user=9.16
sys=0.40, real=2.44 secs]
66325.085: [CMS-concurrent-sweep-start]
66325.444: [CMS-concurrent-sweep: 0.318/0.358 secs] [Times: user=0.51 sys=0.04, real=0.36 secs]
66325.444: [CMS-concurrent-reset-start]
66325.450: [CMS-concurrent-reset: 0.006/0.006 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
66347.427: [GC66347.427: [ParNew: 1686044K->11073K(1887488K), 0.1323420 secs] 2269768K-
>595648K(3984640K), 0.1330670 secs] [Times: user=0.49 sys=0.02, real=0.13 secs]
综上所述:
通过gc日志可以看到CMS remark阶段耗时较长,如果频繁的full gc且remark时间比较长,会导致调用端大面积超时,接下来需要通过jstat命令查看内存情况结合配置的jvm启动参数看一下为啥会频繁的full gc。应用jvm启动参数配置了-XX:CMSInitiatingPermOccupancyFraction=60,持久带使用空间占60%的时候就会触发一次full gc,由于持久带存放的是静态文件,持久带一般情况下对垃圾回收没有显著影响。所以可考虑去掉该配置项。
解决方案:
持久带用于存放静态文件,如今Java类、方法等, 持久代一般情况下对垃圾回收没有显著影响,应用启动后持久带使用量占比即接近60%,所以可考虑去掉该配置项。
同时增加配置项-XX:+CMSScavengeBeforeRemark,在CMS GC前启动一次young gc,目的在于减少old gen对ygc gen的引用,降低remark时的开销(通过上述案例可观察到,一般CMS的GC耗时 80%都在remark阶段 )
jvm启动参数更正如下:
-Xss512k
-XX:+UseConcMarkSweepGC
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=80
-XX:+CMSScavengeBeforeRemark
-XX:+CMSParallelRemarkEnabled
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/export/Logs/jvm/gc.log
-javaagent:/export/servers/jtrace-agent/pinpoint-bootstrap-1.6.2.jar
-Dpinpoint.applicationName=afs
-Dpinpoint.sampling.rate=100
备注:相比有问题版本,去除了配置项:-XX:CMSInitiatingPermOccupancyFraction=60。
-XX:CMSInitiatingOccupancyFraction 修改为80(Concurrent Mark Sweep (CMS)
Collector (oracle.com)),添加配置项-XX:+CMSScavengeBeforeRemark,为了个更好的观察
gc日志,修改时间戳打印格式为-XX:+PrintGCDateStamps
修正后结果如下图所示:
1、调用端调用服务正常,不再出现rpc exception
2、应用young gc情况如下
问题场景
某定时任务job 收到cpu连续(配置的时间是180s)使用超过90%的报警;
问题定位
a) 观察报警中的jvm监控,发现周期性出现即每天8:00,cpu从5%-99%大约持续3分钟左右然后恢复正常,平时cpu使用率较为平稳(排除因为应用发布导致的cpu升高)
b) 任务系统周期性出现很可能是定时执行了大量运算导致的,查看任务系统页面,确实存在一个8点执行的定时任务
c) 分析代码梳理该任务的业务逻辑为:一个兜底的定时的job任务;其中涉及大量复杂运算,现在猜测基本是改任务导致的,那么可以复现一下,确认下。
复现操作很简单:
c1)找一台机器,观察jvm相关监控;观察日志
c2) 修改分片数量为1且指定分片容器ip为监控的容器ip, 点击执行分片,查看分片确认成功后,点击执行一次。通过观察步骤c1)成功复现。
d) 至此基本方向确定是这个任务导致的CPU升高,接下来分析为何CPU升高,以及如何优化问题。
问题分析
对于2C4G优化方案:a) 由于这个任务是兜底的,不需要立即执行完成,且执行频率为1天1次可以将线程池调整为1个线程;
b) 申请容器升级为4c*8G
2C4G配置的执行job单线程跑任务
parallelStream()原理:
并行流使用问题分析:
//code1
WORDS.entrySet().parallelStream().sorted((a,b)-
>b.getValue().compareTo(a.getValue())).collect(Collectors.toList());
//code2
Set<String> words = new ConcurrentHashSet<>();
words1.parallelStream().forEach(word -> words.add(word.getText()));
由此可知,默认的ForkJoinPool获取的是当前系统的核心数量,如果应用部署在docker容器中,那么就获取的是宿主机的CPU核心数
Runtime.getRuntime().availableProcessors()问题 :
容器明明分配的是2个逻辑核心数,为什么java获取的会是物理机的核心数呢?如何解决这个问题呢。
是否是容器构建的时候某些参数配置的原因导致的? 将问题反馈给运维,但是对方并未解决该问题。
那么java是否可以解决呢?毕竟如果我们自定义线程池设置线程数量也会使用
Runtime.getRuntime().availableProcessors()这个方法,
这其实是JDK的一个问题,已经trace在JDK-8140793,原因是获取CPU核数是通过读取两个环境变量,其中
其中_SC_NPROCESSORS_CONF 就是我们需要容器真实的CPU数量。
获取CPU数量的源码
第一种办法是使用新版本的Jdku131以上的版本1。
另外一个办法是使用自编译上面的源代码,通过LD_PRLOAD的方式将修改后的so文件加载进去
Mock掉CPU的核数
jdk官方链接声明:Java SE support for Docker CPU and memory limits
使用方法一测试:测试环境容器验证,验证通过,结果如下:
jdk版本 jdk1.8.0_20 :返回cpu核心数28
jdk版本: jdk-1.8.0_192 返回cpu核心数2
建议
尽量使用lambda表达式遍历数据,推荐使用常规的for、for-each模式
原因:
// lambda并没有起到简化代码的作用,反而会增加系统压力
List<ValidationResult> results = children.stream()
.map(e -> e.validate(context, nextCell, nextCoverCells, occupyAreas))
.collect(Collectors.toList());
List<ValidationResult> failureList =
results.stream().filter(ValidationResult::isFailure).collect(Collectors.toList());
List<ValidationResult> successList =
results.stream().filter(ValidationResult::isSuccess).collect(Collectors.toList());
正例:
// 优化后总cpu下降2%-4%左右。
// 备注:cpu下降如此明显的原因是该代码的调用频率非常高,真正的N万次/秒调用
List<ValidationResult> failureList = new ArrayList<>(4);
List<ValidationResult> successList = new ArrayList<>(4);
for (PathPreAllocationValidator child : children) {
ValidationResult result = child.validate(context, currCell, previousCell, nextCell);
if (result.isSuccess()) {
successList.add(result);
} else {
failureList.add(result);
}
}
非标导入使用easyexcel组件进行导入处理,10几万的数据量引发CPU彪高
1、查看线程栈相关信息
2、pinpoint监控查看性能及代码调用情况
3、是否存在大量阻塞慢SQL
4、是否存在短时间内频繁日志输出
使用之前分表导入的30万数据进行导入操作,myops查看排名前十线程栈相关信息(如下图),发现lbs_non_standard_account_common单个线程消费占用CPU 8%左右,启动的线程数量很多
1、AnalysisEventListener的子类CommonImportExcelListener的invoke方法已将近每秒1万的速率迅速完成业务逻辑校验发送MQ,该MQ内部消费进行业务处理逻辑处理。
30万的数据将近30秒内发送MQ完成,给消费内部逻辑带来压力导致CPU彪高,使用RateLimiter进行了MQ发送的限流。
2、调整消费者主题“lbs_top” 的maxConcurrent参数(该参数设置为单组启动线程数),单个应用实例启动线程的最大数=maxConcurrent参数* 分组数量.
3、针对lbs_top消费逻辑中的部分业务查询进行内存缓存改造
数据库 cpu 占用率过高事故分析
时间:2018.2.5 18:00:00 --2018.2.6 10:30:00
问题:中台数据库 cpu 占用率超过 89%
原因:系统 DB sql 存在慢查询,并且读写均在主库,随着系统流量的增长, 数据库 cpu 占用持续增高,2 月 5 日当天数据库 cpu 占用率达到了 89%,2 月 6 日 cpu 占 用率达到 91%;
该系统最近每天超于 30%的量增长,2 月 5 日下午 18:00:00 左右田xx 通过 mdc 监控观察行情数据库的 cpu 占用率达到 89%,而且业务方第二天 2 月 6 日要上 6 个新的产品,预估流量会高于 5 日流量峰值,系统风险非常高。