深入解析CPU性能火焰圖生成的內(nèi)部原理
在進(jìn)行CPU性能優(yōu)化的時(shí)候,我們經(jīng)常先需要分析出來(lái)我們的應(yīng)用程序中的CPU資源在哪些函數(shù)中使用的比較多,這樣才能高效地優(yōu)化。一個(gè)非常好的分析工具就是《性能之巔》作者 Brendan Gregg 發(fā)明的火焰圖。

我們今天就來(lái)介紹下火焰圖的使用方法,以及它的工作原理。
一、火焰圖的使用
為了更好地展示火焰圖的原理,我專門寫了一小段代碼,
完整的源碼放到了Github上了:https://github.com/yanfeizhang/coder-kung-fu/blob/main/tests/cpu/test09/main.c。
接下來(lái)我們用這個(gè)代碼實(shí)際體驗(yàn)一下火焰圖是如何生成的。在本節(jié)中,我們只講如何使用,原理后面的小節(jié)再展開(kāi)。
這個(gè)時(shí)候,在你執(zhí)行命令的當(dāng)前目錄下生成了一個(gè)perf.data文件。接下來(lái)咱們需要把Brendan Gregg的生成火焰圖的項(xiàng)目下載下來(lái)。我們需要這個(gè)項(xiàng)目里的兩個(gè)perl腳本。
接下來(lái)我們使用 perf script 解析這個(gè)輸出文件,并把輸出結(jié)果傳入到 FlameGraph/stackcollapse-perf.pl 腳本中來(lái)進(jìn)一步解析,最后交由 FlameGraph/flamegraph.pl 來(lái)生成svg 格式的火焰圖。具體命令可以一行來(lái)完成。
這樣,一副火焰圖就生成好了。

之所以選擇我提供一個(gè) demo 代碼來(lái)生成,是因?yàn)檫@個(gè)足夠簡(jiǎn)單和清晰,方便大家理解。在上面這個(gè)火焰圖中,可以看出 main 函數(shù)調(diào)用了 funcA、funcB、funcC,其中 funcA 又調(diào)用了 funcD、funcE,然后這些函數(shù)的開(kāi)銷又都不是自己花掉的,而是因?yàn)樽约赫{(diào)用的一個(gè) CPU 密集型的函數(shù) caculate。整個(gè)系統(tǒng)的調(diào)用棧的耗時(shí)統(tǒng)計(jì)就十分清晰的展現(xiàn)在眼前了。
如果要對(duì)這個(gè)項(xiàng)目進(jìn)行性能優(yōu)化。在上方的火焰圖中看雖然funcA、funcB、funcC、funcD、funcE這幾個(gè)函數(shù)的耗時(shí)都挺長(zhǎng),但它們的耗時(shí)并不是自己用掉的。而且都花在執(zhí)行子函數(shù)里了。我們真正應(yīng)該關(guān)注的是火焰圖最上方 caculate 這種又長(zhǎng)又平的函數(shù)。因?yàn)樗攀钦嬲ǖ?CPU 時(shí)間的代碼。其它項(xiàng)目中也一樣,拿到火焰圖后,從最上方開(kāi)始,把耗時(shí)比較長(zhǎng)的函數(shù)找出來(lái),優(yōu)化掉。
另外就是在實(shí)際的項(xiàng)目中,可能函數(shù)會(huì)非常的多,并不像上面這么簡(jiǎn)單,很多函數(shù)名可能都被折疊起來(lái)了。這個(gè)也好辦,svg 格式的圖片是支持交互的,你可以點(diǎn)擊其中的某個(gè)函數(shù),然后就可以展開(kāi)了只詳細(xì)地看這個(gè)函數(shù)以及其子函數(shù)的火焰圖了。
怎么樣,火焰圖使用起來(lái)是不是還挺簡(jiǎn)單的。接下來(lái)的小節(jié)中我們?cè)賮?lái)講講火焰圖生成全過(guò)程的內(nèi)部原理。理解了這個(gè),你才能講火焰圖用的得心應(yīng)手。
二、perf采樣
2.1 perf 介紹
在生成火焰圖的第一步中,就是需要對(duì)你要觀察的進(jìn)程或服務(wù)器進(jìn)行采樣。采樣可用的工具有好幾個(gè),我們這里用的是 perf record。
上面的命令中 -g 指的是采樣的時(shí)候要記錄調(diào)用棧的信息。./main 是啟動(dòng) main 程序,并只采樣這一個(gè)進(jìn)程。這只是個(gè)最簡(jiǎn)單的用法,其實(shí) perf record 的功能非常的豐富。
它可以指定采集事件。當(dāng)前系統(tǒng)支持的事件列表可以用過(guò) perf list 來(lái)查看。默認(rèn)情況下采集的是 Hardware event 下的 cycles 這一事件。假如我們想采樣 cache-misses 事件,我們可以通過(guò) -e 參數(shù)指定。
還可以指定采樣的方式。該命令支持兩種采樣方式,時(shí)間頻率采樣,事件次數(shù)發(fā)生采樣。-F 參數(shù)指定的是每秒鐘采樣多少次。-c參數(shù)指定的是每發(fā)生多少次采樣一次。
還可以指定要記錄的CPU核
還可以采集內(nèi)核的調(diào)用棧
在使用 perf record 執(zhí)行后,會(huì)將采樣到的數(shù)據(jù)都生成到 perf.data 文件中。在上面的實(shí)驗(yàn)中,雖然我們只采集了幾秒,但是生成的文件還挺大的,有 800 多 KB。我們通過(guò) perf script 命令可以解析查看一下該文件的內(nèi)容。大概有 5 萬(wàn)多行。其中的內(nèi)容就是采樣 cycles 事件時(shí)的調(diào)用棧信息。
除了 perf script 外,還可以使用 perf report 來(lái)查看和渲染結(jié)果。

