星驰编程网

免费编程资源分享平台_编程教程_代码示例_开发技术文章

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

1 问题现象

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

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

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

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

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 兼容。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言