Java 程序員的 Linux 性能調(diào)優(yōu)寶典:三大經(jīng)典場景深度剖析
本文整理了Linux系統(tǒng)性能問題排查的通用方法論和實(shí)踐,將針對以下三個經(jīng)典場景展開探討:
- I/O性能瓶頸
- CPU飆升
- 偶發(fā)CPU飆升
同時考慮到筆者文章的受眾面大部分都是Java開發(fā)人員,所以復(fù)現(xiàn)問題故障的例子也都采用Java進(jìn)行編碼部署復(fù)現(xiàn),對應(yīng)的示例也都會在案例排查的最后展開說明。

一、應(yīng)用程序延遲升高
第一個案例是用戶反饋系統(tǒng)延遲升高,網(wǎng)卡打開緩慢。從開發(fā)者的角度一定要明白,所有表現(xiàn)為卡頓、延遲的原因很大概率是系統(tǒng)資源吃緊,只有在資源分配不足的情況下,才會導(dǎo)致程序運(yùn)行阻塞,無法及時處理用戶的請求。
關(guān)于服務(wù)器的閾值指標(biāo),按照業(yè)界通用的經(jīng)驗(yàn),對應(yīng)CPU和內(nèi)存的負(fù)載要求的合理上限為:
- CPU使用率控制在75%左右
- 內(nèi)存使用率控制在80%以內(nèi)
- 虛擬內(nèi)存盡可能保持在0%
- 負(fù)載不超過CPU核心數(shù)的75%
筆者一般會先通過top查看操作系統(tǒng)的CPU利用率,這里筆者因?yàn)槭莻€人服務(wù)器原因則采用更強(qiáng)大、更直觀的htop查看個人服務(wù)器的資源使用情況,對應(yīng)的安裝指令如下:
sudo apt update
sudo apt install htop而本次htop輸出的指標(biāo)如下:
- 服務(wù)器為6核,對應(yīng)的CPU使用率分別是3.9、0、0、2.7、0.7、1.4,按照業(yè)界的通用標(biāo)準(zhǔn),當(dāng)前服務(wù)器各核心CPU使用率較低,但需結(jié)合系統(tǒng)負(fù)載綜合判斷
- Mem代表了內(nèi)存使用率,內(nèi)存一般情況下是用于存儲程序代碼和數(shù)據(jù),分為物理內(nèi)存和虛擬內(nèi)存,物理內(nèi)存顯示內(nèi)存接近8G僅用了1G不到,使用率不到80%,說明資源冗余
- Swp顯示交換空間即虛擬內(nèi)存的使用情況,可以看到也僅僅用了32M,并沒有大量的內(nèi)存數(shù)據(jù)被置換到交換空間,結(jié)合第2點(diǎn)來看,內(nèi)存資源充足
- Tasks顯示進(jìn)程數(shù)和線程數(shù)一共有35個進(jìn)程,這35個進(jìn)程對應(yīng)100個線程處理,Kthr顯示指標(biāo)為0說明有0個內(nèi)核線程,而Running為1說明有一個用戶進(jìn)程在運(yùn)行
- 而系統(tǒng)平均負(fù)載近1分鐘為4.96,按照業(yè)界標(biāo)準(zhǔn)CPU核心數(shù)*0.75作為系統(tǒng)負(fù)載的運(yùn)算閾值,如果超過這個值則說明系統(tǒng)處于高負(fù)載狀態(tài),很明顯我們的6核服務(wù)器系統(tǒng)負(fù)載偏高了
綜合來看,服務(wù)器系統(tǒng)負(fù)載偏高但各CPU核心使用率較低,結(jié)合內(nèi)存使用情況,問題可能出現(xiàn)在I/O資源等待上,此時我們就要從I/O資源角度進(jìn)一步排查問題:

我們從I/O資源排查入手,通過vmstat 1執(zhí)行每秒一次的監(jiān)控指標(biāo)輸出,以筆者的服務(wù)器為例,可以看到如下幾個指標(biāo):
- r:按照文檔解釋為The number of runnable processes (running or waiting for run time)即正在運(yùn)行或等待運(yùn)行的進(jìn)程數(shù),如果大于CPU核心數(shù)則說明CPU處于過載狀態(tài),而當(dāng)前服務(wù)器這個值為0,說明隊(duì)列處理狀態(tài)良好
- b::按照文檔解釋為The number of processes blocked waiting for I/O to complete即等待I/O完成的進(jìn)程數(shù),從參數(shù)b可以看出有大量進(jìn)程等待I/O,說明當(dāng)前服務(wù)器存在I/O瓶頸。
- swpd:the amount of swap memory used即交換空間也就是虛擬內(nèi)存的使用,而當(dāng)前服務(wù)器已被使用30468說明存在緩存置換,由此參數(shù)結(jié)合buff(緩存中尚未寫入磁盤的內(nèi)容)和cache(從磁盤加載出來的緩存數(shù)據(jù))來看,當(dāng)前內(nèi)存資源持續(xù)升高,存在讀寫虛擬內(nèi)存的情況,存在I/O性能瓶頸。
- 從bo來看有大量任務(wù)進(jìn)行每秒寫塊
- 針對CPU一個板塊輸出的us(用戶代碼執(zhí)行時間)、sy(內(nèi)核執(zhí)行時間)、id(空閑時間)、wa(等待I/O的時間),其中wa即等待I/O的時間持續(xù)處于一個高數(shù)值的狀態(tài),更進(jìn)一步明確CPU在空轉(zhuǎn),等待I/O完成,而I/O資源處于吃緊的狀態(tài)

考慮為I/O資源瓶頸,我們優(yōu)先從網(wǎng)絡(luò)I/O角度排查問題,這里筆者采用nload進(jìn)行網(wǎng)絡(luò)資源診斷,如果沒有下載的可以自行通過yum或者apt的方式自行下載,這里筆者也貼出ubuntu服務(wù)器的下載指令:
# ubuntu下載安裝nload
sudo apt install nload -y鍵入nload實(shí)時輸出網(wǎng)絡(luò)帶寬的使用情況,可以看到:
- 輸入流量(incoming)即下載流量,當(dāng)前網(wǎng)速基本控制在1KB,僅在最大網(wǎng)速的20%左右,一般認(rèn)為只有當(dāng)前網(wǎng)速無限接近于最大網(wǎng)速才可認(rèn)為帶寬使用率接近飽和
- 輸出流量(outgoing)即上傳流量,同理當(dāng)前也僅僅使用8%,也未達(dá)到飽和的閾值
所以I/O資源吃緊的問題原因并非網(wǎng)絡(luò)I/O,我們需要進(jìn)一步從服務(wù)器磁盤I/O角度進(jìn)一步排查:

所以從這些指標(biāo)來看,存在大量的線程在等待I/O資源的分配而進(jìn)入阻塞,所以筆者基于iostat -x 1使每一秒都輸出更詳細(xì)的信息,可以看到sdd盤對應(yīng)的磁盤忙碌百分比util基本跑滿100%,已基本接近飽和,此時基本是確認(rèn)有大量線程在進(jìn)行I/O讀寫任務(wù)了,且查看I/O讀寫指標(biāo):
- 每秒讀寫的吞吐量w/s為175
- 每秒寫入wkB/s的172704KB
- SSD盤util即I/O資源利用率為100%,已經(jīng)遠(yuǎn)超業(yè)界閾值60%,說明存在I/O性能瓶頸,需要補(bǔ)充說明的是當(dāng)CPU100%時進(jìn)程調(diào)度會因?yàn)椴僮飨到y(tǒng)優(yōu)先級設(shè)置的原因并不會導(dǎo)致進(jìn)程阻塞,但是I/O設(shè)備則不同,它不能區(qū)分優(yōu)先級進(jìn)行I/O中斷響應(yīng),所以這個數(shù)值高的情況下就會使得大量I/O請求阻塞
- 寫請求的平均等待時間w_await為5151ms
換算下來172704KB/175每秒寫入的速率為987KB每秒,由此可確定因?yàn)榇疟P性能讀寫性能瓶頸導(dǎo)致大量I/O讀寫任務(wù)阻塞,進(jìn)而導(dǎo)致服務(wù)器卡頓,用戶體驗(yàn)差:

