本文主要分享火焰圖使用技巧,介紹 systemtap 的原理機制,如何使用火焰圖快速定位性能問題原因,同時加深對 systemtap 的理解。
讓我們回想一下,曾經(jīng)作為編程新手的我們是如何調(diào)優(yōu)程序的?通常是在沒有數(shù)據(jù)的情況下依靠主觀臆斷來瞎蒙,稍微有些經(jīng)驗的同學則會對差異代碼進行二分或者逐段調(diào)試。這種定位問題的方式不僅耗時耗力,而且還不具有通用性,當遇到其他類似的性能問題時,需要重復踩坑、填坑,那么如何避免這種情況呢?
俗語有曰:兵欲善其事必先利其器,個人認為,程序員定位性能問題也需要一件“利器”。如同醫(yī)生給病人看病,需要依靠專業(yè)的醫(yī)學工具(比如 X 光片、聽診器等)進行診斷,最后依據(jù)醫(yī)學工具的檢驗結(jié)果快速精準的定位出病因所在。性能調(diào)優(yōu)工具(比如 perf / gprof 等)之于性能調(diào)優(yōu)就像 X 光之于病人一樣,它可以一針見血的指出程序的性能瓶頸。
但是常用的性能調(diào)優(yōu)工具 perf 等,在呈現(xiàn)內(nèi)容上只能單一的列出調(diào)用棧或者非層次化的時間分布,不夠直觀。這里我推薦大家配合使用火焰圖,它將 perf 等工具采集的數(shù)據(jù)呈現(xiàn)得更為直觀。
初識火焰圖
火焰圖(Flame Graph)是由 linux 性能優(yōu)化大師 Brendan Gregg 發(fā)明的,和所有其他的 profiling 方法不同的是,火焰圖以一個全局的視野來看待時間分布,它從底部往頂部,列出所有可能導致性能瓶頸的調(diào)用棧。

火焰圖整個圖形看起來就像一個跳動的火焰,這就是它名字的由來。
火焰圖有以下特征(這里以 on-cpu 火焰圖為例):
- 每一列代表一個調(diào)用棧,每一個格子代表一個函數(shù)
- 縱軸展示了棧的深度,按照調(diào)用關(guān)系從下到上排列。最頂上格子代表采樣時,正在占用 cpu 的函數(shù)。
- 橫軸的意義是指:火焰圖將采集的多個調(diào)用棧信息,通過按字母橫向排序的方式將眾多信息聚合在一起。需要注意的是它并不代表時間。
- 橫軸格子的寬度代表其在采樣中出現(xiàn)頻率,所以一個格子的寬度越大,說明它是瓶頸原因的可能性就越大。
- 火焰圖格子的顏色是隨機的暖色調(diào),方便區(qū)分各個調(diào)用信息。
- 其他的采樣方式也可以使用火焰圖, on-cpu 火焰圖橫軸是指 cpu 占用時間,off-cpu 火焰圖橫軸則代表阻塞時間。
- 采樣可以是單線程、多線程、多進程甚至是多 host,進階用法可以參考附錄進階閱讀。
火焰圖類型
常見的火焰圖類型有 On-CPU,Off-CPU,還有 Memory,Hot/Cold,Differential 等等。他們分別適合處理什么樣的問題呢?
這里筆者主要使用到的是 On-CPU、Off-CPU 以及 Memory 火焰圖,所以這里僅僅對這三種火焰圖作比較,也歡迎大家補充和斧正。

