CPU 100% 优化排查实战
1 问题背景
收到运维同学的报警,某些服务器负载非常高。经过初步排查,发现服务器上运行的只有我们的 Java 应用程序。于是,我们开始了一系列的排查和优化工作。
2 排查步骤
2.1 获取进程信息
首先,使用 ps
命令获取应用的 PID:
ps -ef | grep java
2.2 查看线程 CPU 使用情况
使用 top -Hp pid
命令查看该进程的线程信息,并按 CPU 使用率排序(输入大写 P
):
top -Hp <pid>
发现某些线程的 CPU 使用率高达 99.9%。
2.3 导出线程栈信息
为了进一步分析,使用 jstack
命令将线程栈信息导出到日志文件中:
jstack <pid> > pid.log
2.4 分析线程栈
在 99.9% CPU 使用率的线程中,随机选择一个线程(例如 pid=194283
),将其转换为 16 进制(2f6eb
),并在线程快照中查找对应的线程信息。
发现这些线程都与 Disruptor 队列相关,且都在执行 java.lang.Thread.yield
方法。
2.5 使用分析工具
为了更直观地查看线程状态,将线程快照信息上传到 fastthread.io 进行分析。分析结果显示,几乎所有消耗 CPU 的线程都与 Disruptor 队列相关,且都在执行 yield
方法。
2.6 初步判断
初步判断,大量线程执行 yield
方法后,互相竞争导致 CPU 使用率增高。通过对堆栈的分析,发现确实与 Disruptor 有关。
3 Disruptor 使用方式
3.1 引入依赖
在 pom.xml
文件中引入 Disruptor 的依赖:
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>
3.2 定义事件
定义事件 LongEvent
:
public static class LongEvent {
private long value;
public void set(long value) {
this.value = value;
}
@Override
public String toString() {
return "LongEvent{value=" + value + '}';
}
}
3.3 定义事件工厂
定义事件工厂 LongEventFactory
:
public static class LongEventFactory implements EventFactory<LongEvent> {
@Override
public LongEvent newInstance() {
return new LongEvent();
}
}
3.4 定义事件处理器
定义事件处理器 LongEventHandler
:
public static class LongEventHandler implements EventHandler<LongEvent> {
@Override
public void onEvent(LongEvent event, long sequence, boolean endOfBatch) {
System.out.println("Event: " + event);
}
}
3.5 定义事件发布者
定义事件发布者:
public static void main(String[] args) throws InterruptedException {
// 指定 Ring Buffer 的大小
int bufferSize = 1024;
// 构建 Disruptor
Disruptor<LongEvent> disruptor = new Disruptor<>(
new LongEventFactory(),
bufferSize,
Executors.defaultThreadFactory());
// 连接事件处理器
disruptor.handleEventsWith(new LongEventHandler());
// 启动 Disruptor
disruptor.start();
// 获取 Ring Buffer
RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();
// 生产事件
ByteBuffer bb = ByteBuffer.allocate(8);
for (long l = 0; l < 100; l++) {
bb.putLong(0, l);
ringBuffer.publishEvent((event, sequence, buffer) -> event.set(buffer.getLong(0)), bb);
Thread.sleep(1000);
}
// 关闭 Disruptor
disruptor.shutdown();
}
简单解释下:
LongEvent
:这是要通过 Disruptor 传递的数据或事件。
LongEventFactory
:用于创建事件对象的工厂类。
LongEventHandler
:事件处理器,定义了如何处理事件。
Disruptor
构建:创建了一个 Disruptor 实例,指定了事件工厂、缓冲区大小和线程工厂。
事件发布:示例中演示了如何发布事件到 Ring Buffer。
运行结果如下:
4 问题定位与优化
4.1 问题定位
通过代码审查,发现每个业务场景内部都使用了 2 个 Disruptor 队列来解耦。假设有 7 个业务,则创建了 14 个 Disruptor 队列,每个队列有一个消费者,总共 14 个消费者。配置的消费等待策略为 YieldingWaitStrategy
,这种策略会执行 yield
来让出 CPU。
4.2 本地模拟
为了验证问题,在本地创建了 15 个 Disruptor 队列,并结合监控观察 CPU 使用情况。发现 CPU 使用率确实很高,且线程状态与生产环境一致。
注意看代码 YieldingWaitStrategy:
以及事件处理器:
创建了 15 个 Disruptor 队列,同时每个队列都用线程池来往 Disruptor队列 里面发送 100W 条数据。消费程序仅仅只是打印一下。
跑了一段时间,发现 CPU 使用率确实很高。
同时 dump 线程发现和生产环境中的现象也是一致的:消费线程都处于 RUNNABLE 状态,同时都在执行 yield。
4.3 调整等待策略
通过查询 Disruptor 官方文档,发现 YieldingWaitStrategy
适用于消费线程数量小于 CPU 核心数的情况。而当前场景中,消费线程数远超过 CPU 核心数。因此,将等待策略调整为 BlockingWaitStrategy
,发现 CPU 使用率明显降低。
运行后的结果如下:
dump 线程后,发现大部分线程都处于 waiting 状态。
4.4 进一步优化
将 Disruptor 队列调整为 1 个,并保持 YieldingWaitStrategy
策略,发现 CPU 使用率保持平稳。
5 优化方案
- 调整等待策略:将等待策略从
YieldingWaitStrategy
调整为BlockingWaitStrategy
,以降低 CPU 使用率。 - 应用拆分:将现有业务拆分为多个应用,每个应用处理一种业务类型,分别部署,以实现隔离。
- 线程池优化:调整线程池配置,减少线程数量,避免空闲线程占用资源。
6 总结
通过本次排查,我们发现 CPU 使用率过高的问题与 Disruptor 的等待策略和线程数量有关。通过调整等待策略和应用拆分,可以有效降低 CPU 使用率。希望本次排查思路能为大家提供一些启发。
7 思维导图
8 参考链接
一次生产环境中 CPU 占用 100% 排查优化实践