CPU 100% 优化排查实战
CPU 100% 优化排查实战
1 问题背景
某服务器负载异常升高,经初步排查发现,服务器上仅运行着一个Java应用程序。技术人员随即展开了详细的排查和优化工作。
2 排查步骤
2.1 获取进程信息
首先使用ps
命令获取应用的PID:
ps -ef | grep java
2.2 查看线程 CPU 使用情况
使用top
命令查看该进程的线程信息,并按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%排查优化实践