专业的JAVA编程教程与资源

网站首页 > java教程 正文

我用半天时间解决了困扰团队一年多的cpu使用率过高问题

temp10 2025-07-10 20:39:20 java教程 1 ℃ 0 评论

1 问题现象

使用docker stats发现机器上的有一个容器占用的cpu特别高,接近400%

但系统几乎没负荷,只有一两个设备和用户。据同事说,这个现象持续很久了,从他接手项目就是这样。

我用半天时间解决了困扰团队一年多的cpu使用率过高问题

到现在至少有一年多了,一直没人知道是什么原因,只知道是历史遗留问题。

正好周末我有空,就花了点时间研究一下。

2 使用arthas分析cpu占用情况

1 下载arthas

分析java的cpu使用问题,必须使用arthas。arthas可以到github下载:https://github.com/alibaba/arthas/releases

2 进入容器分析

先将下载下来的arthas-bin.zip解压,然后拷贝到容器里面

docker cp arthas-boot.jar f2dfdf703594:/arthas-boot.jar
docker exec -it f2dfdf703594 /bin/bash # 这里的f2dfdf703594就是前面cpu使用率过高的容器名称
cd /

3 切换到java进程所在用户启动arthas

容器里面的java进程是使用xxxxx_user用户启动的,如果直接使用arthas访问会报错

INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 1 iot-xxxxx.jar
  [2]: 91 -- main class information unavailable
1
[INFO] arthas home: /root/.arthas/lib/4.0.5/arthas
[INFO] Try to attach process 1
Picked up JAVA_TOOL_OPTIONS: 
com.sun.tools.attach.AttachNotSupportedException: Unable to open socket file: target process not responding or HotSpot VM not loaded
        at sun.tools.attach.LinuxVirtualMachine.<init>(LinuxVirtualMachine.java:106)
        at sun.tools.attach.LinuxAttachProvider.attachVirtualMachine(LinuxAttachProvider.java:78)
        at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:250)
        at com.taobao.arthas.core.Arthas.attachAgent(Arthas.java:102)
        at com.taobao.arthas.core.Arthas.<init>(Arthas.java:27)
        at com.taobao.arthas.core.Arthas.main(Arthas.java:161)
[ERROR] Start arthas failed, exception stack trace: 
[ERROR] attach fail, targetPid: 1

需要先切换为xxxxx_user再访问

usermod -s /bin/bash xxxxx_user # 允许xxxxx_user用户登录
su xxxxx_user # 切换xxxxx_user
whoami # 确认切换成功
cd / && java -jar arthas-boot.jar

4 分析占用情况

运行arthas,选择目标进程为iot-xxxxx.jar

执行thread -n 5,看看cpu占用排行前5的线程堆栈情况

输出如下:

bash-4.4$ cd / && java -jar arthas-boot.jar
[INFO] JAVA_HOME: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.312.b07-2.el8_5.x86_64/jre
[INFO] arthas-boot version: 4.0.5
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 1 iot-xxxxx.jar
  [2]: 91 -- main class information unavailable
1
[INFO] arthas home: /home/xxxxx_user/.arthas/lib/4.0.5/arthas
[INFO] The target process already listen port 3658, skip attach.
[INFO] arthas-client connect 127.0.0.1 3658
  ,---.  ,------. ,--------.,--.  ,--.  ,---.   ,---.                           
 /  O  \ |  .--. ''--.  .--'|  '--'  | /  O  \ '   .-'                          
|  .-.  ||  '--'.'   |  |   |  .--.  ||  .-.  |`.  `-.                          
|  | |  ||  |\  \    |  |   |  |  |  ||  | |  |.-'    |                         
`--' `--'`--' '--'   `--'   `--'  `--'`--' `--'`-----'                          

wiki        https://arthas.aliyun.com/doc                                       
tutorials   https://arthas.aliyun.com/doc/arthas-tutorials.html                 
version     4.0.5                                                               
main_class  iot-xxxxx.jar                                           
pid         1                                                                   
start_time  2025-06-13 19:33:44.764                                             
currnt_time 2025-06-15 10:18:11.722                                             

[arthas@1]$ thread -n 5
"Disruptor_Thread-2" Id=125 cpuUsage=98.32% deltaTime=199ms time=142858586ms RUNNABLE
    at java.lang.Thread.yield(Native Method)
    at com.lmax.disruptor.YieldingWaitStrategy.applyWaitMethod(YieldingWaitStrategy.java:58)
    at com.lmax.disruptor.YieldingWaitStrategy.waitFor(YieldingWaitStrategy.java:40)
    at com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:56)
    at com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:159)
    at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125)
    at java.lang.Thread.run(Thread.java:748)


"Disruptor_Thread-3" Id=126 cpuUsage=97.3% deltaTime=197ms time=142859828ms RUNNABLE
    at java.lang.Thread.yield(Native Method)
    at com.lmax.disruptor.YieldingWaitStrategy.applyWaitMethod(YieldingWaitStrategy.java:58)
    at com.lmax.disruptor.YieldingWaitStrategy.waitFor(YieldingWaitStrategy.java:40)
    at com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:56)
    at com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:159)
    at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125)
    at java.lang.Thread.run(Thread.java:748)


"Disruptor_Thread-1" Id=124 cpuUsage=96.94% deltaTime=196ms time=142868416ms RUNNABLE
    at java.lang.Thread.yield(Native Method)
    at com.lmax.disruptor.YieldingWaitStrategy.applyWaitMethod(YieldingWaitStrategy.java:58)
    at com.lmax.disruptor.YieldingWaitStrategy.waitFor(YieldingWaitStrategy.java:40)
    at com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:56)
    at com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:159)
    at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125)
    at java.lang.Thread.run(Thread.java:748)


"Disruptor_Thread-0" Id=123 cpuUsage=94.72% deltaTime=192ms time=142871747ms RUNNABLE
    at java.lang.Thread.yield(Native Method)
    at com.lmax.disruptor.YieldingWaitStrategy.applyWaitMethod(YieldingWaitStrategy.java:58)
    at com.lmax.disruptor.YieldingWaitStrategy.waitFor(YieldingWaitStrategy.java:40)
    at com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:56)
    at com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:159)
    at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125)
    at java.lang.Thread.run(Thread.java:748)

可以看到占用cpu靠前的进程都是disruptor框架的线程,disruptor这个框架我不熟,只能丢给豆包分析一下是否正常

根据豆包的分析,我怀疑是我们项目使用的策略问题。检查相关代码,发现使用的是YieldingWaitStrategy

        // 阻塞策略
        //BlockingWaitStrategy、SleepingWaitStrategy、YieldingWaitStrategy,YieldingWaitStrategy  的性能是最好的,适合用于低延迟的系统。在要求极高性能且事件处理线数小于 CPU 逻辑核心数的场景中,推荐使用此策略;例如,CPU开启超线程的特性。
        WaitStrategy strategy = null;
        int processors = Runtime.getRuntime().availableProcessors();
        // 4是指4个handle
        if (processors < 4 || processors < (4 * 2)) {
            strategy = new BlockingWaitStrategy();
        } else {
            strategy = new YieldingWaitStrategy();
        }

让豆包给我整理一下,有没有更好的策略

策略名称

核心机制

CPU 消耗

最低延迟

适用场景

BlockingWaitStrategy

使用 ReentrantLock+Condition 实现线程阻塞 / 唤醒

毫秒级

资源受限、对延迟不敏感的系统

SleepingWaitStrategy

多级策略:自旋→yield→固定时间 sleep(默认 100 纳秒)

中低

微秒级

一般业务系统(平衡延迟和 CPU)

YieldingWaitStrategy

自旋 + Thread.yield ()

纳秒级

低延迟、CPU 资源充足的场景

BusySpinWaitStrategy

纯忙等待(死循环)

极高

纳秒级

对延迟要求极致且有专用 CPU 核心的场景

PhasedBackoffWaitStrategy

自适应策略:自旋→yield→指数退避 sleep→最终可回退到 Blocking 策略

动态调整

纳秒→毫秒

负载波动大的复杂系统

TimeoutBlockingWaitStrategy

带超时的 Blocking 策略(防止永久阻塞)

毫秒级

需要超时控制的场景

发现PhasedBackoffWaitStrategy可能更适合我们系统,问豆包如果用它代替,效果为什么样,答案是这样的

指标

YieldingWaitStrategy

PhasedBackoffWaitStrategy

平均 CPU 使用率 (%)

~95

~30-60(取决于配置)

平均延迟 (μs)

~0.2

~0.5-1(初始阶段接近)

99.9% 分位延迟 (μs)

~1

~2-5(需优化配置)

吞吐量 (TPS)

接近(可能略有下降)


3 结论

当cpu核数 < 8时,系统启用了超低延时的策略,导致系统没有负载时,cpu也持续高使用率。

应该可以考虑使用更适配的策略如PhasedBackoffWaitStrategy,或者使用参数较合理的BlockingWaitStrategy

4 arthas其他功能

arthas是分析老项目性能问题的利器,除了前面介绍的查看cpu占用高的线程堆栈外,arthas还有其它好用的功能,可以到在线教程查看

https://arthas.aliyun.com/doc/

我这里贴几个豆包ai介绍的常用功能

基础监控功能

1. dashboard - 系统概览

功能

实时展示 JVM 进程的整体状态,包括线程、内存、GC、类加载等信息。

示例输出

ID NAME GROUP PRIORITY STATE %CPU TIME INTERRUPTED DAEMON

1 main main 5 RUNNABLE 0.0 0:00:00 false false

2 Reference Handler system 10 WAITING 0.0 0:00:00 false true
...

常用参数

-i 2000:指定刷新间隔(毫秒)。

2. thread - 线程分析

功能

