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

關鍵詞:Flame Graph、perf、perl。html

 

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

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

通過FlameGraph.git處理,最終生成矢量SVG圖形,能夠形象的看出不一樣部分佔用狀況,以及包含與被包含狀況。github

除了反應CPU使用狀況的CPU FlameGraph,還有幾種Flame Graph:Memory Flame GraphOff-CPU Flame GraphHot/Cold Flame GraphDifferential Flame Graph編程

 

本文目的是記錄如何使用Flame Graph;而後對其流程進行簡單分析,瞭解其數據前因後果;最後分析測試結果。瀏覽器

基本上作到知其然知其因此然。ide

1. Flame Graph使用

構造測試程序以下,能夠啓動5個線程。svg

每一個線程都有本身的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。

相關文章
相關標籤/搜索