【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺(jué)得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ?


2.2 內(nèi)核工作過(guò)程
我們來(lái)簡(jiǎn)單看一下內(nèi)核是如何工作的。
perf在采樣的過(guò)程大概分為兩步,一是調(diào)用 perf_event_open 來(lái)打開(kāi)一個(gè) event 文件,而是調(diào)用 read、mmap等系統(tǒng)調(diào)用讀取內(nèi)核采樣回來(lái)的數(shù)據(jù)。整體的工作流程圖大概如下

其中 perf_event_open 完成了非常重要的幾項(xiàng)工作。
創(chuàng)建各種event內(nèi)核對(duì)象
創(chuàng)建各種event文件句柄
指定采樣處理回調(diào)
我們來(lái)看下它的幾個(gè)關(guān)鍵執(zhí)行過(guò)程。在 perf_event_open 調(diào)用的 perf_event_alloc 指定了采樣處理回調(diào)函數(shù)為,比如perf_event_output_backward、perf_event_output_forward等
當(dāng) perf_event_open 創(chuàng)建事件對(duì)象,并打開(kāi)后,硬件上發(fā)生的事件就可以出發(fā)執(zhí)行了。內(nèi)核注冊(cè)相應(yīng)的硬件中斷處理函數(shù)是 perf_event_nmi_handler。
這樣 CPU 硬件會(huì)根據(jù) perf_event_open 調(diào)用時(shí)指定的周期發(fā)起中斷,調(diào)用 perf_event_nmi_handler 通知內(nèi)核進(jìn)行采樣處理
該終端處理函數(shù)的函數(shù)調(diào)用鏈經(jīng)過(guò) x86_pmu_handle_irq 到達(dá) perf_event_overflow。其中 perf_event_overflow 是一個(gè)關(guān)鍵的采樣函數(shù)。無(wú)論是硬件事件采樣,還是軟件事件采樣都會(huì)調(diào)用到它。它會(huì)調(diào)用 perf_event_open 時(shí)注冊(cè)的 overflow_handler。我們假設(shè) overflow_handler 為 perf_event_output_forward
在 __perf_event_output 中真正進(jìn)行了采樣處理
如果開(kāi)啟了 PERF_SAMPLE_CALLCHAIN,則不僅僅會(huì)把當(dāng)前在執(zhí)行的函數(shù)名采集下來(lái),還會(huì)把整個(gè)調(diào)用鏈都記錄起來(lái)。
這樣硬件和內(nèi)核一起協(xié)助配合就完成了函數(shù)調(diào)用棧的采樣。后面 perf 工具就可以讀取這些數(shù)據(jù)并進(jìn)行下一次的處理了。
三、FlameGraph工作過(guò)程
前面我們用 perf script 解析是看到的函數(shù)調(diào)用棧信息比較的長(zhǎng)。
在畫火焰圖的前一步得需要對(duì)這個(gè)數(shù)據(jù)進(jìn)行一下預(yù)處理。stackcollapse-perf.pl 腳本會(huì)統(tǒng)計(jì)每個(gè)調(diào)用?;厮莩霈F(xiàn)的次數(shù),并將調(diào)用棧處理為一行。行前面表示的是調(diào)用棧,后面輸出的是采樣到該函數(shù)在運(yùn)行的次數(shù)。
上面 perf script 5 萬(wàn)多行的輸出,經(jīng)過(guò) stackcollapse.pl 預(yù)處理后,輸出只有不到 10 行。數(shù)據(jù)量大大地得到了簡(jiǎn)化。在 FlameGraph 項(xiàng)目目錄下,能看到好多 stackcollapse 開(kāi)頭的文件

