如何定位线上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-...

[2] 《操作系统原理-处理器调度》 liuyehcf.github.io/2017/09/25/...

[3] Why is the "top" command showing a CPU usage of 799%? superuser.com/questions/4...

[4] Nacos 出现大量线程创建的问题排查 www.liaochuntao.cn/2019/09/04/...

[5] 深度探索JFR - JFR详细介绍与生产问题定位落地 - 1. JFR说明与启动配置 zhuanlan.zhihu.com/p/122247741

[6] How to use JDK Flight Recorder (JFR)? access.redhat.com/solutions/6...

[7] Java 性能工具:async-profiler、JMH 优化实践探析-吴伟杰 www.bilibili.com/video/BV1YU...

[8] Java Flight Recording for JDK 8 [closed] stackoverflow.com/questions/5...

相关推荐
凡人的AI工具箱19 分钟前
15分钟学 Go 第 54 天 :项目总结与经验分享
开发语言·人工智能·后端·算法·golang
程序员阿龙34 分钟前
基于Spring Boot的在线性格测试系统设计与实现(源码+定制+开发)智能性格测试与用户个性分析平台、在线心理测评系统的开发、性格测试与个性数据管理系统
java·spring boot·后端·性格测评平台开发·用户性格测评系统·个性分析与推荐系统·心理测评与数据分析
Future_yzx39 分钟前
Spring基础之——控制反转(IOC)、依赖注入(DI)与切面编程(AOP)概念详解(适合小白,初学者必看)
java·后端·spring
计算机-秋大田2 小时前
基于Spring Boot的船运物流管理系统的设计与实现,LW+源码+讲解
java·论文阅读·spring boot·后端·vue
想花3 小时前
源码分析Spring Boot (v3.3.0)
java·spring boot·后端
machnerrn3 小时前
Flask和Python实现在线课堂学生疲劳检测系统设计与实现
后端·python·flask
_.Switch3 小时前
高效Django随机查询优化方案
后端·python·django
小码编匠3 小时前
C# 高性能动态获取对象属性值
后端·c#·.net
yyycqupt5 小时前
多路转接之poll
服务器·c++·后端·网络协议
泰山小张只吃荷园6 小时前
通过SpringTask模拟打印机定时向数据库传入模拟数据
java·开发语言·后端·spring·mybatis