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


關鍵詞:Flame Graph、perf、perl。

 

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

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

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

除了反應CPU使用情況的CPU FlameGraph,還有幾種Flame Graph:Memory Flame GraphOff-CPU Flame GraphHot/Cold Flame GraphDifferential 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.plflamegraph.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

Hot/Cold FlameGraph》將On-CPU FlameGraph和Off-CPU FlameGraph融合到一張圖中,這樣就可以一目了然時間都耗費在哪里了。

但是目前生成的結果分析起來仍然比較困難,還處在實驗階段。

3.4 Differential FlameGraph

Differential FlameGraph》比較兩份FlameGraph,用於比較兩個版本差異,更好地確定性能regression。

 實際環境中的Differential FlameGraph較難分析,這里構造兩個FlameGraph然后進行Differential比較。

分別構造偽數據out.folded1和out.folded2如下:

out.folded1

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_x 1
createFlame;start_thread;thread_fun4;func_d;func_c;func_b;func_a 5
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

out.folded2

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_x 1
createFlame;start_thread;thread_fun5;func_e;func_d;func_c;func_b;func_a 5
View Code

分別生成兩者FlameGraph及Differential FlameGraph。 

./flamegraph.pl < out.folded1 > out_1.svg
./flamegraph.pl < out.folded2 > out_2.svg
./difffolded.pl out.folded1 out.folded2 | ./flamegraph.pl > diff.svg
./difffolded.pl out.folded2 out.folded1 | ./flamegraph.pl > diff2.svg

 

 圖1

  

圖2

圖1和圖2反映了兩組數據的差異,圖2相比圖1thread_fun4少了func_x,減小了func_a;thread_fun5的func_a增大了,多了func_x。

下面是圖1相對於圖2的Differential FlameGraph,可以看出輪廓基本和圖2一致。

圖2丟掉的thread_fun4的func_x,不顯示;func_a變小用藍色標識。圖2新增的thread_fun5的fun_x,thread_fun5的func_a用紅色標識。

 

圖3

然后再以圖1為基礎進行查分,如下圖: 

 

圖4

 4. 小結

CPU FlameGraph用於查找程序執行的熱點,找出性能瓶頸。Memory FlameGraph用於簡單分析內存泄漏或者增長趨勢。

相對於CPU FlameGraph,Off-CPU FlameGraph能找出進程好在CPU之外的時間,對於提高進程性能找出浪費時間有效。

Hot/Cold FlameGraph將CPU FlameGraph和Off-CPU FlameGraph兩者融合到一張圖中,更清晰的展示進程時間分配。

Differential FlameGraph可用於性能Regression對比。

 

參考文檔:

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM