:從系統(tǒng)到代碼的四步定位法)
1. 項目概述從一次線上告警說起那天下午監(jiān)控大屏上一個刺眼的紅色告警彈了出來“生產(chǎn)環(huán)境某核心服務(wù)CPU使用率持續(xù)超過90%”。整個團隊的心都提到了嗓子眼。這可不是普通的測試環(huán)境而是承載著每秒數(shù)千筆交易的核心Java應(yīng)用。告警就是命令我們立刻投入了戰(zhàn)斗。在接下來的半小時里我們像偵探一樣從宏觀的系統(tǒng)負(fù)載到具體的進程再到線程最后定位到一行有問題的代碼。這個過程就是一次標(biāo)準(zhǔn)的“進程CPU占用率過高”排查實戰(zhàn)。事后我把整個流程梳理成筆記它不僅僅是一套命令的堆砌更是一種層層遞進、抽絲剝繭的排查思維。無論你是運維工程師、開發(fā)人員還是系統(tǒng)管理員掌握這套流程都能讓你在面對類似問題時從慌亂變得從容快速恢復(fù)服務(wù)并找到根因。這篇筆記就是我結(jié)合那次實戰(zhàn)和多年經(jīng)驗為你總結(jié)的“CPU高占用排查指南”。2. 排查流程總覽與核心思路排查CPU問題最忌諱的就是一上來就扎進代碼里漫無目的地看。一個高效的排查流程必須是自頂向下、由表及里的。我們的核心思路可以概括為“四步定位法”系統(tǒng) - 進程 - 線程 - 代碼。這就像醫(yī)生看病先看整體生命體征系統(tǒng)負(fù)載再檢查是哪個器官出了問題哪個進程接著看器官里哪部分組織異常哪個線程最后進行病理分析哪行代碼。第一步系統(tǒng)級觀察。目的是確認(rèn)問題真實存在并排除由系統(tǒng)級因素如其他無關(guān)進程、硬件資源不足導(dǎo)致的整體負(fù)載高。工具主要是top或htop。第二步進程級定位。在確認(rèn)系統(tǒng)整體負(fù)載高后迅速定位到罪魁禍?zhǔn)住悄膫€或哪幾個進程消耗了絕大部分的CPU資源。這里top命令的交互模式或ps命令是主力。第三步線程級深挖。一個Java進程CPU高可能是其中某一個或幾個線程在“瘋狂工作”。我們需要進入進程內(nèi)部看看是哪些線程在“作祟”。top -Hp或jstack是這里的關(guān)鍵。第四步代碼級根因分析。結(jié)合線程堆棧信息和可能的性能剖析工具最終定位到有問題的代碼邏輯比如死循環(huán)、低效算法、鎖競爭等。這個流程是普適的無論是排查java進程、baidunetdiskunite進程還是wechatappex這類你不熟悉的進程方法論都是一致的。下面我們就拆解每一步的具體操作和心法。3. 第一步系統(tǒng)級宏觀觀察與初步判斷當(dāng)收到告警或發(fā)現(xiàn)系統(tǒng)卡頓時不要慌首先通過SSH或控制臺登錄到目標(biāo)服務(wù)器。第一個使用的命令永遠是top。3.1 使用 top 命令進行全局掃描在終端輸入top后你會看到一個動態(tài)刷新的界面。我們的注意力應(yīng)該集中在頭部幾行摘要信息上負(fù)載平均值load average例如load average: 1.05, 0.70, 0.80。這三個值分別代表過去1分鐘、5分鐘、15分鐘的系統(tǒng)平均負(fù)載。對于單核CPU持續(xù)高于1通常意味著有進程在排隊等待CPU對于多核比如4核則閾值相應(yīng)提高如4。如果1分鐘值遠高于15分鐘值說明負(fù)載在近期陡增是突發(fā)現(xiàn)象??傮wCPU使用率%Cpu(s)us用戶態(tài)運行用戶進程所占用的CPU時間百分比。這是我們排查應(yīng)用問題最關(guān)注的指標(biāo)。sy內(nèi)核態(tài)運行內(nèi)核進程所占用的CPU時間百分比。過高可能意味著系統(tǒng)調(diào)用頻繁或內(nèi)核有瓶頸。id空閑CPU空閑時間百分比。我們通常希望ussy高時id很低。如果us或sy長期高于80%甚至接近100%就明確證實了CPU資源已成為瓶頸。注意在虛擬化環(huán)境如k8s虛擬機中top看到的CPU使用率是相對于虛擬機分配到的vCPU的。如果宿主機資源爭搶嚴(yán)重虛擬機內(nèi)看到的id可能很高但應(yīng)用依然響應(yīng)慢這時需要結(jié)合宿主機監(jiān)控看。3.2 解讀進程列表并鎖定目標(biāo)top下半部分的進程列表默認(rèn)按CPU使用率降序排列。這是我們鎖定目標(biāo)的關(guān)鍵區(qū)域。PID進程ID進程的唯一標(biāo)識。USER進程所有者。可以幫助你快速判斷這是否是一個預(yù)期的系統(tǒng)進程或應(yīng)用進程。%CPU該進程占用CPU的百分比。這是最關(guān)鍵的列。立刻找到那個 %CPU 異常高的進程。一個健康的Java應(yīng)用在無負(fù)載時%CPU可能在0%~5%波動高負(fù)載時可能達到幾十甚至上百對于多核CPU可以超過100%比如800%代表占滿了8個核。COMMAND進程啟動命令。這里可以看到進程名例如java、nginx、mysqld等。如果是Java進程通常能看到包含主類的全路徑或jar包名。實操技巧在top界面中按P大寫可以強制按CPU使用率排序默認(rèn)即是。按M可以按內(nèi)存使用率排序有時高CPU伴隨高內(nèi)存可以輔助判斷。按1可以展開顯示每個邏輯CPU核心的使用情況對于判斷CPU使用是否均衡很有幫助。如果進程列表刷新太快看不清可以按s然后輸入一個數(shù)字如5將刷新間隔改為5秒。假設(shè)我們通過top發(fā)現(xiàn)一個PID為12345的Java進程其%CPU持續(xù)在250%左右而其他進程都很低。那么目標(biāo)進程就初步鎖定了PID12345。4. 第二步進程級深度剖析與信息收集鎖定高CPU進程后我們需要收集關(guān)于這個進程的更多詳細(xì)信息為下一步的線程分析做準(zhǔn)備。4.1 使用 ps 命令獲取進程快照top是動態(tài)視圖而ps能給我們一個靜態(tài)的快照方便記錄和分享。一個非常實用的命令組合是ps -eo pid,user,%cpu,%mem,command --sort-%cpu | head -20這個命令會列出所有進程的PID、用戶、CPU、內(nèi)存和命令并按CPU使用率降序排列顯示前20條。你可以從中再次確認(rèn)你的目標(biāo)進程是否“名列前茅”。要獲取某個特定進程比如PID 12345的詳細(xì)信息可以用ps -fp 12345或者更詳細(xì)的ps aux | grep 123454.2 進階工具與場景分析htop可以看作是top的增強版界面更友好支持鼠標(biāo)操作顏色區(qū)分樹狀顯示進程關(guān)系。如果你有安裝權(quán)限強烈推薦使用htop它能讓你更直觀地看到進程和線程。pidstat這是一個更專業(yè)的性能統(tǒng)計工具來自sysstat包。它可以按周期采樣特定進程的CPU、內(nèi)存、IO等數(shù)據(jù)對于需要持續(xù)監(jiān)控一段時間變化的場景非常有用。# 每2秒采樣一次針對PID 12345共采樣5次 pidstat -p 12345 2 5特殊場景思考終端進程啟動失敗: 啟動期間發(fā)生本機異常這類錯誤通常與進程啟動環(huán)境有關(guān)可能涉及終端模擬器、PTY配置等。如果這個失敗的進程反復(fù)嘗試啟動可能會短暫推高CPU但通常不會造成持續(xù)高占用。排查重點應(yīng)是解決啟動失敗的根本原因。挖礦進程被隱藏這是安全應(yīng)急場景。惡意挖礦進程往往會改名、隱藏其進程名或通過rootkit技術(shù)從ps/top列表中隱藏。此時不能完全信任top。需要結(jié)合系統(tǒng)整體負(fù)載load average異常高、網(wǎng)絡(luò)連接netstat發(fā)現(xiàn)異常外連、以及cat /proc/loadavg等底層命令綜合判斷。使用chkrootkit、rkhunter或終端輸入ls -la /proc/[0-9]*/exe查看進程真實路徑可能發(fā)現(xiàn)端倪。baidunetdiskunite進程或alibabasafe service進程這類廠商軟件的守護進程。首先確認(rèn)其是否為官方正常進程。有時它們由于Bug或資源爭搶可能導(dǎo)致CPU高。排查思路不變先定位然后根據(jù)其日志或官方文檔分析。5. 第三步線程級微觀洞察與熱點定位找到高CPU進程后真正的挑戰(zhàn)才開始。一個Java進程內(nèi)部有幾十甚至上百個線程我們需要找出是哪個線程在“瘋狂燃燒CPU”。5.1 使用 top 查看進程內(nèi)線程這是最快捷的方法。首先在top界面中按Shift H有些版本默認(rèn)已開啟線程模式。這會打開線程顯示模式進程列表會變成線程列表?;蛘吒苯拥姆绞绞鞘褂妹顃op -H -p 12345-H表示顯示線程-p 12345指定進程ID。這時top顯示的就是進程12345內(nèi)部的所有線程同樣按CPU排序。記下那個CPU占用最高的線程的PID注意這里顯示的是線程ID在Java里通常稱為nid我們記為TID例如12401。5.2 將線程ID轉(zhuǎn)換為十六進制Java的線程堆棧信息中線程ID是以十六進制表示的。而top或ps看到的是十進制。我們需要進行轉(zhuǎn)換。假設(shè)高CPU線程的TID是12401。printf “%x\n” 12401輸出會是3071。這個0x3071就是我們下一步在堆棧信息中要尋找的關(guān)鍵標(biāo)識。5.3 使用 jstack 獲取線程堆棧jstack是JDK自帶的工具用于打印Java進程的線程堆棧信息。這是定位代碼問題的“顯微鏡”。jstack 12345 /tmp/thread_dump_$(date %Y%m%d_%H%M%S).log這條命令將進程12345的線程堆棧輸出到/tmp目錄下的一個帶時間戳的文件中。強烈建議在問題發(fā)生時立即抓取多次如間隔10秒抓取2-3次通過對比可以更容易發(fā)現(xiàn)始終處于運行狀態(tài)的線程。打開堆棧文件搜索我們之前轉(zhuǎn)換得到的十六進制線程ID3071。你會找到類似這樣的段落“http-nio-8080-exec-1” #32 daemon prio5 os_prio0 tid0x00007f8b3820e800 nid0x3071 runnable [0x00007f8b1f7f9000] java.lang.Thread.State: RUNNABLE at com.example.app.ProblemClass.infiniteLoop(ProblemClass.java:25) at com.example.app.ProblemClass.run(ProblemClass.java:15) ...看nid0x3071對上了并且線程狀態(tài)是RUNNABLE最重要的是它告訴了我們代碼位置com.example.app.ProblemClass.infiniteLoop(ProblemClass.java:25)。問題很可能就出在這個類的第25行的一個循環(huán)或密集計算中。實操心得如果jstack執(zhí)行很慢或卡住可能是因為進程CPU太高JVM的 Safepoint 機制無法到達??梢試L試使用jstack -F強制打印但可能會使JVM停頓更久生產(chǎn)環(huán)境慎用。除了jstackjcmd是更現(xiàn)代的統(tǒng)一命令行工具功能類似jcmd 12345 Thread.print。對于electron應(yīng)用electron 渲染層向主進程發(fā)送信息其本質(zhì)是Node.js進程。你可以使用node的調(diào)試工具或llnode來分析但高CPU排查思路相通先找到Node進程再用top -H看線程用--inspect參數(shù)獲取分析剖面。6. 第四步代碼級根因分析與常見模式拿到問題線程的堆棧信息就像偵探拿到了關(guān)鍵證據(jù)。接下來就是分析代碼邏輯。高CPU的代碼根因通常有以下幾種模式6.1 無限循環(huán)或密集計算這是最直接的原因。堆棧會停留在某個循環(huán)或計算方法內(nèi)部。特征線程狀態(tài)持續(xù)為RUNNABLE堆棧頂部始終是同一個業(yè)務(wù)方法。解決檢查循環(huán)條件是否永遠為真或者算法復(fù)雜度是否在特定數(shù)據(jù)下爆炸如嵌套循環(huán)處理大數(shù)據(jù)集。6.2 鎖競爭激烈線程沒有在“計算”而是在“等待”或“爭搶”但top看到的可能是系統(tǒng)態(tài)CPU (sy) 偏高因為線程頻繁地在用戶態(tài)和內(nèi)核態(tài)之間切換進行系統(tǒng)調(diào)用以獲取鎖。特征可能看到多個線程狀態(tài)為BLOCKED或WAITING等待同一個鎖waiting on 0x0000000712345678。使用jstack多次采樣會發(fā)現(xiàn)線程在RUNNABLE搶鎖和BLOCKED之間切換。解決分析鎖的粒度考慮使用更細(xì)粒度的鎖、并發(fā)容器如ConcurrentHashMap或改用無鎖數(shù)據(jù)結(jié)構(gòu)。6.3 低效的IO或外部調(diào)用線程在等待網(wǎng)絡(luò)響應(yīng)或磁盤IO時狀態(tài)可能是WAITING(on object monitor) 或TIMED_WAITING但如果IO操作設(shè)置不當(dāng)如超時時間極短導(dǎo)致重試風(fēng)暴或者處理IO結(jié)果的回調(diào)函數(shù)中有密集計算也會導(dǎo)致高CPU。特征堆棧中可能包含Socket.read、HttpClient.execute、數(shù)據(jù)庫驅(qū)動方法等。結(jié)合iostat、netstat等命令查看系統(tǒng)IO和網(wǎng)絡(luò)狀態(tài)。解決優(yōu)化IO邏輯增加合理的超時和重試機制使用異步非阻塞IO如NIO減少線程等待。6.4 JVM自身活動在某些情況下高CPU可能是由JVM的GC線程或JIT編譯線程引起的。GC導(dǎo)致頻繁的Full GC會導(dǎo)致所有應(yīng)用線程暫停但GC線程自身會消耗大量CPU??梢酝ㄟ^jstat -gcutil 12345 1000每秒觀察GC情況如果FGCFull GC次數(shù)和FGCTFull GC時間快速上升同時CPU高則很可能是內(nèi)存問題觸發(fā)了GC風(fēng)暴。JIT編譯在應(yīng)用啟動后一段時間或觸發(fā)新的熱點代碼時JIT編譯線程會活躍可能導(dǎo)致短暫的CPU尖峰這通常是正?,F(xiàn)象。6.5 結(jié)合性能剖析工具對于更復(fù)雜的問題或者為了量化代碼中各個方法的熱度可以借助性能剖析工具。Arthas阿里開源的Java診斷神器。使用thread命令可以直接查看最忙的線程使用profiler命令可以生成火焰圖直觀展示CPU時間在方法調(diào)用上的分布。Async-Profiler一款低開銷的性能分析器可以生成非常精確的CPU或內(nèi)存火焰圖。VisualVM或JProfiler圖形化工具功能強大適合在開發(fā)或測試環(huán)境進行深度性能分析。火焰圖是分析CPU熱點最強大的工具之一。它自上而下顯示調(diào)用棧寬度代表消耗的CPU時間。最頂層的“平頂山”就是最熱點的代碼路徑一目了然。7. 實戰(zhàn)案例與排查技巧實錄讓我們復(fù)盤一個簡化版的真實案例串聯(lián)整個流程。場景線上訂單服務(wù)CPU使用率突然飆升到300%。系統(tǒng)觀察top命令顯示系統(tǒng)us占用超過80%load average的1分鐘值達到10機器為4核。一個名為order-service.jar的Java進程%CPU穩(wěn)定在280%。PID為8888。進程確認(rèn)ps -fp 8888確認(rèn)這是我們的訂單服務(wù)。線程定位top -H -p 8888發(fā)現(xiàn)一個TID為9999的線程持續(xù)占用約95%的CPU。轉(zhuǎn)換十六進制printf “%x\n” 9999得到270f。堆棧分析連續(xù)執(zhí)行兩次jstack 8888 /tmp/dump1.log和jstack 8888 /tmp/dump2.log。在兩個文件中搜索nid0x270f發(fā)現(xiàn)該線程狀態(tài)均為RUNNABLE堆棧頂部都指向同一個方法com.xxx.order.service.coupon.CouponCalculator.calculateBatch(List)。代碼根因查看CouponCalculator.calculateBatch代碼發(fā)現(xiàn)其中有一個針對用戶訂單列表的循環(huán)循環(huán)內(nèi)部又調(diào)用了另一個isEligible方法而該方法執(zhí)行了一個未使用索引的數(shù)據(jù)庫級聯(lián)查詢。當(dāng)批量處理用戶數(shù)增多時算法復(fù)雜度呈指數(shù)增長導(dǎo)致CPU暴增。臨時解決與優(yōu)化立即通過配置中心降級該批量計算功能CPU回落。長期優(yōu)化方案是為查詢添加緩存、優(yōu)化數(shù)據(jù)庫索引、將O(n2)的算法重構(gòu)為O(n log n)。常見問題排查表現(xiàn)象/問題可能原因排查命令/方向top顯示%CPU高但jstack看不到RUNNABLE的熱點線程1. GC 導(dǎo)致。2. JNI 本地代碼。3. 排查間隔中熱點轉(zhuǎn)移。1.jstat -gcutil觀察GC。2. 使用能分析本地棧的工具如perf或Async-Profiler。3. 縮短采樣間隔多次抓取。線程狀態(tài)多是BLOCKED或WAITING激烈的鎖競爭或資源等待。分析jstack輸出中l(wèi)ocked 0x...和waiting on 0x...指向的同一個對象找到持有鎖的線程。%sy系統(tǒng)態(tài)CPU異常高1. 大量的系統(tǒng)調(diào)用如頻繁的IO。2. 進程/線程上下文切換頻繁。1.strace -p PID跟蹤系統(tǒng)調(diào)用生產(chǎn)環(huán)境慎用性能影響大。2.vmstat 1查看cs上下文切換列是否過高。懷疑是隱藏進程如挖礦進程被 rootkit 隱藏。1. 檢查/proc目錄下的進程ID數(shù)量與ps列出的是否差異巨大。2. 使用unhide等工具掃描。3. 檢查計劃任務(wù) (crontab -l)、系統(tǒng)服務(wù) (systemctl list-units) 和啟動項。Java進程啟動失敗或崩潰如opencv導(dǎo)致進程崩潰或無法啟動 conpty1. 查看應(yīng)用日志、系統(tǒng)日志 (/var/log/messages或journalctl)。2. 檢查核心轉(zhuǎn)儲文件 (core dump)。3. 檢查環(huán)境變量、依賴庫 (ldd)。獨家避坑技巧保存現(xiàn)場在重啟“問題進程”前務(wù)必保存以下信息至少2-3份間隔數(shù)秒的jstack輸出、top -H截圖、vmstat和iostat的統(tǒng)計信息。這些是事后分析的唯一證據(jù)。對比分析法在問題發(fā)生前后分別對進程做一次jstack。用文本對比工具如diff比較看哪些線程是新出現(xiàn)的或狀態(tài)發(fā)生了集中變化。監(jiān)控要全面不要只監(jiān)控CPU。內(nèi)存、磁盤IO、網(wǎng)絡(luò)流量、GC日志、應(yīng)用業(yè)務(wù)指標(biāo)如QPS、耗時的聯(lián)動異常往往能給你更早、更準(zhǔn)確的預(yù)警。CPU高通常是結(jié)果而不是原因。理解工具局限jstack在極端高負(fù)載下可能失效。Arthas的thread命令在這種情況下往往更可靠。對于Go、Python等語言進程思路一致工具換為pprof、py-spy等即可。排查CPU問題本質(zhì)上是一個“觀察 - 假設(shè) - 驗證”的科學(xué)過程。這套流程筆記為你提供了系統(tǒng)的觀察工具和驗證手段。真正的功力在于根據(jù)看到的線索結(jié)合對自身系統(tǒng)架構(gòu)和代碼的理解做出最合理的假設(shè)并快速驗證它。每一次成功的排查不僅是解決問題的過程更是加深你對系統(tǒng)理解的過程。