火焰圖詳解:如何讀懂 CPU 火焰圖,找出燒掉 CPU 的程式碼
火焰圖(flame graph)是一種呼叫棧取樣的視覺化方法,它展示的是程式把 CPU 時間花在了哪裡。2011 年由 Brendan Gregg 發明,它把取樣到的函式呼叫從下往上堆疊,讓你在幾秒鐘內就能讀出一個程式最熱的程式碼路徑。所有火焰圖都遵循四條規則:寬度是該函式在取樣中的佔比,不是持續時間;y 軸是呼叫深度;頂邊才是真正 on-CPU 的位置(根在上的渲染方向則相反,看底邊);x 軸按字母序排列,不是時間軸。如果只能記住一件事,記住這四條。
你需要這張圖的時刻,通常是這樣的:告警響了,top 顯示某個程序 CPU 佔用 100%,然後……就沒有然後了。top 能告訴你是哪個程序在燒 CPU,卻回答不了真正的問題——是哪個函式、哪個檔案、哪一行程式碼。火焰圖補上的正是這一步:它把排查從"程序級"推進到"程式碼行級"。本文會先給出四條讀圖規則和常見形態,再用三個真實生產案例(PHP、Go、Erlang)帶你從一張真實的火焰圖一路走到罪魁程式碼行。
如何讀懂火焰圖
大多數指南告訴你火焰圖"展示 CPU 使用情況"。這句話正確但無用。以下是你真正需要的讀圖方法。
四條規則
1. 寬度 = 取樣佔比,不是持續時間。 一個方框佔據圖的 40%,意味著該函式出現在 40% 的呼叫棧取樣中。它不意味著該函式執行了 40% 的時鐘時間。對 CPU 效能分析來說,兩者的區別通常不大;但當有人問"為甚麼這個框在週二的圖上更寬了"時,答案是取樣次數,不是掛鐘時間。
2. y 軸是呼叫深度。 底部是入口(main、start_thread、執行時的引導函式),頂部是 CPU 實際在執行的葉函式。中間每個方框都是呼叫鏈上的呼叫者。
3. 頂邊是 on-CPU 的位置。 想知道甚麼在燒你的 CPU?看圖的頂邊。頂邊最寬的方框就是正在 CPU 上執行的函式。它們下面的所有東西都是"怎麼走到這裡的"。注意渲染方向:經典火焰圖根在底、火苗向上;不少現代工具(包括本文案例所用的 OpenResty XRay)渲染為根在頂、葉函式向下——這時本條規則映象到底邊。兩種方向的資料完全一樣,詳見後文變體一節。
4. x 軸是字母序,不是時間。 這是最常見的誤讀。火焰圖不是時間線。水平排列是按字母序(或地址序),目的是讓相同的棧幀合併成更寬的方框。兩個並排的方框不代表"A 先發生,然後 B 發生"。舉個例子:decode 出現在 encode 左邊,並不意味著解碼先於編碼發生——只是字母 d 排在 e 前面而已。
形態識別
內化四條規則之後,讀火焰圖就變成了看形狀:
Plateau(寬而平的頂部):一個函式獨佔 CPU。這是最簡單的模式——看這個 plateau 函式做了甚麼,你就找到了瓶頸。下面 PHP 的例子中,
preg_match就是這樣的瓶頸——配套報告顯示以它為葉函式的路徑佔 56.2% 的 CPU。Tower(高而窄的尖刺):一個很深的呼叫棧,但觸發頻率低。除非多個 tower 匯聚成一個寬底座,否則它們通常不是你的問題。一張全是 plateau 的火焰圖裡出現一個孤立的 tower,那只是噪聲。
Hair(頂邊上大量細小的毛刺):每個只出現在幾次取樣中的短暫呼叫——核心中斷、定時器 tick、訊號處理函式。Hair 是正常的。如果 hair 很寬,那你遇到的是中斷風暴(interrupt storm),不是應用 bug。
底座平坦、頂部參差:你的框架或執行時(底座部分)是健康的,差異在應用層程式碼(上方的尖刺)。從底座分叉的地方開始往上讀。下面 PHP 的例子就是典型——框架層每層等寬,熱點全在更深的業務程式碼裡。
這些形態不必死記,下面「三個真實案例」一節裡都能對上號:PHP 是深底座、Erlang 是葉端分叉。先看真圖,形態自然就內化了。
Self 與 total
火焰圖裡每個方框有兩個量:total(總計)是它的完整寬度——包含它呼叫的所有函式;self(自身)是該函式位於棧頂部的取樣佔比(即沒有更上層的被呼叫者被取樣到)。分析器裡也常把這兩個量寫作 “self time” 和 “total time”,但在取樣式火焰圖裡這個 “time” 並不準確——和規則 1 一樣,底層量是取樣次數/佔比,不是掛鐘時間。Go 的 pprof 把它們叫作 flat(self)和 cum(cumulative,即 total)就更貼切:名字裡沒有 “time”。(Chrome DevTools 確實叫 “Self Time”/“Total Time”,但那是因為它產出的是 flame chart,x 軸真的是時間——參見後文變體一節。)
total 很大但 self 很小的函式是排程器(dispatcher):它自己不燒 CPU,只是把工作分發給下面的子函式。
如果你的 profiler 支援按 self(pprof 裡的 flat)排序,用它。Self 最高的函式才是 CPU 真正在跑的——它們是你的最佳化目標。
三個真實案例:從火焰圖到罪魁程式碼行
前面的規則和形態是詞彙表,真實案例才教你做判斷。以下三個案例來自透過動態追蹤對真實執行中的程序進行剖析的結果——無需修改程式碼,無需重啟。先說清兩件事:OpenResty XRay 渲染的火焰圖是根在頂、葉函式在底的方向(即後文的 icicle 方向),並用紅色高亮標出最熱回溯——所以讀下面的圖要看底部和紅色路徑;案例中的百分比數字來自與火焰圖同一次分析生成的程式碼路徑報告。每個案例遵循同一個三步讀圖法:你看到甚麼 → 怎麼認出它 → 它意味著甚麼。
案例一:PHP preg_match — 56.2% CPU 花在迴圈裡的正則
你看到甚麼: 一張根在上的 PHP 語言級火焰圖。可見部分是 Laravel 框架的引導與中介軟體鏈——從 server.php 經 Kernel::handle 到 Pipeline 中介軟體,一層層幾乎等寬地往下傳。這是「深底座」形態:框架本身不燒 CPU,只是把請求一路傳下去,真正的熱點在呼叫鏈更深處的業務程式碼裡。
怎麼認出它: 底座每層幾乎等寬,說明 CPU 沒有在框架層被分掉;順著最寬的一條鏈往下追即可到達業務函式。配套的程式碼路徑報告直接給出了答案:排名第一的最熱 PHP 路徑以 preg_match 為葉函式,經由 Laravel 的 callAction 到達 processOrders,佔 56.2% 的 CPU 時間——葉函式就是 CPU 燒掉的地方。
它意味著甚麼: processOrders(ProductServiceProvider.php 第 437 行)的一個迴圈裡在反覆匹配同一個正規表示式,每次迭代都支付一次 PCRE 匹配的代價。修復方向也來自源案例:把正規表示式預編譯好並複用,讓它不必在每次迭代時重新構建。
同一個程序的 C 語言級別程式碼路徑報告印證了這一結論:
pcre2_match_8 是最熱的 C 函式,佔 34.7%,經由 php_pcre_match_impl 呼叫。當兩個語言級別指向同一個瓶頸時,你可以確信這個診斷是對的。
完整排查過程:PHP 高 CPU 使用率:定位最熱的 PHP 程式碼路徑
案例二:Go regexp.MustCompile — 36.8% CPU 花在正則編譯
你看到甚麼: 一張根在上的 Go 語言級火焰圖,最熱回溯被紅色高亮標出:HTTP 處理鏈經 chat.Handle 走到 prev_processor.go 第 17 行的 CheckMessage,再進入 regexp.MustCompile,其下展開一整片紅色的 regexp/syntax 編譯呼叫。配套報告顯示這條正則編譯路徑佔 36.8% 的 CPU 時間。
怎麼認出它: 跟著紅色高亮走到葉端,函式名說的是另一個故事:這不是正則匹配(執行)——而是正則編譯(構建自動機)。編譯比匹配昂貴得多,在熱路徑裡做編譯是 Go 程式碼的常見錯誤。
它意味著甚麼: prev_processor.go 第 17 行的 CheckMessage 函式每次呼叫都在執行 regexp.MustCompile。通行做法是每個正則只編譯一次並複用編譯結果——例如放在包級變數裡初始化。把編譯移出熱路徑,這條佔 36.8% CPU 的編譯呼叫鏈就不復存在。
完整排查過程:Go 高 CPU 佔用:正規表示式編譯消耗了 36.8% 的 CPU 時間
案例三:Erlang erts_pcre_exec — lists:filter 迴圈中的 PCRE 回溯
你看到甚麼: 根在上的 Erlang 語言級火焰圖:從 cowboy 的請求處理鏈走到業務函式 check_resp_content,再經 lists:filter 的匿名函式到達底部的 erts_pcre_exec,紅色高亮的 C:match.constprop.1 就是最熱回溯的終點。C 語言級別的程式碼路徑報告確認同一條鏈——最熱 C 路徑是 match ← erts_pcre_exec ← re:run:
怎麼認出它: 葉級分叉形態。不同於 Go 案例的單一紅色主幹,這張圖在最底部分出幾條並列的葉路徑(erts_iolist_size、erts_iolist_to_buf、erts_pcre_exec——前兩個在圖中顯示為截斷後的標籤)。規則不變:跟著最寬、被紅色高亮的那條走——它通向 erts_pcre_exec,正則呼叫。
它意味著甚麼: 列表中的每一個元素都在與一個 PCRE 正規表示式做匹配。PCRE 使用回溯 NFA 引擎,包含 .*、.+ 或巢狀選擇分支的模式可能導致指數級匹配時間——這種失敗模式叫做災難性回溯。修復方案是最佳化正則模式以避免回溯,或者換用非回溯的正則引擎。
完整排查過程:Erlang 高 CPU 使用率:透過火焰圖追蹤 PCRE 正則瓶頸
三個案例的共同規律
三個瓶頸都和正則相關,但結論是通用的:
- 先看葉函式一端(本文三例即底邊)。有紅色高亮時直接跟著高亮走。最寬的葉方框就是你的起點。
- 往下讀上下文。 下方的方框告訴你為甚麼這個函式是熱的——誰在呼叫它、從哪裡呼叫、在甚麼迴圈裡。
- 跨語言級別交叉驗證。 當應用層火焰圖(PHP/Go/Erlang)和 C 層火焰圖指向同一個瓶頸時,你得到的是一個確認的診斷,不是假說。
三個案例恰好都是正則瓶頸;想看其他形態的真實圖——鎖等待、記憶體分配等——可以從下文技術棧路由表最後一列的各技術棧案例文章,以及後文的 off-CPU 與記憶體火焰圖變體入手。
從 100% CPU 到罪魁程式碼行
回到開頭那個場景:top 停在了程序級。把前面的讀圖方法串起來,四步就能走完從 100% CPU 到具體程式碼行的全程:
第 1 步:找到程序。 top 或 htop 告訴你哪個程序在燒 CPU。記下 PID。
第 2 步:獲取火焰圖。 用 profiler 從那個 PID 採集呼叫棧取樣。下一節會介紹每種技術棧用甚麼工具。
第 3 步:讀葉函式一端。 找到火焰圖葉端最寬的方框——那就是直接燒 CPU 的函式。懸停或點選即可看到它的原始檔和行號。注意:要找的是葉端最寬(self 最高)的框,不是整體最寬的——底部那些貫穿全寬的方框往往是框架或排程器,total 很大但自己不燒 CPU(見上文「Self 與 total」一節)。
第 4 步:讀程式碼。 開啟檔案,跳到那一行。現在你知道了甚麼在燒 CPU 以及為甚麼被呼叫,因為火焰圖給了你完整的呼叫棧。
不管甚麼語言或執行時,這個流程都一樣。變化的只是第 2 步用甚麼工具。
技術棧路由表
| 技術棧 | 工具 | 一行命令 / 入口 | 火焰圖案例文章 |
|---|---|---|---|
| Linux(任意原生二進位制) | perf | perf record -g -p PID; perf script | stackcollapse-perf.pl | flamegraph.pl | — |
| Go | pprof | go tool pprof -http=:8080 http://host:port/debug/pprof/profile | Go 高 CPU |
| Java | async-profiler | asprof -d 30 -f out.html PID | Java CPU 分析 |
| Node.js | 0x / --prof | 0x app.js | Node.js CPU 分析 |
| PHP | Excimer(取樣) | — | PHP 高 CPU |
| Perl | Devel::NYTProf | perl -d:NYTProf script.pl | Perl 高 CPU |
| Erlang/BEAM | eflame | — | Erlang 高 CPU |
| Rust | cargo-flamegraph | cargo flamegraph --pid PID | Rust/Sled 高 CPU |
| Nginx/OpenResty | — | — | Nginx CPU 最熱請求、Lua CPU 火焰圖 |
| C/C++(Envoy、llama.cpp 等) | perf | 同 Linux 行 | Envoy CPU、llama.cpp CPU |
上表中的每種工具都需要某種形式的插樁、重編譯或重啟——下一節細說這個成本,以及如何繞開它。
如何為你的技術棧生成火焰圖
如果你從未生成過火焰圖,前面的技術棧路由表為主流執行時給出了工具入口。通用步驟是:
- 採集呼叫棧取樣——從目標程序採集(工具因技術棧而異)。
- 摺疊——把取樣摺疊成文字格式(每行一個唯一棧,帶計數)。
- 渲染——把摺疊後的棧渲染成 SVG 或互動式 HTML。
Brendan Gregg 的 FlameGraph 倉庫提供了 stackcollapse-*.pl 和 flamegraph.pl 用於第 2、3 步。大多數現代工具(pprof、async-profiler、cargo-flamegraph)一條命令完成全部三步。
所有這些工具的共同代價是訪問許可權:你需要修改程序(加 flag、用 frame pointer 重編譯、重啟以啟用 debug agent)或擁有 root/CAP_SYS_ADMIN 許可權來使用 perf/eBPF。在生產環境中,這個代價是真實存在的——尤其是在事故當中,你最不想做的事就是重啟。
OpenResty XRay 消除了這個代價。它透過動態追蹤掛接到一個正在執行的程序——不需要改程式碼、不需要重編譯、不需要重啟——可以為上面路由表中列出的所有技術棧生成火焰圖。本文三個案例中的火焰圖就是這樣採集的:來自正在執行的程序,零停機。
Icicle 圖、flame chart 與其他變體
Icicle Graph(冰柱圖)
Icicle graph(也叫倒火焰圖)把呼叫棧翻轉:根在頂部,葉函式向下生長。資料完全一樣——相同的取樣、相同的寬度——只是方向變了。
大多數現代效能分析工具(Grafana Pyroscope、Polar Signals、Datadog 的 continuous profiler)預設使用 icicle 方向。如果你用的工具在頂部顯示根節點,你看到的就是 icicle graph——本文三個案例中的 OpenResty XRay 火焰圖正是這個方向。讀圖規則相同:寬度是取樣佔比,底邊變成了 on-CPU 的位置,x 軸仍然不是時間。
Flame chart 與 flame graph 的區別
Flame chart 看起來像火焰圖,但 x 軸是時間。Flame chart 由瀏覽器 DevTools(Chrome 的 Performance 面板)和一些 tracing 工具生成。在 flame chart 中,水平位置意味著"這件事先發生,那件事後發生"——左右順序是有意義的。
最快的區分方法:如果相同的函式名出現在多個互不合並的方框裡,那是 flame chart(按時間排列)。如果相同的名字總是合併成一個更寬的方框,那是 flame graph(按字母序合併)。
其他變體
火焰圖不限於 CPU:
- Off-CPU 火焰圖展示程式在等甚麼——鎖、I/O、sleep 呼叫。參見 Off-CPU 分析實戰。
- 記憶體火焰圖展示哪些呼叫路徑在分配記憶體。參見 Django 記憶體分析。
- 差分火焰圖(Differential flame graph) 對比兩次取樣,展示變化了甚麼——適合上線前後對比。
每種變體改變的是寬度代表甚麼(等待時間、分配位元組、取樣差值),但堆疊結構不變。
AI 能替你讀火焰圖嗎?
能加速,但替代不了基本功。AI 對常見模式(迴圈裡的正則、鎖競爭、框架開銷)的識別確實快,對第一次讀圖的工程師是有用的起點。但本文的四條規則足夠簡單,基本功不需要 AI。AI 真正增值的地方,是把火焰圖關聯到你的具體程式碼庫和執行時——而這需要的不僅僅是一張 SVG 圖片。
OpenResty XRay 的 AI 助手正是這麼做的:它不是分析一張靜態圖片,而是直接基於產生火焰圖的動態追蹤資料——原始的呼叫棧取樣、程序後設資料、執行時上下文。這意味著它可以跨語言級別關聯(PHP + C、Go + 執行時),識別你所用框架特有的模式,給出引用你的原始檔和行號的建議。
常見問題
火焰圖中方框的寬度代表甚麼?
寬度代表包含該函式的呼叫棧取樣佔總取樣的比例。一個方框佔圖寬的 30%,意味著在整個取樣期間,該函式出現在 30% 的取樣中。它是 CPU 時間份額的近似值,不是掛鐘時間的測量。
火焰圖中的顏色有含義嗎?
在 Brendan Gregg 的原始火焰圖中,顏色是隨機的暖色調——沒有語義含義。一些工具會按模組、語言級別或包名分配顏色(例如紅色表示核心、綠色表示應用程式碼)。檢視你的工具是否有圖例說明。如果沒有圖例,顏色只是裝飾。OpenResty XRay 的火焰圖採用橙色和藍色兩種配色:橙色的是 CPU 火焰圖,藍色的是 off-CPU 火焰圖。
怎樣在火焰圖中找到瓶頸?
看圖的頂邊(如果是 icicle graph 則看底邊)。頂邊 self 佔比最高的方框就是那個函式——它是直接在執行的,不只是分發工作。那就是你的瓶頸。往下讀可以看到到達它的呼叫鏈。
我在 top 裡看到 100% CPU 但不知道是哪段程式碼造成的,怎麼辦?
top 告訴你哪個程序在燒 CPU,但不告訴你是哪個函式或哪一行。你需要一張火焰圖。把 profiler 掛接到那個程序的 PID 上(參見上面的技術棧路由表),採集 10–30 秒的呼叫棧取樣,然後渲染火焰圖。葉函式一端(經典方向在頂邊,根在上的方向在底邊)最寬的 plateau 就是罪魁函式。懸停或點選它即可看到原始檔和行號。如果你是在追查高 P99 延遲、且 CPU 佔用偏高,火焰圖正是這一步——完整的診斷路徑與根因譜系見 P99 延遲詳解。
關於 OpenResty XRay
OpenResty XRay 是一個動態追蹤產品,它可以自動分析執行中的應用,以解決效能問題、行為問題和安全漏洞,並提供可行的建議。在底層實現上,OpenResty XRay 由我們的 Y 語言驅動,可以在不同環境下支援多種不同的執行時,如 Stap+、eBPF+、GDB 和 ODB。
關於作者
章亦春是開源 OpenResty® 專案創始人兼 OpenResty Inc. 公司 CEO 和創始人。
章亦春(Github ID: agentzh),生於中國江蘇,現定居美國灣區。他是中國早期開源技術和文化的倡導者和領軍人物,曾供職於多家國際知名的高科技企業,如 Cloudflare、雅虎、阿里巴巴, 是 “邊緣計算”、“動態追蹤 “和 “機器程式設計 “的先驅,擁有超過 22 年的程式設計及 16 年的開源經驗。作為擁有超過 4000 萬全球域名使用者的開源專案的領導者。他基於其 OpenResty® 開源專案打造的高科技企業 OpenResty Inc. 位於美國矽谷中心。其主打的兩個產品 OpenResty XRay(利用動態追蹤技術的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最適合微服務和分散式流量的全能型閘道器軟體),廣受全球眾多上市及大型企業青睞。在 OpenResty 以外,章亦春為多個開源專案貢獻了累計超過百萬行程式碼,其中包括,Linux 核心、Nginx、LuaJIT、GDB、SystemTap、LLVM、Perl 等,並編寫過 60 多個開源軟體庫。
關注我們
如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的部落格網站 。也歡迎掃碼關注我們的微信公眾號:
翻譯
我們提供了英文版原文和中譯版(本文)。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮採用,非常感謝!

























