1. 程式人生 > >使用Flame Graph進行系統性能分析

使用Flame Graph進行系統性能分析

ima cut htm -- () 還需要 通過 The i++

關鍵詞:Flame Graph、perf、perl。

FlameGraph是由BrendanGregg開發的一款開源可視化性能分析工具,形象的成為火焰圖。

從底向上像火苗一樣逐漸變小,也反映了相互之間的包含關系,下面的框條包含上面內容。

經過FlameGraph.git處理,最終生成矢量SVG圖形,可以形象的看出不同部分占用情況,以及包含與被包含情況。

除了反應CPU使用情況的CPU FlameGraph,還有幾種Flame Graph:Memory Flame Graph、Off-CPU Flame Graph、Hot/Cold Flame Graph、Differential Flame Graph。

本文目的是記錄如何使用Flame Graph;然後對其流程進行簡單分析,了解其數據來龍去脈;最後分析測試結果。

基本上做到知其然知其所以然。

1. Flame Graph使用

構造測試程序如下,可以啟動5個線程。

每個線程都有自己的thread_funcx(),while(1)裏面再調用函數。

在8核CPU上執行,預測應該每個thread_funcx()都會占用相同的比例,因為都是100%占用CPU,然後裏面的函數比例呈現階梯形。

技術分享圖片
#include <stdio.h>
#include <pthread.h>

#define LOOP_COUNT 1000000

void
func_a(void) { int i; for(i=0; i<LOOP_COUNT; i++); } void func_b(void) { int i; for(i=0; i<LOOP_COUNT; i++); func_a(); } void func_c(void) { int i; for(i=0; i<LOOP_COUNT; i++); func_b(); } void func_d(void) { int i; for(i=0; i<LOOP_COUNT; i++); func_c(); }
void func_e(void) { int i; for(i=0; i<LOOP_COUNT; i++); func_d(); } void* thread_fun1(void* param) { while(1) { int i; for(i=0;i<LOOP_COUNT;i++); func_a(); } } void* thread_fun2(void* param) { while(1) { int i; for(i=0;i<LOOP_COUNT;i++); func_b(); } } void* thread_fun3(void* param) { while(1) { int i; for(i=0;i<LOOP_COUNT;i++); func_c(); } } void* thread_fun4(void* param) { while(1) { int i; for(i=0;i<LOOP_COUNT;i++); func_d(); } } void* thread_fun5(void* param) { while(1) { int i; for(i=0;i<LOOP_COUNT;i++); func_e(); } } int main(void) { int ret; pthread_t tid1, tid2, tid3, tid4, tid5; ret=pthread_create(&tid1, NULL, thread_fun1, NULL); if(ret==-1){ printf("Create pthread failed.\n"); return -1; } ret=pthread_create(&tid2, NULL, thread_fun2, NULL); if(ret==-1){ printf("Create pthread failed.\n"); return -1; } ret=pthread_create(&tid3, NULL, thread_fun3, NULL); if(ret==-1){ printf("Create pthread failed.\n"); return -1; } ret=pthread_create(&tid4, NULL, thread_fun4, NULL); if(ret==-1){ printf("Create pthread failed.\n"); return -1; } ret=pthread_create(&tid5, NULL, thread_fun5, NULL); if(ret==-1){ printf("Create pthread failed.\n"); return -1; } if(pthread_join(tid1,NULL)!=0){ printf("pthrad join failed.\n"); return -1; } if(pthread_join(tid2,NULL)!=0){ printf("pthrad join failed.\n"); return -1; } if(pthread_join(tid3,NULL)!=0){ printf("pthrad join failed.\n"); return -1; } if(pthread_join(tid4,NULL)!=0){ printf("pthrad join failed.\n"); return -1; } if(pthread_join(tid5,NULL)!=0){ printf("pthrad join failed.\n"); return -1; } return 0; }
View Code

編譯然後執行結果:

gcc createFlame.c -o createFlame -pthread
./createFlame

在sudo su權限中進行perf record和FlameGraph生成;-F 999采樣率999Hz,-a包括所有CPU,-g使能call-graph錄制,-- sleep 60記錄60秒時長。

perf record -F 999 -a -g -- sleep 60
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > out.svg

在瀏覽器中查看結果如下:

技術分享圖片

可以看出createFlame應用,調用start_thread創建線程,五個線程函數占用相等寬度。

線程函數以下的層級調用寬度相差基本一致。

使用perf report -g查看start_thread,然後逐級展開調用及其占比。

整個start_thread占據99%,然後5個線程均分,因為每個都獨占一個CPU。

每個線程裏面函數占比,與FlameGraph中一致。