查看线程详情,定位阻塞、死锁或高 CPU 线程。

常用命令

thread -n 3 # 查看 CPU 使用率最高的 3 个线程

thread -b # 找出阻塞其他线程的线程

thread <threadId> # 查看指定线程的堆栈

示例

"http-nio-8080-exec-1" Id=12 BLOCKED on java.util.concurrent.locks.ReentrantLock$NonfairSync@7c53a9eb

3. jvm - JVM 信息

功能

查看 JVM 运行时信息,如启动参数、系统属性、内存区域等。

用分析

1. watch - 方法监控

功能

监听方法的入参、返回值、异常及执行耗时。

示例

watch com.example.Service methodName '{params, returnObj, throwExp}' -x 2 -n 50

参数说明

-x 2:指定输出对象的深度。

-n 50:只监控 50 次调用。

2. trace - 方法调用链路分析

功能

追踪方法内部调用路径,计算每个子调用的耗时和次数。

示例

trace com.example.Service methodName

输出示例

[Trace] ---

`---[0.013ms] com.example.Service:methodName()

+---[0.002ms] com.example.Dao:query()

+---[0.005ms] com.example.Validator:validate()

`---[0.006ms] com.example.Cache:update()

3. stack - 方法调用栈

功能

记录方法被调用的调用路径。

示例

stack com.example.Service methodName

4. tt - 时光回溯(TimeTunnel)

功能

记录方法调用的所有信息,并可以进行回放。

示例

tt -t com.example.Service methodName # 记录方法调用

tt -i 1000 -p # 回放第 1000 次调用

类与字节码操作

1. sc - 类查找

功能

查找 JVM 中已加载的类。

示例

sc -d *Service # 查找所有 Service 结尾的类

sc -d com.example.UserService # 查看类的详细信息

2. sm - 方法查找

功能

查找类中的方法。

示例

sm com.example.UserService * # 查看 UserService 类的所有方法

3. jad - 反编译

功能

反编译已加载的类,查看运行时的代码。

示例

jad com.example.UserService

4. redefine - 热更新类

功能

在不重启 JVM 的情况下更新类的字节码。

示例

# 1. 反编译获取源码

jad com.example.UserService > /tmp/UserService.java

# 2. 修改代码并编译

javac -cp .:/path/to/your/classes /tmp/UserService.java

# 3. 重新定义类

redefine /tmp/UserService.class

JVM 参数与状态

1. vmoption - JVM 参数调整

功能

查看和修改 JVM 参数。

示例

vmoption PrintGCDetails true # 开启 GC 详细日志

vmoption -l # 列出所有 JVM 参数

2. heapdump - 堆转储

功能

生成当前堆内存的快照(类似 jmap -dump)。

示例

heapdump /tmp/dump.hprof

3. perfcounter - 性能计数器

功能

查看 JVM 内部的性能计数器信息。

示例

perfcounter

日志功能

1. logger - 日志级别调整

功能

动态调整日志框架的日志级别。

示例

logger -l # 查看所有 logger 及其级别

logger --name root --level debug # 将 root logger 级别设为 DEBUG

2. 结合 watch 监控日志

功能

监控方法调用时的日志输出。

示例

watch com.example.Service processOrder '{params[0], returnObj, #cost}' -x 2

高级功能

1. profiler - 性能分析

功能

基于 Async-profiler 生成 CPU 火焰图或内存分配火焰图。

示例

profiler start # 开始采集 CPU 火焰图

profiler stop # 停止采集并生成报告

profiler start --event alloc # 采集内存分配火焰图

2. ognl - 执行表达式

功能

执行 OGNL 表达式,动态获取或修改对象状态。

示例

ognl '@System@getProperty("java.home")' # 获取系统属性

ognl '@com.example.Cache@instance.clear()' # 清空缓存

3. mc & retransform - 内存编译与加载

功能

动态编译 Java 代码并加载到 JVM 中。

示例

# 1. 编写 Java 代码到 /tmp/Test.java

# 2. 内存编译

mc /tmp/Test.java -d /tmp

# 3. 加载类

retransform /tmp/com/example/Test.class

常用组合技巧

1. 定位高 CPU 问题

步骤

1. 查看 CPU 使用率最高的线程

thread -n 3

2. 获取线程 ID 对应的堆栈

thread <threadId>

3. 使用 profiler 生成火焰图

profiler start

# 等待一段时间后

profiler stop

2. 排查方法性能问题

步骤

1. 监控方法执行耗时

watch com.example.Service methodName '{params, returnObj, cost}' -x 2

2. 分析方法内部调用链路

trace com.example.Service methodName

3. 查看方法调用路径

stack com.example.Service methodName

注意事项

1. 性能影响

部分功能(如 trace、profiler)可能对性能有一定影响,建议在低峰期使用。

2. 权限要求

需要有足够的权限访问目标 JVM 进程。

3. 版本兼容性

确保 Arthas 版本与目标 JVM 兼容。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表