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)文章
Java?synchronized關(guān)鍵字性能考量及優(yōu)化探索
這篇文章主要為大家介紹了Java?synchronized關(guān)鍵字性能考量及優(yōu)化探索示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12Springboot自定義注解&傳參&簡單應(yīng)用方式
SpringBoot框架中,通過自定義注解結(jié)合AOP可以實現(xiàn)功能如日志記錄與耗時統(tǒng)計,首先創(chuàng)建LogController和TimeConsuming注解,并為LogController定義參數(shù),然后,在目標方法上應(yīng)用這些注解,最后,使用AspectJ的AOP功能,通過切點表達式定位這些注解2024-10-10Java中String、StringBuffer和StringBuilder的區(qū)別
這篇文章主要介紹了Java中String、StringBuffer和StringBuilder的區(qū)別,StringBuilder與StringBuffer都繼承自AbstractStringBuilder類,在AbstractStringBuilder中也是使用字符數(shù)組保存字符串char[]value但是沒有final關(guān)鍵字修飾,所以這兩個可變,需要的朋友可以參考下2024-01-01SpringBoot整合Mybatis-Plus實現(xiàn)微信注冊登錄的示例代碼
微信是不可或缺的通訊工具,本文主要介紹了SpringBoot整合Mybatis-Plus實現(xiàn)微信注冊登錄的示例代碼,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的可以了解一下2024-02-02