如何定位线上Java应用CPU飙高

理清概念

对这个问题,感觉自己认知的还是不够清晰,于是今天就来理清楚思路,说到线上CPU飙高,说起这个问题的时候,我想到了什么呢? 我想起了处理器调度,我想起Linux CFS调度,CFS是 Completely Fair Scheduler,也就是完全公平调度,也就是说它完全实现了所谓的"完全公平"调度算法,将CPU资源均匀的分配给各个进程(在内核中被称为任务,(Linux采用轻量级进程实现线程,所以这里说的线程可以认为线程的同义语), 粗略的说,如果一台计算机有一个CPU,多个计算密集型进程,那么采用CFS调度器时,每个进程占用的时间为:

我记得曾经我的某位老师告诉我,线程是去抢CPU的,但其实这是有点本末倒置,事实上是CPU从就绪队列里面选取任务进行执行,现代计算机基本上都是多核处理器,Linux默认的CFS调度采用的是多队列,也就是说应用程序提交的任务,由操作系统决定放入到哪个队列:

所谓抢占式调度不是线程去抢CPU,而是线程的时间片消耗尽,被CPU调度器强制打断,与协作式相相对,协作式调度则依靠被调度方主动弃权。到现在我们知道CPU是多核心的,每个核心都在不断的执行就绪队列里面的任务,如果就绪队列里面有任务的话,在这个角度上来说,多个核心可以同时执行任务,也就是并行,与并发相对,某个核心在一个时间点只能执行一个任务,如果我们将时间拉长,多个任务可以在这段时间都获得了执行,交错执行。也就是说一台计算机现在有多个核心,那么使用率应当是多个核心的累加,所以如果是八核十六线程,可以看到使用率最高达到1600%,这意味着每个核心都跑满了。等等你说到了八核十六线程,这是什么意思,不是一个核心跑一个任务嘛,怎么这一平均,就变成一个核心跑两个线程了, 我的电脑就是八核十六线程,在我的电脑上执行下面的代码输出就是16:

java 复制代码
System.out.println(Runtime.getRuntime().availableProcessors());

那这是什么意思呢? 这其实要扯上超线程这种技术,通常情况下,一个核心可以在同一时间执行一个线程,当激活并支持超线程技术时,核心可以在同一时间执行两个线程。那么当你用top在Linux查看CPU使用率的时候事实上是多个核心使用率的累加, 所以如果你的如果你的逻辑处理器是四个核心,每个核心的使用率为百分之百二十,用top指令看总的使用率也就是百分之八十,那如何看每颗核心的使用率呢:

java 复制代码
#每秒统计一下CPU的使用率
mpstat -P ALL 3
# top 指令之后 摁1就行    

该如何理解CPU飙高?

我思考这个问题的时候,也是从CPU利用率这个角度来入手的,只不过理解CPU使用率的计算公式为,假设某核心的能力为100条指令,在某段时间内执行了90条指令,然后利用率就是百分之九十,但事实上的利用率的公式为1-空闲时间/CPU时间,这个公式暗含的意思是在一段时间内,CPU在不断被使用,这里我们没有用分类,分成几种情况,因为一种本质可以导出很多种不同的情况。这样讲可能有些抽象,我们看下实际的例子。

例子1: Nacos 出现大量线程创建的问题排查

Nacos做为服务中心和配置中心为大家所熟知,我从Nacos的Github上选取了一些例子来说明情况,其中一个例子是issue5764,标题为: "当项目用了多个数据源并配置spring.cloud.nacos.config.refreshable-dataids 时线程数不停飙升 ":

这个issue也很贴心说了问题之后,还说了自己是怎么排查的,他真的,我哭死,这也是我们后面要介绍的工具。一个CPU高占用背后可能是多个问题,这个issue很有参考价值,我决定盘一盘,首先issue里面提到issue1605 ,我们顺着链接看一看, Issue1605的标题也很朴素:

下面贴心的给了线程的堆栈,还有点美中不足的是这位兄弟上传的图裂了,但是没关系,他把堆栈信息也粘贴上来了, 堆栈信息比较长,这里我挑一些比较核心的,来说明一下问题:

Runnable说明当前线程在执行或者在等待资源,例如I/O,或者处理器等。我们接着看堆栈信息:

下面还有不同的线程池,好像看不出来太多有价值的信息,但是柳暗花明下面放了一个分析的链接:

真的好贴心,我们顺着链接进去看:

最后分析的结论是用户大量创建了NacosConfigService,链接中的图片已经裂了,但是没关系文章说了采样发现里面有大量的ClientWorker对象,有两千多个,然后一个对应这么多线程,而且都有任务在身,想来CPU高占用也不冤枉。

Nacos Issue 859

我们接着看那个帖子里面的issue:

这个jmap我们在《JVM学习笔记(一) 初遇篇》里面已经让它漏过头了,可以用这个命令来导出dump文件,用来分析内存使用问题。现在还没有有价值的信息,我们接着往下翻:

Spring Cloud在配置刷新时触发Context的refresh操作,这里会导致nacos-client实例被重复创建。这个Nacos-Client实例也就是上面说的ClientWorker,也是一样的问题,大量线程被创建。

Nacos Issue 5764

这位大哥说刷新配置导致Controller被销毁,销毁之后又要重建,我们看下这个大哥的分析之路,这位大哥首先还是用了jsstack去获取线程上下文信息:

java 复制代码
Thread 15906: (state = BLOCKED)
 - sun.misc.Unsafe.park(boolean, long) @bci=0 (Compiled frame; information may be imprecise)
 - java.util.concurrent.locks.LockSupport.park(java.lang.Object) @bci=14, line=175 (Compiled frame)
 - java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt() @bci=1, line=836 (Compiled frame)
 - java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireShared(int) @bci=83, line=967 (Compiled frame)
 - java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireShared(int) @bci=10, line=1283 (Compiled frame)
 - java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock.lock() @bci=5, line=727 (Compiled frame)
 - org.springframework.cloud.context.scope.GenericScope$LockedScopedProxyFactoryBean.invoke(org.aopalliance.intercept.MethodInvocation) @bci=132, line=494 (Compiled frame)
 - org.springframework.aop.framework.ReflectiveMethodInvocation.proceed() @bci=120, line=186 (Compiled frame)
 - org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed() @bci=1, line=747 (Compiled frame)
 - org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(java.lang.Object, java.lang.reflect.Method, java.lang.Object[], org.springframework.cglib.proxy.MethodProxy) @bci=133, line=689 (Compiled frame)

这位大哥说,大量线程阻塞在这个接口上面,但是我们阻塞状态是等待获取锁,是不占用CPU的,那么看线程堆栈是阻塞在ReentrantReadWriteLock的lock方法,阻塞意味着此时有线程在写,所以读线程在不能读,读线程在lock的时候做了些什么呢? ReentrantReadWriteLock基于AQS:

java 复制代码
/**
 * Acquires the read lock.
 *  尝试获取读锁
 * <p>Acquires the read lock if the write lock is not held by
 * another thread and returns immediately.
 * 如果写锁没被另一个持有立刻返回
 * <p>If the write lock is held by another thread then
 * the current thread becomes disabled for thread scheduling
 * purposes and lies dormant until the read lock has been acquired.
 * 如果写锁被另一个线程所持有,当前线程将会被调度器禁用,直到获取读锁
 */
public void lock() {
    sync.acquireShared(1);
}
java 复制代码
public final void acquireShared(int arg) 0{
    if (tryAcquireShared(arg) < 0)      
        // 看上面的调用显然走到了这里
        doAcquireShared(arg);
}

所以也是有大量线程在不断使用CPU。到这里其实答案已经出来了,CPU飙高就是CPU在不断的执行任务,什么是对齐颗粒度,就是颗粒度对齐。

其实也不见得需要大量线程,每个核心一个队列,我们为每个核心都准备一个停不下来的任务,CPU的使用率就会上去。我们下面也举了这样一个例子。

那么如何定位问题?

这里我们介绍一些工具,通过这些工具来定位究竟是哪一行代码出现了问题,这里我们主要介绍Java Flight Record 、Java Mission Control,Java Flight Record来自Oracle,暗含的意思是JVM的黑盒子,记录运行中JVM的问题,且对性能影响非常小,官方宣称在百分之一左右,可以伴随应用启动而启动,在JDK8u40之后可以在运行时灵活的开启关闭。Java Flight Record 产生记录,而Java Mission Control则用来分析JFR产生的记录,再简单介绍一下async-profiler,async-profiler是针对JVM的采样分析工具,不受Safepoint Bias影响,性能开销低(采样频率可以调整),生产可用,可基于Java Agent启动,或指定PID连接已有JVM,支持多种输出格式(HTML、SVG、JFR等)及格式转换。在IDEA的mac 、Linux版本默认集成,Windows上的是IntelliJ Profile,也就是说async-profiler不支持Windows,但不影响async-profiler的优秀:

JFR支持全部的操作系统,我们还是先介绍JFR。

JFR简介

应用启动时启动

首先我们写一个持续占用CPU的代码,像下面这样:

java 复制代码
@GetMapping("/test")
public  void test(){
    ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    for (int i = 0; i < 16; i++) {
        pool.execute(()->{
            randomNumber();
        });
    }
}
public void randomNumber() {
    Random random = new Random(100);
    while (true){
        random.nextInt();
    }
}

在IDEA中的vm options中加上下面的参数:

java 复制代码
-XX:+UnlockCommercialFeatures -XX:StartFlightRecording=filename=flight.jfr,dumponexit=true,delay=3s,size=1024m,maxage=30d
  • -XX:+UnlockCommercialFeatures 8需要这个参数, 解锁商业许可, 在现在不需要收费了,oracle将JFR和JMC都捐献给了OpenJDK,这个参数只在JDK8需要,在11和17 不需要此参数
  • -XX:StartFlightRecording=filename=flight.jfr 开启JFR记录,指定文件名字
  • dumponexit 程序退出的时候是否导出.jfr文件
  • maxage 文件最多保存时间
  • maxsize 最大文件大小
  • delay 延迟多久开始记录

然后我们用JMC打开这个文件,JMC 也就是 Java mission Control,在JDK的bin目录下面:

我们这里选择打开JFR文件:

应用启动之后启动

java 复制代码
# 标准格式,启动JFR
jcmd <pid> JFR.start
# name是记录名称 为采集的JFR 起一个名字
jcmd 4204 JFR.start name=profile_online  filename=D:\tools\recording.jfr maxage=1d maxsize=1g
# 停止采集    
jcmd 4204  JFR.stop name=profile_online

我在IDEA里面启动应用,然后用jcmd去启动JFR有点奇怪的问题,然后我用jar包启动就正常了。

有请async-profiler

shell 复制代码
# 这个页面选择自己合适的架构 https://github.com/async-profiler/async-profiler
tar -zxvf 下载下来的文件名称
# 采集CPU事件 采样180s -f后面跟路径,这里直接放在当前路径下
# 我下载的3.0的bin叫asprof,我没有将其加入到环境变量里面
./asprof  -e cpu -d 180 -f cpu_profile.jfr  $JVM_PID
# 使用 async-profiler 采集 JVM 进程中,阻塞超过 100us 的 lock,采样60 秒并输出到文件output.jfr
./asprof -d 60 -e cpu --lock 1ms -f output.jfr $JVM_PID

这里只做个简单介绍,async-profiler有非常丰富的用法,我们会在后面进行详细介绍。

写在最后

刚开始的问题是CPU飙高的原因,那意味着CPU被持续使用,在最开始的时候我的想法是用CPU的能力去套,但是通过一些例子的分析发现,答案就是CPU被持续使用,有点像《年会不能停》里面的对齐颗粒度是什么意思,就是颗粒度对齐,这个过程中又复习了一下CPU调度,线程是应用程序提交的任务,操作系统根据一定的策略将它们放到不同核心的队列里面,并发并行处于其中。JFR的使用相当简单,又直接位于JDK内部,所以优先介绍了JFR,async-profiler功能丰富,这里简单的提了一嘴async-profiler。那么如何定位线上Java应用CPU飙高呢,我们就需要对JVM做分析,可以用JFR进行记录,也可以用async-profiler进行采样,然后用JMC分析采样之后的文件。

参考资料

1\] 《Linux CFS 调度器:原理、设计与内核实现(2023)》 [arthurchiao.art/blog/linux-...](https://link.juejin.cn?target=https%3A%2F%2Farthurchiao.art%2Fblog%2Flinux-cfs-design-and-implementation-zh%2F "https://arthurchiao.art/blog/linux-cfs-design-and-implementation-zh/") \[2\] 《操作系统原理-处理器调度》 [liuyehcf.github.io/2017/09/25/...](https://link.juejin.cn?target=https%3A%2F%2Fliuyehcf.github.io%2F2017%2F09%2F25%2F%25E6%2593%258D%25E4%25BD%259C%25E7%25B3%25BB%25E7%25BB%259F%25E5%258E%259F%25E7%2590%2586-%25E5%25A4%2584%25E7%2590%2586%25E5%2599%25A8%25E8%25B0%2583%25E5%25BA%25A6%2F "https://liuyehcf.github.io/2017/09/25/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%8E%9F%E7%90%86-%E5%A4%84%E7%90%86%E5%99%A8%E8%B0%83%E5%BA%A6/") \[3\] Why is the "top" command showing a CPU usage of 799%? [superuser.com/questions/4...](https://link.juejin.cn?target=https%3A%2F%2Fsuperuser.com%2Fquestions%2F457624%2Fwhy-is-the-top-command-showing-a-cpu-usage-of-799%2F457634%23457634 "https://superuser.com/questions/457624/why-is-the-top-command-showing-a-cpu-usage-of-799/457634#457634") \[4\] Nacos 出现大量线程创建的问题排查 [www.liaochuntao.cn/2019/09/04/...](https://link.juejin.cn?target=https%3A%2F%2Fwww.liaochuntao.cn%2F2019%2F09%2F04%2Fjava-web-53%2F "https://www.liaochuntao.cn/2019/09/04/java-web-53/") \[5\] 深度探索JFR - JFR详细介绍与生产问题定位落地 - 1. JFR说明与启动配置 [zhuanlan.zhihu.com/p/122247741](https://link.juejin.cn?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F122247741 "https://zhuanlan.zhihu.com/p/122247741") \[6\] How to use JDK Flight Recorder (JFR)? [access.redhat.com/solutions/6...](https://link.juejin.cn?target=https%3A%2F%2Faccess.redhat.com%2Fsolutions%2F662203 "https://access.redhat.com/solutions/662203") \[7\] Java 性能工具:async-profiler、JMH 优化实践探析-吴伟杰 [www.bilibili.com/video/BV1YU...](https://link.juejin.cn?target=https%3A%2F%2Fwww.bilibili.com%2Fvideo%2FBV1YU4y1q7Nq%2F%3Fspm_id_from%3D333.337.search-card.all.click%26vd_source%3Daae3e5b34f3adaad6a7f651d9b6a7799 "https://www.bilibili.com/video/BV1YU4y1q7Nq/?spm_id_from=333.337.search-card.all.click&vd_source=aae3e5b34f3adaad6a7f651d9b6a7799") \[8\] Java Flight Recording for JDK 8 \[closed\] [stackoverflow.com/questions/5...](https://link.juejin.cn?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F52296247%2Fjava-flight-recording-for-jdk-8 "https://stackoverflow.com/questions/52296247/java-flight-recording-for-jdk-8")

相关推荐
神奇小汤圆5 小时前
浅析二叉树、B树、B+树和MySQL索引底层原理
后端
文艺理科生5 小时前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling5 小时前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅5 小时前
springBoot项目有几个端口
java·spring boot·后端
Luke君607975 小时前
Spring Flux方法总结
后端
define95275 小时前
高版本 MySQL 驱动的 DNS 陷阱
后端
忧郁的Mr.Li6 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
暮色妖娆丶6 小时前
SpringBoot 启动流程源码分析 ~ 它其实不复杂
spring boot·后端·spring
Coder_Boy_6 小时前
Deeplearning4j+ Spring Boot 电商用户复购预测案例中相关概念
java·人工智能·spring boot·后端·spring
Java后端的Ai之路7 小时前
【Spring全家桶】-一文弄懂Spring Cloud Gateway
java·后端·spring cloud·gateway