這是因?yàn)楦鞣N語(yǔ)言、各種工具采樣輸出是不一樣的,所以自然也就需要不同的預(yù)處理腳本來(lái)解析。
在經(jīng)過(guò) stackcollapse 處理得到了上面的輸出結(jié)果后,就可以開(kāi)始畫火焰圖了。flamegraph.pl 腳本工作原理是:將上面的一行繪制成一列,采樣數(shù)得到的次數(shù)越大列就越寬。另外就是如果同一級(jí)別如果函數(shù)名一樣,就合并到一起。比如現(xiàn)在有一下數(shù)據(jù)文件:
我可以通過(guò)手工畫一個(gè)類似的火焰圖,如下:

其中 funcA 因?yàn)閮尚杏涗浐喜ⅲ哉紦?jù)了 3 的寬度。funcD 沒(méi)有合并,占據(jù)就是1。另外 funcB、funcC都畫在A上的上方,占據(jù)的寬度都是2。
總結(jié)
火焰圖是一個(gè)非常好的用于分析熱點(diǎn)函數(shù)的工具,只要你關(guān)注性能優(yōu)化,就應(yīng)該學(xué)會(huì)使用它來(lái)分析你的程序。我們今天的文章不光是介紹了火焰圖是如何生成的,而且還介紹了其底層的工作原理。火焰圖的生成主要分兩步,一是采樣,而是渲染。
在采樣這一步,主要依賴的是內(nèi)核提供的 perf_event_open 系統(tǒng)調(diào)用。該系統(tǒng)調(diào)用在內(nèi)部進(jìn)行了非常復(fù)雜的處理過(guò)程。最終內(nèi)核和硬件一起協(xié)同合作,會(huì)定時(shí)將當(dāng)前正在執(zhí)行的函數(shù),以及函數(shù)完整的調(diào)用鏈路都給記錄下來(lái)。
在渲染這一步,Brendan Gregg 提供的腳本會(huì)出 perf 工具輸出的 perf_data 文件進(jìn)行預(yù)處理,然后基于預(yù)處理后的數(shù)據(jù)渲染成 svg 圖片。函數(shù)執(zhí)行的次數(shù)越多,在 svg 圖片中的寬度就越寬。我們就可以非常直觀地看出哪些函數(shù)消耗的 CPU 多了。
最后再補(bǔ)充說(shuō)一句是,我們的火焰圖只是一個(gè)采樣的渲染結(jié)果,并不一定完全代表真實(shí)情況,但也夠用了。
原文作者:開(kāi)發(fā)內(nèi)功修煉