所以,對于系統(tǒng)延遲嚴(yán)重的情況,整體排查思路為:
- 通過top指令查看CPU使用率,若正常進(jìn)入步驟2
- 基于vmstat查看內(nèi)存使用率和I/O資源情況
- 基于nload查看網(wǎng)絡(luò)I/O使用情況
- 基于iostat查看網(wǎng)絡(luò)I/O和磁盤I/O情況最終確認(rèn)問題
本例子的最后筆者也給出本次故障問題的示例代碼:
/**
* 啟動磁盤I/O操作以模擬高I/O負(fù)載
* 通過創(chuàng)建多個I/O任務(wù)線程來模擬高磁盤I/O負(fù)載
*/
private static void startDiskIOOperations() {
log.info("開始高I/O磁盤操作...");
log.info("在另一個終端中運(yùn)行 'iostat -x 1' 來監(jiān)控磁盤利用率。");
// 創(chuàng)建固定線程數(shù)的線程池
executor = Executors.newFixedThreadPool(NUM_THREADS);
// 提交多個任務(wù)以連續(xù)寫入磁盤
for (int i = 0; i < NUM_THREADS; i++) {
executor.submit(new IOTask(i));
}
log.info("磁盤I/O操作已啟動,使用 {} 個線程", NUM_THREADS);
}
/**
* 執(zhí)行連續(xù)寫入操作以模擬高I/O的任務(wù)
* 該類負(fù)責(zé)執(zhí)行磁盤I/O操作,通過不斷寫入和清空文件來模擬高I/O負(fù)載
*/
static class IOTask implements Runnable {
private final int taskId;
public IOTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
// 每個線程寫入自己的臨時文件
String filename = "/tmp/disk_io_test_" + taskId + ".tmp";
try (FileOutputStream fos = new FileOutputStream(filename)) {
log.info("線程-{} 正在寫入 {}", taskId, filename);
// 連續(xù)將數(shù)據(jù)寫入文件并在每次寫入后清空文件
while (!Thread.currentThread().isInterrupted()) {
performDiskIOOperation(fos, taskId);
ThreadUtil.sleep(500);
}
} catch (IOException e) {
log.error("線程-{} 發(fā)生錯誤: {}", taskId, e.getMessage());
}
}
}二、系統(tǒng)操作卡頓
第二個例子也同樣是用戶反饋系統(tǒng)操作卡頓感嚴(yán)重,整體點(diǎn)擊響應(yīng)非常慢,我們還是考慮資源吃緊,優(yōu)先使用top指令查看資源使用情況,從top指令來看:
- 輸出us查看各個核心的CPU使用率跑滿
- 系統(tǒng)平均負(fù)載基本超過70%(6*0.7)已經(jīng)超過4.2
這就是經(jīng)典的計(jì)算密集型任務(wù)跑滿所有線程的經(jīng)典例子:

一般針對CPU跑滿的問題,筆者一般會通過mpstat -P ALL 1查看CPU時間片是否分配均衡,是否出現(xiàn)偏斜導(dǎo)致CPU過熱的情況,例如所有運(yùn)算任務(wù)都往一個CPU核心上跑,經(jīng)過筆者每秒1次的輸出持續(xù)觀察來看,整體資源吃緊,但并沒有出現(xiàn)資源分配偏斜的情況,同時內(nèi)存資源使用率也不高,也沒有大量的iowait等待:

結(jié)合第一步top指令定位到的進(jìn)程是Java進(jìn)程,筆者索性通過Arthas直接定位故障代碼,首先通過thread鎖定問題線程,可以看到pool-前綴的線程基本都是跑滿單個CPU,所以我們就可以通過thread id查看線程的棧幀:

最終鎖定到了這段代碼段,即一個密集的循環(huán)運(yùn)算的線程:

對應(yīng)的筆者也貼出故障代碼段示例,來總結(jié)一下系統(tǒng)使用卡頓的排查思路:
- 基本top查看用戶態(tài)和內(nèi)核態(tài)CPU使用率
- 用戶態(tài)使用率偏高,通過mpstat查看CPU使用是否偏斜,是否保證CPU親和力
- 如果CPU使用沒有出現(xiàn)偏斜,則直接通過問題定位到Java進(jìn)程,結(jié)合Arthas快速定位問題線程進(jìn)行診斷
/**
* 模擬CPU使用率過高的情況
* 通過創(chuàng)建多個CPU密集型任務(wù)線程來模擬高CPU使用率
*/
public static void startHighCPUUsage() {
log.info("開始模擬高CPU使用率...");
// 創(chuàng)建CPU密集型任務(wù)的線程池
ExecutorService cpuExecutor = Executors.newFixedThreadPool(NUM_THREADS);
// 提交多個CPU密集型任務(wù)
for (int i = 0; i < NUM_THREADS; i++) {
cpuExecutor.submit(new CPUIntensiveTask(i));
}
log.info("高CPU使用率模擬已啟動,使用 {} 個線程", NUM_THREADS);
}
/**
* CPU密集型任務(wù),用于模擬高CPU使用率
* 該類通過執(zhí)行復(fù)雜的數(shù)學(xué)計(jì)算來占用CPU資源,從而模擬高CPU使用率場景
*/
static class CPUIntensiveTask implements Runnable {
private final int taskId;
public CPUIntensiveTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
log.info("CPU密集型任務(wù)-{} 已啟動", taskId);
// 執(zhí)行CPU密集型計(jì)算以提高CPU使用率
while (!Thread.currentThread().isInterrupted()) {
// 執(zhí)行一些復(fù)雜的數(shù)學(xué)計(jì)算
double result = 0;
for (int i = 0; i < 1000000; i++) {
result += Math.sqrt(Math.log(i + 1) * Math.cos(i) * Math.sin(i));
}
log.debug("CPU任務(wù)-{} 完成一輪計(jì)算,結(jié)果: {}", taskId, result);
}
log.info("CPU密集型任務(wù)-{} 已結(jié)束", taskId);
}
}三、持續(xù)的偶發(fā)性系統(tǒng)卡頓問題排查
此類問題比較棘手,系統(tǒng)偶發(fā)卡頓意味著是瞬時、頻繁的資源吃緊,我們還是直接使用top指令無法明確立刻捕捉到進(jìn)程,可能剛剛一看到飆升的進(jìn)程就消失了。
同理使用mpstat、vmstat指令無法準(zhǔn)確定位到超短期飆升問題的進(jìn)程,而基于iostat也沒有看到明顯的I/O資源吃緊,所以我們可以采用perf指令解決問題,以筆者的Ubuntu服務(wù)器為例,對應(yīng)的安裝步驟:
# 安裝perf工具
sudo apt install linux-tools-generic
sudo apt install linux-tools-common在筆者完成安裝并啟動之后,系統(tǒng)拋出WARNING: perf not found for kernel xxxx的異常,對應(yīng)的解決方案是要主動安裝linux-tools-generic并定位到linux-tools目錄下找到自己的generic版本完成符號鏈接,以筆者本次安裝為例就是6.8.0-79:
sudo ln -s /usr/lib/linux-tools/6.8.0-79-generic/perf /usr/bin/perf完成上述安裝之后,我們就可以通過將頻率降低設(shè)置為99并將監(jiān)控結(jié)果導(dǎo)出到tmp目錄下的perf.data中:
sudo perf record -F 99 -a -g -o /tmp/perf.data sleep 10可能很多讀者好奇為什么需要將頻率設(shè)置為99Hz,這樣做的目的是為了避免與系統(tǒng)定時器中斷頻率(通常為100Hz)同步,從而避免鎖步采樣導(dǎo)致的偏差。
鎖步采樣是指采樣頻率與系統(tǒng)定時器中斷頻率相同或成倍數(shù)關(guān)系時,采樣點(diǎn)會固定在相同的時間位置上,導(dǎo)致采樣結(jié)果不能準(zhǔn)確反映系統(tǒng)整體的性能狀況。
使用99Hz這樣的素?cái)?shù)頻率可以減少與系統(tǒng)周期性活動同步的概率,從而獲得更全面、更準(zhǔn)確的性能數(shù)據(jù)。
舉個簡單的例子,若我們試圖確定道路是否出現(xiàn)擁堵,且通過24h一次的抽檢查,那么當(dāng)前樣本就可能與交通保持一個平行的同步狀態(tài),例如:
- 交通車流情況在每天8點(diǎn)-12點(diǎn)擁堵,而我們的程序也是恰好在每天9點(diǎn)采集,那么它就會認(rèn)為交通情況異常擁堵
- 若每天14點(diǎn)進(jìn)行一次采集那么就避開了交通阻塞的高峰期則會得到一個相反的、也是不正確的結(jié)論
為了規(guī)避相同周期頻率導(dǎo)致的lockstep即鎖同步采樣,我們可以適當(dāng)降低頻率避免與交通周期時間同步,保證一天的數(shù)據(jù)能夠在一個周期內(nèi)被完整地采集到,而本例最好的做法就是將定時間隔改為23h,這樣一來每個23天的樣本周期就會得到一天中所有時間的數(shù)據(jù)就能做到全面地了解到交通情況:

等待片刻后perf指令就會將結(jié)果輸出到perf.data目錄下:
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.701 MB /tmp/perf.data (586 samples) ]此時,通過sudo perf report查看報(bào)告,可以看到一個pid為1115751的Java進(jìn)程對應(yīng)線程CPU使用率飆升到86,此時我們就可以基于這條信息到指定的進(jìn)程上查看該線程是否存在密集的運(yùn)算:

最后我們也給出本示例的問題代碼:
/**
* 模擬CPU瞬間飆高然后降低的情況
* 實(shí)現(xiàn)每10秒一次的CPU使用率飆高和降低循環(huán)(僅使用單核)
*/
public static void startCPUSpikeAndDrop() {
log.info("開始模擬CPU瞬間飆高然后降低...");
// 創(chuàng)建用于CPU飆高的線程池(僅使用單核)
ExecutorService spikeExecutor = Executors.newFixedThreadPool(1);
// 提交單個CPU密集型任務(wù)來制造飆高
spikeExecutor.submit(new CPUSpikeTask(0));
log.info("CPU瞬間飆高已啟動,使用 {} 個線程", 1);
// 每隔10秒切換CPU飆高狀態(tài),實(shí)現(xiàn)周期性飆高和降低
Thread spikeController = new Thread(() -> {
boolean isSpiking = true;
ExecutorService currentExecutor = spikeExecutor;
while (!Thread.currentThread().isInterrupted()) {
try {
// 等待10秒
Thread.sleep(10000);
if (isSpiking) {
// 停止當(dāng)前的CPU飆高任務(wù)
currentExecutor.shutdownNow();
log.info("CPU使用率已降低");
} else {
// 啟動新的CPU飆高任務(wù)
currentExecutor = Executors.newFixedThreadPool(1);
currentExecutor.submit(new CPUSpikeTask(0));
log.info("CPU使用率已飆高");
}
// 切換狀態(tài)
isSpiking = !isSpiking;
} catch (InterruptedException e) {
log.error("CPU飆高控制線程被中斷", e);
break;
}
}
});
spikeController.setDaemon(true);
spikeController.start();
}
/**
* CPU瞬間飆高任務(wù),用于模擬CPU瞬間飆高然后降低的情況
* 該類通過執(zhí)行密集的數(shù)學(xué)計(jì)算來模擬CPU使用率的瞬時飆高,并在指定時間后自動停止
*/
static class CPUSpikeTask implements Runnable {
private final int taskId;
public CPUSpikeTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
log.info("CPU瞬間飆高任務(wù)-{} 已啟動", taskId);
// 執(zhí)行空循環(huán)以提高CPU使用率
while (!Thread.currentThread().isInterrupted()) {
// 空循環(huán)消耗CPU
}
log.info("CPU瞬間飆高任務(wù)-{} 已結(jié)束", taskId);
}
}四、小結(jié)
本文針對應(yīng)用延遲、系統(tǒng)卡頓、偶發(fā)頻繁卡頓三種常見的系統(tǒng)故障給出通用普適的排查思路,整體來說此類問題歸根結(jié)底都是系統(tǒng)資源吃緊,需要找到飽和的資源結(jié)合代碼推測根源并制定修復(fù)策略,以本文為例,通用的排查思路都為:
- 基于top查看CPU、內(nèi)存、負(fù)載
- 若CPU未飽和則通過vmstat查看I/O資源使用情況
- 明確I/O瓶頸通過nload和iostat查詢是網(wǎng)絡(luò)I/O還是磁盤I/O
- 若上述排查都無果,且CPU負(fù)載偶發(fā)飆高,可通過perf并調(diào)整頻率監(jiān)控系統(tǒng)定位系統(tǒng)中異常運(yùn)行的資源
- 結(jié)合上述推斷結(jié)果查看是否是異常消耗,如果是則優(yōu)化代碼,反之結(jié)合情況增加硬件資源

























