Java應(yīng)用程序CPU100%問題排查優(yōu)化實戰(zhàn)
Java 應(yīng)用程序CPU 100%問題排查優(yōu)化實戰(zhàn)
今天再給大家講一個 CPU 100% 優(yōu)化排查實戰(zhàn)。
收到運維同學(xué)的報警,說某些服務(wù)器負載非常高,讓我們開發(fā)定位問題。拿到問題后先去服務(wù)器上看了看,發(fā)現(xiàn)運行的只有我們的 Java 應(yīng)用程序。于是先用 ps 命令拿到了應(yīng)用的 PID。
ps:查看進程的命令;PID:進程 ID。ps -ef | grep java 可以查看所有的 Java 進程。前面也曾講過。
接著使用 top -Hp pid 將這個進程的線程顯示出來。輸入大寫 P 可以將線程按照 CPU 使用比例排序,于是得到以下結(jié)果。

果然,某些線程的 CPU 使用率非常高,99.9% 可不是非常高嘛(??)。
為了方便問題定位,我立馬使用 jstack pid > pid.log 將線程棧 dump 到日志文件中。關(guān)于 jstack 命令,我們前面剛剛講過。
我在上面 99.9% 的線程中隨機選了一個 pid=194283 的,轉(zhuǎn)換為 16 進制(2f6eb)后在線程快照中查詢:

線程快照中線程 ID 都是16進制的。
發(fā)現(xiàn)這是 Disruptor 的一個堆棧,好家伙,這不前面剛遇到過嘛,老熟人啊, 強如 Disruptor 也發(fā)生內(nèi)存溢出?
真沒想到,再來一次!
為了更加直觀的查看線程的狀態(tài),我將快照信息上傳到了專門的分析平臺上:http://fastthread.io/,估計有球友用過。

其中有一項展示了所有消耗 CPU 的線程,我仔細看了下,發(fā)現(xiàn)幾乎都和上面的堆棧一樣。
也就是說,都是 Disruptor 隊列的堆棧,都在執(zhí)行 java.lang.Thread.yield。
眾所周知,yield 方法會暗示當(dāng)前線程讓出 CPU 資源,讓其他線程來競爭(多線程的時候我們講過 yield,相信大家還有印象)。
根據(jù)剛才的線程快照發(fā)現(xiàn),處于 RUNNABLE 狀態(tài)并且都在執(zhí)行 yield 的線程大概有 30幾個。
初步判斷,大量線程執(zhí)行 yield 之后,在互相競爭導(dǎo)致 CPU 使用率增高,通過對堆棧的分析可以發(fā)現(xiàn),確實和 Disruptor 有關(guān)。
好家伙,又是它。
既然如此,我們來大致看一下 Disruptor 的使用方式吧。看有多少球友使用過。
第一步,在 pom.xml 文件中引入 Disruptor 的依賴:
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>
第二步,定義事件 LongEvent:
public static class LongEvent {
private long value;
public void set(long value) {
this.value = value;
}
@Override
public String toString() {
return "LongEvent{value=" + value + '}';
}
}
第三步,定義事件工廠:
// 定義事件工廠
public static class LongEventFactory implements EventFactory<LongEvent> {
@Override
public LongEvent newInstance() {
return new LongEvent();
}
}
第四步,定義事件處理器:
// 定義事件處理器
public static class LongEventHandler implements EventHandler<LongEvent> {
@Override
public void onEvent(LongEvent event, long sequence, boolean endOfBatch) {
System.out.println("Event: " + event);
}
}
第五步,定義事件發(fā)布者:
public static void main(String[] args) throws InterruptedException {
// 指定 Ring Buffer 的大小
int bufferSize = 1024;
// 構(gòu)建 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();
// 生產(chǎn)事件
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);
}
// 關(guān)閉 Disruptor
disruptor.shutdown();
}
簡單解釋下:
- LongEvent:這是要通過 Disruptor 傳遞的數(shù)據(jù)或事件。
- LongEventFactory:用于創(chuàng)建事件對象的工廠類。
- LongEventHandler:事件處理器,定義了如何處理事件。
- Disruptor 構(gòu)建:創(chuàng)建了一個 Disruptor 實例,指定了事件工廠、緩沖區(qū)大小和線程工廠。
- 事件發(fā)布:示例中演示了如何發(fā)布事件到 Ring Buffer。
大家可以運行看一下輸出結(jié)果。
解決問題
我查了下代碼,發(fā)現(xiàn)每一個業(yè)務(wù)場景在內(nèi)部都會使用 2 個 Disruptor 隊列來解耦。
假設(shè)現(xiàn)在有 7 個業(yè)務(wù),那就等于創(chuàng)建了 2*7=14 個 Disruptor 隊列,同時每個隊列有一個消費者,也就是總共有 14 個消費者(生產(chǎn)環(huán)境更多)。
同時發(fā)現(xiàn)配置的消費等待策略為 YieldingWaitStrategy,這種等待策略會執(zhí)行 yield 來讓出 CPU。代碼如下:

初步來看,和等待策略有很大的關(guān)系。
本地模擬
為了驗證,我在本地創(chuàng)建了 15 個 Disruptor 隊列,同時結(jié)合監(jiān)控觀察 CPU 的使用情況。
注意看代碼 YieldingWaitStrategy:

以及事件處理器:

創(chuàng)建了 15 個 Disruptor 隊列,同時每個隊列都用線程池來往 Disruptor隊列 里面發(fā)送 100W 條數(shù)據(jù)。消費程序僅僅只是打印一下。

跑了一段時間,發(fā)現(xiàn) CPU 使用率確實很高。

同時 dump 線程發(fā)現(xiàn)和生產(chǎn)環(huán)境中的現(xiàn)象也是一致的:消費線程都處于 RUNNABLE 狀態(tài),同時都在執(zhí)行 yield。
通過查詢 Disruptor 官方文檔發(fā)現(xiàn):

YieldingWaitStrategy 是一種充分壓榨 CPU 的策略,使用自旋 + yield的方式來提高性能。當(dāng)消費線程(Event Handler threads)的數(shù)量小于 CPU 核心數(shù)時推薦使用該策略。

同時查到其他的等待策略,比如說 BlockingWaitStrategy (也是默認的策略),使用的是鎖的機制,對 CPU 的使用率不高。
于是我將等待策略調(diào)整為 BlockingWaitStrategy。

運行后的結(jié)果如下:

和剛才的結(jié)果對比,發(fā)現(xiàn) CPU 的使用率有明顯的降低;同時 dump 線程后,發(fā)現(xiàn)大部分線程都處于 waiting 狀態(tài)。

優(yōu)化解決
看樣子,將等待策略換為 BlockingWaitStrategy 可以減緩 CPU 的使用,不過我留意到官方對 YieldingWaitStrategy 的描述是這樣的:
當(dāng)消費線程(Event Handler threads)的數(shù)量小于 CPU 核心數(shù)時推薦使用該策略。
而現(xiàn)在的使用場景是,消費線程數(shù)已經(jīng)大大的超過了核心 CPU 數(shù),因為我的使用方式是一個 Disruptor 隊列一個消費者,所以我將隊列調(diào)整為 1 個又試了試(策略依然是 YieldingWaitStrategy)。

查看運行效果:

跑了一分鐘,發(fā)現(xiàn) CPU 的使用率一直都比較平穩(wěn)。
小結(jié)
排查到此,可以得出結(jié)論了,想要根本解決這個問題需要將我們現(xiàn)有的業(yè)務(wù)拆分;現(xiàn)在是一個應(yīng)用里同時處理了 N 個業(yè)務(wù),每個業(yè)務(wù)都會使用好幾個 Disruptor 隊列。
由于在一臺服務(wù)器上運行,所以就會導(dǎo)致 CPU 的使用率居高不下。
由于是老系統(tǒng),所以我們的調(diào)整方式如下:
先將等待策略調(diào)整為 BlockingWaitStrategy,可以有效降低 CPU 的使用率(業(yè)務(wù)上也還能接受)。第二步就需要將應(yīng)用拆分,一個應(yīng)用處理一種業(yè)務(wù)類型;然后分別部署,這樣可以互相隔離互不影響。
當(dāng)然還有一些其他的優(yōu)化,比如說這次 dump 發(fā)現(xiàn)應(yīng)用程序創(chuàng)建了 800+ 個線程。創(chuàng)建線程池的方式也是核心線程數(shù)和最大線程數(shù)一樣,就導(dǎo)致一些空閑的線程得不到回收。應(yīng)該將創(chuàng)建線程池的方式調(diào)整一下,將線程數(shù)降下來,盡量物盡其用。
好,生產(chǎn)環(huán)境中,一般也就是會遇到 OOM 和 CPU 這兩個問題,那也希望這種排查思路能夠給大家一些啟發(fā)~
以上就是Java應(yīng)用程序CPU100%問題排查優(yōu)化實戰(zhàn)的詳細內(nèi)容,更多關(guān)于Java應(yīng)用程序CPU100%的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Idea連接數(shù)據(jù)庫并執(zhí)行SQL語句的方法示例
這篇文章主要介紹了Idea連接數(shù)據(jù)庫并執(zhí)行SQL語句的方法示例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
解決ResourceBundle.getBundle文件路徑問題
這篇文章主要介紹了解決ResourceBundle.getBundle文件路徑問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01
通過Spring Boot配置動態(tài)數(shù)據(jù)源訪問多個數(shù)據(jù)庫的實現(xiàn)代碼
這篇文章主要介紹了通過Spring Boot配置動態(tài)數(shù)據(jù)源訪問多個數(shù)據(jù)庫的實現(xiàn)代碼,需要的朋友可以參考下2018-03-03
idea手動執(zhí)行maven命令的三種實現(xiàn)方式
這篇文章主要介紹了idea手動執(zhí)行maven命令的三種實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08
Java 并發(fā)編程面試題Future 模式及實現(xiàn)方法
FutureTask是Future接口的一個實現(xiàn),常與Callable一起使用,CompletableFuture是Java8引入的,擴展了Future的功能,支持異步任務(wù)的編排和組合,提供了更強大的函數(shù)式編程能力,這篇文章主要介紹了Java 并發(fā)編程面試題Future 模式及實現(xiàn)方法,需要的朋友可以參考下2025-04-04