火焰圖分析技巧
- 縱軸代表調(diào)用棧的深度(棧楨數(shù)),用于表示函數(shù)間調(diào)用關(guān)系:下面的函數(shù)是上面函數(shù)的父函數(shù)。
- 橫軸代表調(diào)用頻次,一個格子的寬度越大,越說明其可能是瓶頸原因。
- 不同類型火焰圖適合優(yōu)化的場景不同,比如 on-cpu 火焰圖適合分析 cpu 占用高的問題函數(shù),off-cpu 火焰圖適合解決阻塞和鎖搶占問題。
- 無意義的事情:橫向先后順序是為了聚合,跟函數(shù)間依賴或調(diào)用關(guān)系無關(guān);火焰圖各種顏色是為方便區(qū)分,本身不具有特殊含義
- 多練習:進行性能優(yōu)化有意識的使用火焰圖的方式進行性能調(diào)優(yōu)(如果時間充裕)
如何繪制火焰圖?
要生成火焰圖,必須要有一個順手的動態(tài)追蹤工具,如果操作系統(tǒng)是 Linux 的話,那么通常通常是 perf 或者 systemtap 中的一種。其中 perf 相對更常用,多數(shù) Linux 都包含了 perf 這個工具,可以直接使用;SystemTap 則功能更為強大,監(jiān)控也更為靈活。網(wǎng)上關(guān)于如何使用 perf 繪制火焰圖的文章非常多而且豐富,所以本文將以 SystemTap 為例。
SystemTap 是動態(tài)追蹤工具,它通過探針機制,來采集內(nèi)核或者應用程序的運行信息,從而可以不用修改內(nèi)核和應用程序的代碼,就獲得豐富的信息,幫你分析、定位想要排查的問題。SystemTap 定義了一種類似的 DSL 腳本語言,方便用戶根據(jù)需要自由擴展。不過,不同于動態(tài)追蹤的鼻祖 DTrace ,SystemTap 并沒有常駐內(nèi)核的運行時,它需要先把腳本編譯為內(nèi)核模塊,然后再插入到內(nèi)核中執(zhí)行。這也導致 SystemTap 啟動比較緩慢,并且依賴于完整的調(diào)試符號表。
使用 SystemTap 繪制火焰圖的主要流程如下:
- 安裝 SystemTap 以及 操作系統(tǒng)符號調(diào)試表
- 根據(jù)自己所需繪制的火焰圖類型以及進程類型選擇合適的腳本
- 生成內(nèi)核模塊
- 運行 SystemTap 或者運行生成的內(nèi)核模塊統(tǒng)計數(shù)據(jù)
- 將統(tǒng)計數(shù)據(jù)轉(zhuǎn)換成火焰圖
本文演示步驟將會基于操作系統(tǒng) Tlinux 2.2
安裝 SystemTap 以及 操作系統(tǒng)符號調(diào)試表
使用 yum 工具安裝 systemtap:
yum install systemtap systemtap-runtime
由于 systemtap 工具依賴于完整的調(diào)試符號表,而且生產(chǎn)環(huán)境不同機器的內(nèi)核版本不同(雖然都是Tlinux 2.2版本,但是內(nèi)核版本后面的小版本不一樣,可以通過 uname -a 命令查看)所以我們還需要安裝 kernel-debuginfo 包、 kernel-devel 包 我這里是安裝了這兩個依賴包
kernel-devel-3.10.107-1-tlinux2-0046.x86_64
kernel-debuginfo-3.10.107-1-tlinux2-0046.x86_64
根據(jù)自己所需繪制的火焰圖類型以及進程類型選擇合適的腳本
使用 SystemTap 統(tǒng)計相關(guān)數(shù)據(jù)往往需要自己依照它的語法,編寫腳本,具有一定門檻。幸運的是,github 上春哥(agentzh)開源了兩組他常用的 SystemTap 腳本:
openresty-systemtap-toolkit 和 stapxx,這兩個工具集能夠覆蓋大部分 C 進程、Nginx 進程以及 Openresty 進程的性能問題場景。
我們這里需要繪制 off-cpu 火焰圖,所以使用 sample-bt-off-cpu 腳本即可
生成內(nèi)核模塊
現(xiàn)在我們有了統(tǒng)計腳本,也安裝好了 systemtap,正常來說就可以使用了,但由于 systemtap 是通過生成內(nèi)核模塊的方式統(tǒng)計相關(guān)探針的統(tǒng)計數(shù)據(jù),而 tlinux 要求所有運行的內(nèi)核模塊需要先到 tlinux 平臺簽名才可以運行,所以:
故需要先修改 off-cpu 腳本,讓其先生成內(nèi)核模塊;之后對該內(nèi)核模塊作簽名;最后使用 systemtap 命令手工運行該腳本,統(tǒng)計監(jiān)控數(shù)據(jù)
Systemtap 執(zhí)行流程如下:

- parse:分析腳本語法
- elaborate:展開腳本 中定義的探針和連接預定義腳本庫,分析內(nèi)核和內(nèi)核模塊的調(diào)試信息
- translate:.將腳本編譯成C語言內(nèi)核模塊文件放 在$HOME/xxx.c 緩存起來,避免同一腳本多次編譯
- build:將c語言模塊文件編譯成.ko的內(nèi)核模塊,也緩存起來。
- 把模塊交給staprun,staprun加載內(nèi)核模塊到內(nèi)核空間,stapio連接內(nèi)核模塊和用戶空間,提供交互IO通道,采集數(shù)據(jù)。
所以我們這里修改下 off-cpu 的 stap 腳本,讓其只運行完第四階段,只生成一個內(nèi)核模塊
// 在 stap 命令后增加 -p4 參數(shù),告訴systemtap,當前只需要執(zhí)行到第四階段
open my $in, "|stap -p4 --skip-badvars --all-modules -x $pid -d '$exec_path' --ldd $d_so_args $stap_args -"
or die "Cannot run stap: $!n";
修改好之后運行腳本,會生成一個內(nèi)核模塊
// -p 8682 是需要監(jiān)控的進程的進程號
// -t 30 是指會采樣30秒
./sample-bt-off-cpu -p 8692 -t 30
生成的內(nèi)核模塊名稱形如 stap_xxxxx.ko模塊名稱 由于讀者并不需要關(guān)心內(nèi)核模塊簽名,故章節(jié)略過
運行內(nèi)核模塊統(tǒng)計數(shù)據(jù)
內(nèi)核模塊簽名完成后,便可以使用 staprun 命令手工運行相關(guān)內(nèi)核模塊了
命令:
// 注意:簽名腳本會將生產(chǎn)的內(nèi)核模塊重命名,需要將名字改回去……(腳本bug)
staprun -x {進程號} {內(nèi)核模塊名} > demo.bt
值得注意的是,監(jiān)控的進程要有一定負載 systemtap 才可以采集到相關(guān)數(shù)據(jù),即在采集時,同時需要要有一定請求量(通常是自己構(gòu)造請求,壓測進程)
將統(tǒng)計數(shù)據(jù)轉(zhuǎn)換成火焰圖
獲得了統(tǒng)計數(shù)據(jù) demo.bt 后,便可以使用火焰圖工具繪制火焰圖了
下載 FlameGraph,鏈接:
https://github.com/brendangregg/FlameGraph
命令:
./stackcollapse-stap.pl demo.bt > demo.folded
./flamegraph.pl demo.folded > demo.svg
這樣便獲得了 off-cpu 火焰圖:

看圖說話
趁熱打鐵,通過幾張火焰圖熟悉下如何使用火焰圖
圖片來自于春哥微博或者個人近期定位的問題
on-cpu 火焰圖
Apache APISIX QPS急劇下降問題

Apache APISIX 是一個開源國產(chǎn)的高性能 API 網(wǎng)關(guān),之前在進行選型壓測時,發(fā)現(xiàn)當 Route 匹配不同場景下, QPS 急劇下降,在其 CPU (四十八核)占用率幾乎達到100%的情況下只有幾千 QPS,通過繪制火焰圖發(fā)現(xiàn),其主要耗時在一個 table 插入階段(lj_cf_table_insert),分析代碼發(fā)現(xiàn)是該 table 一直沒有釋放,每次匹配不中路由會插入數(shù)據(jù),導致表越來越大,后續(xù)插入耗時過長導致 QPS 下降。
off-cpu 火焰圖
nginx 互斥鎖問題

這是一張 nginx 的 off-cpu 火焰圖,我們可以很快鎖定到
ngx_common_set_cache_fs_size -> ngx_shmtx_lock -> sem_wait 這段邏輯使用到了互斥鎖,它讓 nginx 進程絕大部分阻塞等待時間花費在獲取該鎖。
agent 監(jiān)控上報斷點問題

這是一張 agent 的 off-cpu 火焰圖,它是一個多線程異步事件模型,主線程處理各個消息,多個線程分別負責配置下發(fā)或者監(jiān)控上報的職責。當前問題出現(xiàn)在監(jiān)控上報性能差,無法在周期(一分鐘)內(nèi)完成監(jiān)控數(shù)據(jù)上報,導致監(jiān)控斷點,通過 off-cpu 火焰圖我們可以分析出,該上報線程花費了大量的時間使用 curl_easy_perform 接口收發(fā) http 監(jiān)控數(shù)據(jù)消息中。
依據(jù)火焰圖將發(fā)送 http 消息的邏輯改為異步非阻塞后,該問題解決。
附錄
進階閱讀
- 谷歌搜索演講:Blazing Performance with Flame Graphs
- 演講 ppt:https://www.slideshare.NET/brendangregg/blazing-performance-with-flame-graphs
- 《SystemTap新手指南》:https://spacewander.gitbooks.io/systemtapbeginnersguide_zh/content/index.html
FAQ
使用 perf 或者 systemtap 的方式采集數(shù)據(jù),會對后臺服務有性能影響嗎?
有,但是很小,可以基本忽略不計。
它們使用系統(tǒng)的探針或者使用一些自定義的動態(tài)探針進行數(shù)據(jù)采集,第一對代碼無侵入性,它既不需要停止服務,也不需要修改應用程序的代碼;第二,它們是以內(nèi)核模塊/內(nèi)核原生的方式跟蹤用戶態(tài)和內(nèi)核態(tài)的所有事件,并通過一系列優(yōu)化措施,進行采樣統(tǒng)計,對目標服務性能影響極小,大概在5%左右或者更低的性能損耗。相較于將進程運行在沙箱的 valgrind 工具或靜態(tài)調(diào)試工具 gdb 來說,動態(tài)追蹤 perf 或者 systemtap 或者 ebpf 的性能損耗基本可以忽略不計。
目標進程重啟后,systemtap 是否需要重新生成內(nèi)核模塊?
不需要。甚至同一個 linux 內(nèi)核版本下的同一個二進制進程(md5值一致),在安裝 kernel 調(diào)試符號表后,便可以在生成采集指標的內(nèi)核模塊,并且可以多次使用。
當 linux 內(nèi)核版本不一致,符號表有變化,需要重新生成內(nèi)核模塊;當目標進程二進制文件重新編譯后,也需要重新生成統(tǒng)計用的 systemtap 內(nèi)核模塊。