技術分享圖片

1.1 查看細節

鼠標移動到FlameGraph框圖上時,會顯示對應進程或函數的被采樣信息。

如果點擊框圖,則以其為基礎展開,放大顯示後面的找關系。已達到縮放,顯示細節和整體。

1.2 查找

在右上角Search或者Ctrl+F,可以在FlameGraph中查找相應符號的框圖。

2. Flame Graph流程分析

從perf record輸出的perf.data,到最終生成out.svg文件,可以分為三步:1.perf script、2.stackcollapse-perf.pl、3.flamegraph.pl。

如果要詳細了解其如何一步一步解析字符串,到最終生成svg矢量圖形可以閱讀stackcollapse-perf.pl和flamegraph.pl兩個perl腳本。

下面借助構造偽數據,來理解其流程。

2.1 perf script

perf script將perf record的記錄轉換成可讀的采樣記錄,每一個下采樣記錄包含應用名稱、以及采樣到的stack信息。

進程名後的進程ID、CPU號、時間戳、cycles數目都是無用信息,下面的stack也只有函數名有效。

技術分享圖片
createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_a (xxx)
                 000 func_b (xxx)
                 000 func_c (xxx)
                 000 func_d (xxx)
                 000 func_e (xxx)
                 000 thread_fun5 (xxx)
                 000 start_thread (xxx)
View Code

構造一份perf script生成的偽數據,來分析流程以及明白FlameGraph的含義。

技術分享圖片
createFlame  0 [0]  0.0:   0 cycles: 
                 000 thread_fun1 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_a (xxx)
                 000 thread_fun1 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 thread_fun2 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_b (xxx)
                 000 thread_fun2 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_a (xxx)
                 000 func_b (xxx)
                 000 thread_fun2 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 thread_fun3 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_c (xxx)
                 000 thread_fun3 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_b (xxx)
                 000 func_c (xxx)
                 000 thread_fun3 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_a (xxx)
                 000 func_b (xxx)
                 000 func_c (xxx)
                 000 thread_fun3 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 thread_fun4 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_d (xxx)
                 000 thread_fun4 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_c (xxx)
                 000 func_d (xxx)
                 000 thread_fun4 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_b (xxx)
                 000 func_c (xxx)
                 000 func_d (xxx)
                 000 thread_fun4 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_a (xxx)
                 000 func_b (xxx)
                 000 func_c (xxx)
                 000 func_d (xxx)
                 000 thread_fun4 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 thread_fun5 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_e (xxx)
                 000 thread_fun5 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_d (xxx)
                 000 func_e (xxx)
                 000 thread_fun5 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_c (xxx)
                 000 func_d (xxx)
                 000 func_e (xxx)
                 000 thread_fun5 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_b (xxx)
                 000 func_c (xxx)
                 000 func_d (xxx)
                 000 func_e (xxx)
                 000 thread_fun5 (xxx)
                 000 start_thread (xxx)

createFlame  0 [0]  0.0:   0 cycles: 
                 000 func_a (xxx)
                 000 func_b (xxx)
                 000 func_c (xxx)
                 000 func_d (xxx)
                 000 func_e (xxx)
                 000 thread_fun5 (xxx)
                 000 start_thread (xxx)
View Code

2.2 stackcollapse-perf.pl

stackcollapse-perf.pl將perf script生成的多行stack記錄轉換成一行,函數之間用逗號隔開,最後的記錄采樣次數用空格隔開。

可以通過./stackcollapse-perf.pl -h查看幫助,查看cat perf_fake.txt | ./stackcollapse-perf.pl輸出。

可以清晰地看出棧的關系和采樣到的次數。

技術分享圖片
createFlame;start_thread;thread_fun1 1
createFlame;start_thread;thread_fun1;func_a 1
createFlame;start_thread;thread_fun2 1
createFlame;start_thread;thread_fun2;func_b 1
createFlame;start_thread;thread_fun2;func_b;func_a 1
createFlame;start_thread;thread_fun3 1
createFlame;start_thread;thread_fun3;func_c 1
createFlame;start_thread;thread_fun3;func_c;func_b 1
createFlame;start_thread;thread_fun3;func_c;func_b;func_a 1
createFlame;start_thread;thread_fun4 1
createFlame;start_thread;thread_fun4;func_d 1
createFlame;start_thread;thread_fun4;func_d;func_c 1
createFlame;start_thread;thread_fun4;func_d;func_c;func_b 1
createFlame;start_thread;thread_fun4;func_d;func_c;func_b;func_a 1
createFlame;start_thread;thread_fun5 1
createFlame;start_thread;thread_fun5;func_e 1
createFlame;start_thread;thread_fun5;func_e;func_d 1
createFlame;start_thread;thread_fun5;func_e;func_d;func_c 1
createFlame;start_thread;thread_fun5;func_e;func_d;func_c;func_b 1
createFlame;start_thread;thread_fun5;func_e;func_d;func_c;func_b;func_a 1
View Code

2.3 flamegraph.pl

那麽stackcollapse-perf.pl的數據經過flamegraph.pl處理之後又是什麽樣子呢?

可以看出svg圖形,就像stackcollapse-perf.pl每一行豎向顯示。

技術分享圖片

那麽簡單修改一下,將thread_fun5的func_a的stack重復4次,圖形會變成什麽樣子呢?

可以看出thread_fun5的func_a變得更寬了。

技術分享圖片

所以不難理解,Flame Graph縱向表示一次調用棧深度,調用關系從下到上;Flame Graph橫向寬度表示被perf record采樣到的次數。

3. Flame Graph結果分析

所有的FlameGraph都是統計采樣結果,根據進程、函數棧進行匹配,同樣棧的采樣計數累加。

FlameGraph的實際應用除了查看CPU使用情況之外,還有通過監控內存分配/釋放函數的MemoryFlameGraph;

記錄進程因為IO、喚醒等耗費時間的Off-CPU FlameGraph;

以及將CPU FlameGraph和Off-CPU FlameGraph進行合並的Hot/Cold FlameGraph;

對兩次不同測試進行比較的DifferentialFlameGraph。

之前對CPU FlameGraph進行了介紹,下面詳細介紹其余四種FlameGraph的使用。

3.1 MemoryFlameGraph

《Memory Leak (and Growth) Flame Graphs》關於內存的FlameGraph和CPU FlameGraph的區別在於CPU是采樣,Memory跟蹤內存trace events,比如malloc()/free()/realloc()/calloc()/brk()/mmap()。

然後在對調用棧進行統計,顯示FlameGraph。其本質上是一樣的。

perf record -e syscalls:sys_enter_mmap -a -g -- sleep 120
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl --color=mem \ --title="Heap Expansion Flame Graph" --countname="calls" > out_mmap.svg

結果如下:

技術分享圖片

但從實際來看這張圖並不能反映Memory Leak,也不能準確反映Memory Grouth。

因為只是記錄mmap()的次數,沒有記錄每次大小;同時沒有記錄munmap()的次數。

3.1.1 一個通過trace events定位內存泄漏的案例

記得之前Debug過內存泄漏問題:運行過一段時間,發現總的內存在增加。查看/proc/meminfo大概是slab內存泄漏,然後查看一下/proc/slabinfo看出是kmalloc-64在不停增加。

所以借助tracing/events/kmem/kmalloc和kfree兩個events,觀察是哪個進程在泄漏內存,同時修改call_site從顯示地址編程顯示符號。

如何確定內存泄漏呢?

以進程作為組,kmalloc()分配大小累加;如果有kfree(),通過ptr匹配從累計值中減去對應kmalloc()大小。

這樣在運行一段時間過後,每個進程的累計值就是增量,可以很輕松的確定增量是多少,以及每個增量的符號表。

3.2 Off-CPU FlameGraph

和CPU FlameGraph相反,Off-CPU FlameGraph反映的是進程沒有在CPU上運行的時間都在幹嘛,這也是影響進程性能的關鍵因素。

比如進程時間片用完導致的進程切換、映射到內存的IO操作、調度延遲等。

《Off-CPU Flame Graphs》循序漸進的介紹了IO造成的Off-CPU時間、包括IO延遲的Off-CPU時間、進程喚醒延時,以及展示進程之間喚醒點棧關系的Chain Graphs。

比如查看Block I/O次數的FlameGraph,這個只能做個參考。如果想要更準確的看IO延遲時間,還需要借助文中提到的biostacks、fileiostacks等工具。

sudo perf record -e block:block_rq_insert -a -g -- sleep 30
sudo perf script --header | ./stackcollapse-perf.pl | ./flamegraph.pl --color=io --title="Block I/O Flame Graph" --countname="I/O" > out.svg

結果如下:

技術分享圖片

3.3 Hot/Cold FlameGraph

3.4 Differential FlameGraph

能有哪些實際應用?

參考文檔:

《Flame Graphs》:關於FlameGraph的來龍去脈,及其詳細介紹匯總。

《The Flame Graph》:發表在acm.org文章,This visualization of software execution is a new necessity for performance profiling and debugging。

使用Flame Graph進行系統性能分析