使用Flame Graph進行系統性能分析
關鍵詞: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 voidView Codefunc_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; }
編譯然後執行結果:
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 1View 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進行系統性能分析