
1. 先把 On-CPU 和 Off-CPU 這兩個(gè)詞掰開(kāi)揉碎性能分析這件事做得久了會(huì)發(fā)現(xiàn)一個(gè)很尷尬的現(xiàn)象很多人拿到一臺(tái)卡頓的機(jī)器第一反應(yīng)是 top、ps、然后開(kāi) perf 抓火焰圖看到 CPU 使用率不高、火焰圖也沒(méi)幾個(gè)尖峰就得出系統(tǒng)沒(méi)問(wèn)題的結(jié)論。但業(yè)務(wù)方還在催接口還是慢。問(wèn)題出在哪出在只看了線(xiàn)程跑在 CPU 上的那段時(shí)間忽略了線(xiàn)程不在 CPU 上的那段更漫長(zhǎng)的時(shí)間。Linux 性能分析里的 On-CPU 和 Off-CPU說(shuō)的就是把一個(gè)線(xiàn)程的完整生命周期切成兩半來(lái)看On-CPU 是線(xiàn)程真正占用處理器執(zhí)行指令的時(shí)間Off-CPU 是線(xiàn)程雖然活著、但沒(méi)有被處理器執(zhí)行的時(shí)間。這兩塊時(shí)間加起來(lái)才是一個(gè)請(qǐng)求從進(jìn)來(lái)到返回的真實(shí)墻鐘耗時(shí)。我做過(guò)的排查里至少有六成的性能問(wèn)題根子在 Off-CPU 上等鎖、等磁盤(pán) IO、等網(wǎng)絡(luò)回包、等定時(shí)器、等被喚醒調(diào)度。而大家習(xí)慣用的工具幾乎全是為 On-CPU 設(shè)計(jì)的。這就導(dǎo)致一個(gè)結(jié)構(gòu)性盲區(qū)——你越用 On-CPU 的工具越會(huì)把注意力放在計(jì)算熱點(diǎn)上越容易漏掉阻塞點(diǎn)。所以這篇內(nèi)容想聊的就是怎么把這兩半時(shí)間都量出來(lái)、怎么看、怎么改。適合已經(jīng)會(huì)用 top、perf 這類(lèi)基礎(chǔ)工具但遇到CPU 不高卻很慢的場(chǎng)景會(huì)卡住的運(yùn)維、后端開(kāi)發(fā)和 SRE也適合剛接觸 Linux 性能調(diào)優(yōu)、想建立一套完整分析框架的朋友。1.1 一個(gè)類(lèi)比餐廳后廚和等位區(qū)把服務(wù)器想象成一家餐廳CPU 核心就是廚師線(xiàn)程就是一道道待做的菜。On-CPU 時(shí)間相當(dāng)于廚師正在灶臺(tái)前顛勺的時(shí)間Off-CPU 時(shí)間是這道菜從點(diǎn)單到出餐之間廚師沒(méi)碰它的所有時(shí)間——可能是在等配菜送過(guò)來(lái)等 IO可能是灶臺(tái)被別的菜占著等鎖也可能是廚師被叫去處理別的單子、這道菜就擱在一邊調(diào)度延遲。只統(tǒng)計(jì)廚師顛勺的時(shí)長(zhǎng)你會(huì)覺(jué)得后廚效率挺高但顧客感受到的是從點(diǎn)單到上菜的完整等待。這兩者之間的差距就是 Off-CPU 分析要填的坑。理解了這層類(lèi)比后面所有的工具和指標(biāo)其實(shí)都是在回答兩個(gè)問(wèn)題廚師到底在哪個(gè)灶臺(tái)上花了多少時(shí)間On-CPU 熱點(diǎn)以及這道菜在沒(méi)被烹飪的時(shí)候究竟卡在哪一步Off-CPU 阻塞點(diǎn)。1.2 為什么只盯 On-CPU 會(huì)系統(tǒng)性誤判CPU 使用率是個(gè)比值分子是忙的時(shí)間分母是總時(shí)間。當(dāng)系統(tǒng)大量線(xiàn)程都在睡覺(jué)等待時(shí)這個(gè)比值天然就低看起來(lái)很閑。但這恰恰可能是最糟的狀態(tài)線(xiàn)程數(shù)開(kāi)了一堆每個(gè)都在等同一個(gè)資源吞吐上不去延遲還高。這類(lèi)問(wèn)題在 On-CPU 視角里幾乎隱身因?yàn)閴焊鶝](méi)有多少指令在跑。更麻煩的是一些常見(jiàn)結(jié)論會(huì)把人帶偏。比如CPU 使用率低說(shuō)明負(fù)載輕可以再壓一壓實(shí)際可能是下游某個(gè)依賴(lài)把請(qǐng)求全堵住了再比如火焰圖沒(méi)熱點(diǎn)所以代碼沒(méi)問(wèn)題實(shí)際瓶頸在 syscall 睡眠或者 futex 等待上采樣器根本沒(méi)采到什么棧。所以建立 On-CPU 和 Off-CPU 的雙視角不是為了多學(xué)幾個(gè)命令而是為了堵住這個(gè)判斷漏洞。1.3 把線(xiàn)程時(shí)間賬本拆開(kāi)一個(gè)線(xiàn)程從被創(chuàng)建到退出它的時(shí)間可以粗略拆成三塊Running在 CPU 上跑、Runnable就緒但在等 CPU、Sleeping睡眠等某個(gè)事件。Running 就是 On-CPU后兩者合起來(lái)是 Off-CPU。這里有個(gè)容易被忽視的細(xì)節(jié)Runnable 狀態(tài)本身也是一種浪費(fèi)它說(shuō)明 CPU 不夠分或者調(diào)度策略讓某些線(xiàn)程遲遲輪不上。很多偶發(fā)尖刺就是 Runnable 排隊(duì)造成的。把這三塊量化出來(lái)你就能給任何一次慢請(qǐng)求做一個(gè)耗時(shí)歸因表多少時(shí)間在算多少時(shí)間在等鎖多少時(shí)間在等 IO多少時(shí)間在等調(diào)度。歸因清楚了優(yōu)化方向基本就是明牌。接下來(lái)的章節(jié)我會(huì)先講 On-CPU 怎么采得準(zhǔn)再講 Off-CPU 怎么把等待棧抓出來(lái)最后給一個(gè)從現(xiàn)象到修復(fù)的完整推演。2. On-CPU 分析采樣、火焰圖與熱點(diǎn)定位On-CPU 分析的目標(biāo)很明確找出線(xiàn)程把時(shí)間花在了哪些代碼路徑上。主流做法是基于定時(shí)中斷的采樣sampling而不是插樁計(jì)數(shù)因?yàn)椴蓸訉?duì)性能影響小、能反映真實(shí)分布也不需要改代碼。采樣器每隔一個(gè)固定周期打斷一次 CPU抓下當(dāng)前的調(diào)用棧攢夠樣本數(shù)之后按棧聚合就得到了調(diào)用分布的近似。樣本越多統(tǒng)計(jì)越穩(wěn)。2.1 perf 采樣機(jī)制和幾個(gè)必須懂的參數(shù)Linux 上最通用的采樣工具就是 perf。它依賴(lài)內(nèi)核的 perf_event 子系統(tǒng)和硬件/軟件 PMU能按時(shí)間頻率或事件計(jì)數(shù)觸發(fā)采樣。一條典型的抓取命令是這樣perf record -F 99 -g -p 12345 -- sleep 30 perf script perf.out ./stackcollapse-perf.pl perf.out perf.folded ./flamegraph.pl perf.folded oncpu.svg幾個(gè)參數(shù)值得掰扯清楚。-F 99 表示每秒采樣 99 次。為什么不是 100因?yàn)檎麛?shù) 100 容易和系統(tǒng)里其他 100Hz 的周期任務(wù)比如某些定時(shí)器產(chǎn)生鎖相采出來(lái)的樣本會(huì)偏向某個(gè)固定相位導(dǎo)致分布失真。99 是個(gè)質(zhì)數(shù)能打散這種共振。-g 是抓完整調(diào)用棧沒(méi)有它你只能看到最頂層的函數(shù)看不到是誰(shuí)調(diào)用的定位就沒(méi)有上下文。-- sleep 30 是限定采集時(shí)長(zhǎng)用 sleep 而不是直接 Ctrl-C是為了讓腳本化更穩(wěn)定、退出更干凈。還有兩個(gè)參數(shù)在排查特定問(wèn)題時(shí)很關(guān)鍵-e 可以指定事件默認(rèn)是 cycles但你也可以換成 cache-misses、branch-misses 來(lái)抓特定瓶頸--call-graph dwarf 會(huì)在用戶(hù)態(tài)棧難以回溯時(shí)改用 DWARF 調(diào)試信息展開(kāi)代價(jià)是開(kāi)銷(xiāo)更大、產(chǎn)出更大。一般先默認(rèn) fpframe pointer跑一遍棧斷了再考慮 dwarf。注意perf 的內(nèi)核棧符號(hào)來(lái)自 /proc/kallsyms用戶(hù)態(tài)符號(hào)需要帶調(diào)試信息的二進(jìn)制。生產(chǎn)環(huán)境經(jīng)常遇到符號(hào)被 strip 掉的情況采出來(lái)全是十六進(jìn)制地址白忙一場(chǎng)。動(dòng)手前先確認(rèn)目標(biāo)進(jìn)程有沒(méi)有 debuginfo 或者帶符號(hào)的構(gòu)建產(chǎn)物。2.2 火焰圖怎么讀哪些形狀代表什么問(wèn)題火焰圖的橫軸是樣本占比不是時(shí)間順序縱軸是調(diào)用深度每一塊是一個(gè)棧幀塊越寬說(shuō)明該函數(shù)在采樣里出現(xiàn)得越多。讀圖的核心心法就一句先找最寬的葉子再看它是被誰(shuí)調(diào)用的。最寬的葉子通常是實(shí)際的 CPU 消耗大戶(hù)但它的元兇可能在它的調(diào)用者那一層——比如一個(gè)通用的序列化函數(shù)很寬但真正的問(wèn)題是某個(gè)業(yè)務(wù)邏輯反復(fù)調(diào)它。幾種典型形狀對(duì)應(yīng)不同問(wèn)題。平頂山形狀底部寬、頂層一排差不多的寬塊通常是循環(huán)或批處理在反復(fù)執(zhí)行同類(lèi)操作優(yōu)化點(diǎn)是降復(fù)雜度或加緩存。塔尖形狀某一層特別寬下面迅速收窄說(shuō)明有個(gè)單點(diǎn)函數(shù)吃滿(mǎn)了 CPU直接盯它。斷層形狀中間某層突然很窄說(shuō)明采樣在這個(gè)函數(shù)里分布稀疏要么是它執(zhí)行快要么是棧在那斷了需要配合 dwarf 或檢查符號(hào)。2.3 符號(hào)丟失、棧截?cái)嗪?JIT 代碼這三類(lèi)坑實(shí)話(huà)說(shuō)On-CPU 分析最容易翻車(chē)的不是命令不會(huì)敲而是采出來(lái)的東西沒(méi)法看。第一類(lèi)坑是符號(hào)丟失解決方式是裝對(duì)應(yīng)版本的 debuginfo 包或者用perf buildid-list確認(rèn) build-id 能不能對(duì)上 debug 文件。第二類(lèi)坑是棧截?cái)鄁rame pointer 被編譯器優(yōu)化掉了-fomit-frame-pointer 是很多發(fā)行版的默認(rèn)這時(shí)候要么重新編譯加 -fno-omit-frame-pointer要么切到 dwarf 模式。深度太深時(shí) perf 默認(rèn)只抓一部分??梢杂?-call-graph dwarf,65528加大棧大小。第三類(lèi)坑是 JIT 代碼Java、Node、部分 Go 場(chǎng)景都會(huì)遇到采出來(lái)是一堆[JIT]或者匿名地址。Java 的解法是掛 perf-map-agent讓 JIT 編譯時(shí)把方法地址映射寫(xiě)到 /tmp/perf-PID.mapperf 就能解析更省事的做法是直接用 async-profiler 抓 CPU 火焰圖它對(duì) JIT 代碼的映射是內(nèi)建的。Node 可以用--perf-basic-prof之類(lèi)的方式輸出映射表。這類(lèi)工作如果一開(kāi)始沒(méi)規(guī)劃好事后補(bǔ)會(huì)很難受建議在部署階段就把符號(hào)和映射準(zhǔn)備好。3. Off-CPU 分析把等待這件事量化聊完 On-CPU重頭戲來(lái)了。Off-CPU 分析要回答的是線(xiàn)程沒(méi)在跑的時(shí)候到底在等什么等了多久。傳統(tǒng)思路是插樁記錄每個(gè)阻塞點(diǎn)的進(jìn)入和退出時(shí)間但這要求改代碼、而且覆蓋不到內(nèi)核路徑。更優(yōu)雅的做法同樣是跟蹤切換內(nèi)核每次發(fā)生上下文切換時(shí)都會(huì)記錄誰(shuí)被換下、誰(shuí)被換上如果我們?cè)谶@兩個(gè)時(shí)刻打點(diǎn)就能算出每個(gè)線(xiàn)程兩次被調(diào)度之間隔了多久并且抓下它被換下時(shí)的調(diào)用棧——那個(gè)棧就是它睡覺(jué)的原因。3.1 線(xiàn)程離開(kāi) CPU 的幾種典型原因歸納一下線(xiàn)程從 Running 變成 Off-CPU常見(jiàn)就這幾類(lèi)。第一類(lèi)是睡眠等待主動(dòng)調(diào)用會(huì)睡眠的 syscall 或同步原語(yǔ)比如 read/write 阻塞、futex 等待、nanosleep。第二類(lèi)是等鎖包括用戶(hù)態(tài)自旋鎖的退化、互斥量、文件鎖內(nèi)核里 futex 是重災(zāi)區(qū)。第三類(lèi)是等 IO包括磁盤(pán)、網(wǎng)絡(luò)、管道很多時(shí)候表現(xiàn)為 epoll_wait 或 read 阻塞。第四類(lèi)是被搶占時(shí)間片用完或被高優(yōu)先級(jí)線(xiàn)程擠下去這時(shí)候線(xiàn)程還是 Runnable只是沒(méi)輪上。前幾類(lèi)是 Sleeping第四類(lèi)是 Runnable。區(qū)分它們很重要Sleeping 說(shuō)明有外部依賴(lài)拖著優(yōu)化方向是縮短依賴(lài)或并發(fā)化Runnable 說(shuō)明 CPU 資源或調(diào)度配置有問(wèn)題優(yōu)化方向是加資源或調(diào)優(yōu)先級(jí)、綁核。很多分析工具會(huì)把兩者混在總 Off-CPU 時(shí)間里看的時(shí)候要留意能不能拆開(kāi)。3.2 offcputime 與 wakeuptime等待棧和喚醒鏈BCC 工具集里的 offcputime 是最常用的入口。它會(huì)跟蹤上下文切換統(tǒng)計(jì)每個(gè)線(xiàn)程 Off-CPU 的累計(jì)時(shí)長(zhǎng)并按被換下時(shí)的棧聚合offcputime -df -p 12345 30 offcpu.folded flamegraph.pl --colorio --titleOff-CPU Time --countnameus offcpu.folded offcpu.svg參數(shù)含義-d 打印分隔符方便后續(xù)折疊-f 輸出折疊格式直接喂給火焰圖腳本-p 限定進(jìn)程最后一個(gè)數(shù)字是采集秒數(shù)。得到的 Off-CPU 火焰圖跟 On-CPU 長(zhǎng)得像但橫軸寬度是阻塞時(shí)長(zhǎng)微秒讀法是哪個(gè)棧把線(xiàn)程卡住得最久。offcputime 解決的是線(xiàn)程在哪睡但它不回答誰(shuí)把它叫醒的。這時(shí)候就要請(qǐng)出 wakeuptime。它把阻塞棧和喚醒棧拼在一起直接告訴你A 線(xiàn)程在某個(gè) futex 上睡了 200ms是被 B 線(xiàn)程喚醒的。這個(gè)信息非常值錢(qián)因?yàn)樗岩蚬溄o串起來(lái)了——很多鎖競(jìng)爭(zhēng)的根因不是持鎖線(xiàn)程慢而是持鎖線(xiàn)程被別的更慢的操作卡住了。wakeuptime -p 12345 30實(shí)測(cè)下來(lái)wakeuptime 在排查線(xiàn)程池互相等待、上游把下游拖死的場(chǎng)景里特別好用。缺點(diǎn)是對(duì)復(fù)雜調(diào)用棧的輸出會(huì)比較長(zhǎng)需要配合過(guò)濾看。3.3 runqlat被忽視的調(diào)度延遲前面說(shuō)過(guò) Runnable 狀態(tài)是獨(dú)立的一類(lèi)浪費(fèi)量化它主要靠 runqlat老版本叫 runqslower。它統(tǒng)計(jì)任務(wù)從進(jìn)入就緒隊(duì)列到真正被調(diào)度執(zhí)行之間的延遲分布輸出是直方圖runqlat -m -P 10-m用毫秒做單位-P按進(jìn)程聚合??催@個(gè)輸出主要看長(zhǎng)尾如果 p99 到了幾十毫秒說(shuō)明某些線(xiàn)程經(jīng)常在就緒隊(duì)列里等很久CPU 飽和或者有 CPU 密集任務(wù)在搶。這類(lèi)延遲在應(yīng)用層往往表現(xiàn)為莫名的抖動(dòng)而且因?yàn)樗幌?CPU 時(shí)間常規(guī)監(jiān)控根本看不到。線(xiàn)上偶發(fā)毛刺排查runqlat 是我必開(kāi)的一個(gè)工具。注意offcputime 統(tǒng)計(jì)的時(shí)長(zhǎng)里其實(shí)已經(jīng)隱含包含了 Runnable 的等待因?yàn)榫€(xiàn)程從被換下到再次被換上中間可能既有睡眠也有排隊(duì)。要精確拆開(kāi)需要同時(shí)看 offcputime 和 runqlat用差值估算。睡了 100ms 里有 80ms 是在排隊(duì)這種結(jié)論只能靠?jī)蓚€(gè)工具配合才能得出。4. 一次真實(shí)排查的完整推演講完工具來(lái)走一遍完整流程比干講參數(shù)有用得多。以下是我處理過(guò)的一個(gè)典型場(chǎng)景做了脫敏和簡(jiǎn)化但思路和步驟是原樣的。4.1 現(xiàn)象描述和初步假設(shè)現(xiàn)象某服務(wù)接口在壓測(cè)時(shí) p99 從 20ms 漲到 900ms但機(jī)器 CPU 使用率只有 25%IO 也不高監(jiān)控上看不出明顯異常。單看資源指標(biāo)這臺(tái)機(jī)器簡(jiǎn)直空閑得可以去度假。這種資源不緊張但延遲爆炸的形態(tài)基本可以判定瓶頸在 Off-CPU而且大概率是排隊(duì)或者鎖不是計(jì)算。先立幾個(gè)假設(shè)一是線(xiàn)程池被某個(gè)慢調(diào)用占滿(mǎn)新請(qǐng)求只能排隊(duì)二是某個(gè)共享資源連接池、緩存、鎖成了串行點(diǎn)三是 GC 或者定時(shí)任務(wù)周期性搶占導(dǎo)致抖動(dòng)。接下來(lái)逐個(gè)驗(yàn)證。4.2 用 On-CPU 排除計(jì)算密集第一步還是先排除計(jì)算問(wèn)題畢竟它最好查。抓 30 秒 On-CPU 火焰圖perf record -F 99 -g -p $(pgrep -f myapp | head -1) -- sleep 30 perf script | ./stackcollapse-perf.pl | ./flamegraph.pl oncpu.svg結(jié)果很干凈最寬的葉子占不到 5%沒(méi)有任何函數(shù)吃滿(mǎn)。這就基本排除了 CPU 熱點(diǎn)也側(cè)面印證問(wèn)題在等待。同時(shí)看了一眼 On-CPU 的采樣總數(shù)30 秒 99Hz 應(yīng)該接近 3000 個(gè)樣本實(shí)際只采到不到 800 個(gè)——這個(gè)差值本身就是信號(hào)大量時(shí)間線(xiàn)程根本沒(méi)在跑采樣器采不到。4.3 用 Off-CPU 定位到鎖競(jìng)爭(zhēng)接下來(lái)抓 Off-CPUoffcputime -df -p $(pgrep -f myapp | head -1) 30 offcpu.folded flamegraph.pl --colorio --titleOff-CPU --countnameus offcpu.folded offcpu.svg火焰圖一出來(lái)最寬的一條棧非常顯眼從業(yè)務(wù)入口一路到pthread_mutex_lock再往下是futex_wait累計(jì)阻塞時(shí)間占了總 Off-CPU 時(shí)間的六成以上。也就是說(shuō)大量線(xiàn)程都堵在同一把鎖上。順著棧找到那個(gè) mutex 對(duì)應(yīng)的代碼位置是一個(gè)本以為讀多寫(xiě)少的共享配置緩存實(shí)際每次刷新都會(huì)整表加寫(xiě)鎖而刷新又調(diào)用了下游接口一旦下游慢鎖就持很久。再用 wakeuptime 確認(rèn)喚醒鏈wakeuptime -p $(pgrep -f myapp | head -1) 30輸出清楚地顯示喚醒這些等鎖線(xiàn)程的正是那個(gè)持鎖的刷新線(xiàn)程而刷新線(xiàn)程自己又阻塞在下游網(wǎng)絡(luò)讀上。到這里因果鏈完整了下游慢 → 刷新線(xiàn)程持鎖時(shí)間長(zhǎng) → 其他線(xiàn)程全堵在鎖上 → 請(qǐng)求排隊(duì) → p99 爆炸。4.4 驗(yàn)證與修復(fù)驗(yàn)證方式很直接給共享緩存的刷新邏輯改成先算好再原子替換指針也就是讀路徑無(wú)鎖、寫(xiě)路徑只做一次指針交換把持鎖時(shí)間從整個(gè)下游調(diào)用縮到指針賦值。改完復(fù)壓p99 回落到 30ms 以?xún)?nèi)Off-CPU 火焰圖上那條 futex_wait 的寬度基本消失了。這次排查的價(jià)值不在于用了多高級(jí)的工具而在于先用 On-CPU 排除、再用 Off-CPU 定位、最后用 wakeuptime 串因果這個(gè)固定套路。你把這套順序記下來(lái)遇到類(lèi)似問(wèn)題就不用靠猜了。5. 環(huán)境準(zhǔn)備與工具選型的一些硬門(mén)檻工具再好環(huán)境不支持也白搭。Off-CPU 這類(lèi)分析工具大多基于 eBPF對(duì)內(nèi)核版本和權(quán)限有實(shí)打?qū)嵉囊筇崆按_認(rèn)能省掉很多抓耳撓腮的時(shí)間。5.1 內(nèi)核版本與 eBPF 能力自檢先看內(nèi)核版本uname -roffcputime、wakeuptime 這類(lèi)工具依賴(lài)掛載 kprobe 的能力實(shí)踐上建議內(nèi)核 4.15 以上5.x 更穩(wěn)。4.9 到 4.14 之間有些工具能跑但偶有兼容問(wèn)題。檢查內(nèi)核配置grep -E CONFIG_BPF|CONFIG_BPF_SYSCALL|CONFIG_KPROBE /boot/config-$(uname -r)幾個(gè)關(guān)鍵項(xiàng)要能查到BCC 工具集才能正常工作。如果是最小化安裝的發(fā)行版可能連 bcc-tools 包都沒(méi)裝。5.2 安裝 BCC 與依賴(lài)主流發(fā)行版基本都有現(xiàn)成包# Debian/Ubuntu 系 sudo apt-get install bpfcc-tools linux-headers-$(uname -r) # RHEL/CentOS 系 sudo yum install bcc-tools kernel-devel-$(uname -r)裝完工具一般在/usr/sbin或者/usr/share/bcc/tools下可能帶-bpfcc后綴。perf 和火焰圖腳本另裝perf 跟內(nèi)核版本綁定最好用包管理器裝避免自己編譯出坑FlameGraph 是一堆 Perl 腳本clone 下來(lái)即可。5.3 容器和虛擬化環(huán)境的特殊處理容器環(huán)境里有兩個(gè)必踩的坑。第一是 PID 命名空間容器里看到的 PID 和宿主機(jī)不是一回事perf 和 bcc 工具如果跑在宿主機(jī)上得用宿主機(jī)的 PID用docker top或者ps -eLf | grep去定位。BCC 的-p參數(shù)接受的是它所在命名空間里的 PID跑在容器內(nèi)就填容器內(nèi) PID。第二是權(quán)限eBPF 傳統(tǒng)上要 root5.8 以后有了 CAP_BPF 和 CAP_PERFMON 可以降權(quán)但生產(chǎn)上多數(shù)還是用 root 或者 privileged 容器。虛擬化環(huán)境里硬件 PMU 事件可能不可用perf 的部分功能會(huì)退化但軟件事件cpu-clock和采樣依然可用Off-CPU 工具基于 kprobe 基本不受影響。這一點(diǎn)在云主機(jī)上不需要太擔(dān)心。提示如果 offcputime 報(bào) HINT: ... failed to attach 之類(lèi)的錯(cuò)誤先確認(rèn)內(nèi)核開(kāi)了 CONFIG_BPF 相關(guān)選項(xiàng)再確認(rèn)自己沒(méi)有在受限的 seccomp 環(huán)境里跑。有些安全加固過(guò)的環(huán)境會(huì)禁用 bpf() 系統(tǒng)調(diào)用。6. 常見(jiàn)問(wèn)題與排查技巧速查把經(jīng)常遇到的坑整理成一張表排查時(shí)直接對(duì)號(hào)入座比翻文檔快?,F(xiàn)象可能原因排查手段處理方式On-CPU 火焰圖全是地址符號(hào)二進(jìn)制被 strip 或缺 debuginfoperf buildid-list對(duì)比安裝 debuginfo 包或帶符號(hào)重編采樣樣本數(shù)遠(yuǎn)低于預(yù)期線(xiàn)程大量 Off-CPU對(duì)比預(yù)期樣本數(shù)轉(zhuǎn) Off-CPU 分析Java 棧顯示為[JIT]JIT 代碼無(wú)符號(hào)映射檢查/tmp/perf-PID.map掛 perf-map-agent 或換 async-profileroffcputime 棧斷在中間棧展開(kāi)深度不足看棧頂部是否有斷裂dwarf 模式或加大棧參數(shù)Off-CPU 圖里大量 idle/swapper采集到空閑任務(wù)觀察最寬塊的名字用-p限定進(jìn)程或加過(guò)濾p99 抖動(dòng)但 CPU 不高調(diào)度延遲長(zhǎng)尾runqlat -m -P查 CPU 飽和或綁核調(diào)優(yōu)先級(jí)等鎖時(shí)間長(zhǎng)但持鎖者不慢持鎖者被下游卡住wakeuptime看喚醒鏈縮短持鎖范圍或改無(wú)鎖容器內(nèi)工具報(bào) PID 找不到命名空間不一致docker top核對(duì)用對(duì)應(yīng)命名空間的 PIDeBPF 工具 attach 失敗內(nèi)核能力或 seccomp 限制查內(nèi)核 config 和安全策略換環(huán)境或提權(quán)這張表里我最想強(qiáng)調(diào)的兩行是采樣樣本數(shù)遠(yuǎn)低于預(yù)期和等鎖時(shí)間長(zhǎng)但持鎖者不慢。前者是一個(gè)很隱蔽的信號(hào)很多人看到 CPU 不高就直接放棄 perf 了其實(shí)差值本身就在告訴你答案后者是翻身常犯的錯(cuò)——盯著等鎖的人看忽略了叫醒他們的人。把這兩個(gè)視角建立起來(lái)絕大多數(shù)詭異延遲都能找到入口。再補(bǔ)幾個(gè)實(shí)操心得。第一采集時(shí)長(zhǎng)別太短O(píng)ff-CPU 事件往往稀疏10 秒可能采不到幾次建議至少 30 秒長(zhǎng)尾問(wèn)題可以拉到幾分鐘。第二采樣時(shí)盡量復(fù)現(xiàn)問(wèn)題場(chǎng)景空跑采集沒(méi)意義要壓測(cè)就邊壓邊采。第三火焰圖看的是分布不是順序別試圖從圖里讀出時(shí)間先后要看先后得用 trace 類(lèi)工具。第四perf script產(chǎn)出的中間文件可能很大幾十秒采集幾百 MB 是常事注意磁盤(pán)空間和后續(xù)處理的內(nèi)存占用。7. 一些踩坑之后才明白的經(jīng)驗(yàn)最后聊點(diǎn)工具之外的東西都是實(shí)打?qū)嵅瘸鰜?lái)的。剛接觸 Off-CPU 分析時(shí)我老想找一個(gè)命令搞定后來(lái)發(fā)現(xiàn)這套分析的價(jià)值恰恰在于組合On-CPU 排除計(jì)算、offcputime 找阻塞點(diǎn)、wakeuptime 串因果、runqlat 補(bǔ)調(diào)度四個(gè)環(huán)節(jié)缺一個(gè)結(jié)論就可能偏。還有一個(gè)體會(huì)是Off-CPU 時(shí)間不是越低越好而是越可解釋越好。有些等待是設(shè)計(jì)使然比如正常的網(wǎng)絡(luò) RTT、必要的批量攢批這些等待是劃算的真正要干掉的是那些沒(méi)必要的、可以并發(fā)化的、因?yàn)閷?shí)現(xiàn)缺陷被放大的等待。所以別追求把 Off-CPU 壓到零先追求能說(shuō)清楚每一段等待花在哪、值不值。另一個(gè)容易忽略的點(diǎn)是采樣開(kāi)銷(xiāo)。eBPF 工具在高頻事件上比如上下文切換特別頻繁的系統(tǒng)本身也會(huì)產(chǎn)生可觀測(cè)的額外負(fù)載采集的時(shí)候要留意對(duì)被分析進(jìn)程的影響別把觀察行為變成了干擾因素。壓測(cè)對(duì)比時(shí)最好先跑一次不采集的基線(xiàn)再跑采集版本把采集開(kāi)銷(xiāo)從結(jié)果里扣掉不然容易冤枉代碼。再分享一個(gè)小技巧如果線(xiàn)上不方便跑 root 工具可以先從/proc/PID/status里的 voluntary_ctxt_switches 和 nonvoluntary_ctxt_switches 看趨勢(shì)兩個(gè)值漲得快說(shuō)明上下文切換頻繁O(jiān)ff-CPU 問(wèn)題概率大這就是個(gè)零成本的先行指標(biāo)用來(lái)決定要不要上重武器很夠用。這套東西我用了好幾年越用越覺(jué)得它不是什么高深技術(shù)就是把線(xiàn)程到底在干嘛這個(gè)問(wèn)題拆成兩半來(lái)回答的樸素方法。工具會(huì)隨內(nèi)核版本變理解On-CPU 加 Off-CPU 等于完整墻鐘時(shí)間這個(gè)賬本關(guān)系才是長(zhǎng)期管用的東西。