使用ftrace分析函數性能

0. 背景

ftrace的功能非常強大,可以在系統的各個關鍵點上採集數據用以追蹤系統的運行情況。既支持預設的靜態插樁點(trace event),也支持每個函數的動態插樁(function tracer)。還可以利用動態插樁來測量函數的執行時間(function graph tracer)。關於ftrace的詳細操作和原理分析可以參考Linux ftrace一文。

本文的主要目的主要是利用ftrace來做新增代碼的性能分析和優化,應用的主要場景如下:

我們在現有的代碼中增加了一批新函數A_*()

功能完成後,我們希望知道兩個問題:

  • Question 1、在運行過程中,新增的函數A_*()造成了多少的性能損失?
  • Question 2、如果需要優化,怎樣找出某個耗時比較大的A_xxx()函數具體開銷在哪裏?

1. trace-cmd工具的安裝

我們可以手工操作/sys/kernel/debug/tracing路徑下的大量的配置文件接口,來使用ftrace的強大功能。但是這些接口對普通用戶來說太多太複雜了,我們可以使用對ftrace功能進行二次封裝的一些命令來操作。

trace-cmd就是ftrace封裝命令其中的一種。該軟件包由兩部分組成:

  • 1、trace-cmd。提供了數據抓取和數據分析的功能。
  • 2、kernelshark。可以用圖形化的方式來詳細分析數據,也可以做數據抓取。

首先我們需要安裝trace-cmd工具,ubuntu下可以通過以下兩種方式安裝:

  • 1、編譯源碼,安裝最新版本:
1、安裝依賴:
sudo apt-get install build-essential git cmake libjson-c-dev -y
sudo apt-get install freeglut3-dev libxmu-dev libxi-dev -y
sudo apt-get install qtbase5-dev -y

2、下載源碼:
git clone https://git.kernel.org/pub/scm/utils/trace-cmd/trace-cmd.git/

3、編譯安裝:
cd trace-cmd
make gui
sudo make install_gui
  • 2、從軟件源安裝,版本較舊:
sudo apt install trace-cmd kernelshark

2. 粗粒度分析

假設我們新增了一批函數名爲vfs_*(),性能分析時我們可以先總體追蹤一下這些函數的耗時,以及耗時在總體時間中的佔比。即Question 1

2.1 使用trace-cmd record -l func命令抓取數據

trace-cmd從per_cpu buffer中抓取原始數據/sys/kernel/debug/tracing/per_cpu/cpu0/trace_pipe_raw,所以它的開銷小並且支持長時間抓取。

sudo trace-cmd record -p function_graph -l vfs_* -F cp -r ~/perf perf.bak

命令的詳細參數含義如下:

  • -p function_graph :指定當前tracer爲function_graph,只有function_graph才能測量函數執行時間
  • -l vfs_* :函數過濾,指定function_graph追蹤哪些函數。function_graph有兩種過濾條件可以配置:
    • -l func。實際對應set_ftrace_filter,這種方式插樁的開銷較小,只會追蹤頂層func的執行時間且支持*等通配符的設置。
    • -g func。實際對應set_graph_function,這種方式插樁的開銷較大,但能追蹤func以及func所有子函數的的執行時間,不支持*等通配符的設置。
  • -F cp -r ~/perf perf.bak :進程過濾,指定對cp -r ~/perf perf.bak這個進程進行追蹤。也可以使用-P pid來指定進程。還可以不指定進程默認對全局進程追蹤,例如sleep 10追蹤10s。

因爲-l func-g func的特點,所以我們在粗粒度分析時使用-l func,在細粒度分析時使用-g func。這也是本文的一個精髓。

2.2 使用trace-cmd report --profile命令分析數據

所有的原始trace數據已經默認存儲到trace.dat文件中了。

1、使用trace-cmd report命令可以把trace.dat解析成文本格式:

$ sudo trace-cmd report| more
...

# 註釋 #    進程名-pid  CPU    時間戳(s)      函數入口/出口  耗時等級  函數耗時      函數名
              cp-3484  [006] 20010.128398: funcgraph_entry:      + 52.946 us  |  vfs_open();
              cp-3484  [006] 20010.128466: funcgraph_entry:      ! 212.370 us |  vfs_read();
              cp-3484  [006] 20010.128723: funcgraph_entry:        1.832 us   |  vfs_read();
              cp-3484  [006] 20010.128725: funcgraph_entry:        0.250 us   |  vfs_read();
              cp-3484  [006] 20010.128729: funcgraph_entry:        0.673 us   |  vfs_open();
              cp-3484  [006] 20010.128730: funcgraph_entry:        0.688 us   |  vfs_read();
              cp-3484  [006] 20010.128731: funcgraph_entry:        0.434 us   |  vfs_read();
              cp-3484  [006] 20010.129213: funcgraph_entry:        1.386 us   |  vfs_open();
              cp-3484  [006] 20010.129215: funcgraph_entry:                   |  vfs_statx_fd() {

文本主要格式的含義如上中文註釋所示,我們讀出開始的時間戳和結束的時間戳,就能計算出操作的總體時間。

2、trace-cmd report --profile命令可以對我們追蹤的函數執行時間進行統計:

$ sudo trace-cmd report --profile
...

task: cp-3484   # 進程名和PID
#註釋#         函數名       次數   總時長(ns)      平均時長(ns)   最大時長(ns)(時間戳s)         最小時長(ns)(時間戳s)
  Event: func: vfs_read() (6017) Total: 565900849 Avg: 94050 Max: 1827017(ts:20010.738236) Min:203(ts:20010.130418)
  Event: func: vfs_write() (3520) Total: 319047851 Avg: 90638 Max: 591045(ts:20010.398434) Min:3437(ts:20011.032217)
  Event: func: vfs_statx() (3865) Total: 49642741 Avg: 12844 Max: 1411479(ts:20010.626101) Min:924(ts:20010.718592)
  Event: func: vfs_mkdir() (690) Total: 9175927 Avg: 13298 Max: 63201(ts:20010.305020) Min:8868(ts:20010.939694)
  Event: func: vfs_getattr() (9529) Total: 5968390 Avg: 626 Max: 31850(ts:20010.459588) Min:260(ts:20010.718595)
  Event: func: vfs_statx_fd() (5666) Total: 5513205 Avg: 973 Max: 32808(ts:20010.539845) Min:434(ts:20010.508351)
  Event: func: vfs_open() (5668) Total: 4346891 Avg: 766 Max: 52724(ts:20010.128451) Min:236(ts:20011.152689)
  Event: func: vfs_getattr_nosec() (9529) Total: 2162038 Avg: 226 Max: 18470(ts:20010.182797) Min:91(ts:20010.718877)

--profile對數據進行了統計和排序。它按照進程爲單位,對每個進程的監控函數的調用時間進行了統計,有調用次數總時長平均時長最大/小時長,並且默認按照總時長進行了排序。

這樣就很方便的找到哪個函數耗時最多,佔比有多少。比如上例中耗時最多的是vfs_read()

計算出所有函數的總體耗時,單位爲ns:

// `$6`指定了第6列`總時長`
sudo trace-cmd report --profile | grep "Event: func:" | awk '{print $6}' | awk '{sum+=$1}END{print sum}'

也可以按照其他維度對數據進行排序:

// 按照`平均時長`進行排序,`k8`指定了第8列`平均時長`
sudo trace-cmd report --profile | grep "Event: func:" | sort -k8 -n -r      

3. 細粒度分析

上一節中我們使用粗粒度分析的方法找出了耗時最長的函數爲vfs_read(),需要進一步分析vfs_read()的耗時究竟消耗在哪個子函數上。即Question 2

3.1 使用trace-cmd record -g func命令抓取數據

上一節已經闡述了,抓取函數內部所有子函數的執行時間,需要使用-g func選項。trace數據已經默認存儲到trace.dat文件中。

 sudo trace-cmd record -p function_graph -g vfs_read -F cp -r ~/perf perf.bak

3.2 使用trace-cmd report --profile命令分析數據

1、使用trace-cmd report命令可以把trace.dat解析成文本格式:

$ sudo trace-cmd report| more
...

              cp-3663  [006] 27162.447945: funcgraph_entry:                   |  vfs_read() {
              cp-3663  [006] 27162.447948: funcgraph_entry:                   |    smp_irq_work_interrupt() {
              cp-3663  [006] 27162.447948: funcgraph_entry:                   |      irq_enter() {
              cp-3663  [006] 27162.447948: funcgraph_entry:        0.122 us   |        rcu_irq_enter();
              cp-3663  [006] 27162.447949: funcgraph_exit:         0.396 us   |      }
              cp-3663  [006] 27162.447949: funcgraph_entry:                   |      __wake_up() {
              cp-3663  [006] 27162.447950: funcgraph_entry:                   |        __wake_up_common_lock() {
              cp-3663  [006] 27162.447950: funcgraph_entry:        0.093 us   |          _raw_spin_lock_irqsave();
              cp-3663  [006] 27162.447950: funcgraph_entry:        0.100 us   |          __wake_up_common();
              cp-3663  [006] 27162.447950: funcgraph_entry:        0.098 us   |          _raw_spin_unlock_irqrestore();
              cp-3663  [006] 27162.447950: funcgraph_exit:         0.666 us   |        }
              cp-3663  [006] 27162.447950: funcgraph_exit:         0.877 us   |      }

可以看到,抓出了函數的層次調用關係,以及在函數結束時打印出了函數執行時間。

2、trace-cmd report --profile命令對所有子函數進行統計:

$ sudo trace-cmd report --profile | more
...

task: cp-3663
  Event: func: vfs_read() (6011) Total: 1573004753 Avg: 261687 Max: 49374656(ts:27164.368077) Min:3394(ts:27162.503426)
  Event: func: __vfs_read() (6011) Total: 1530911551 Avg: 254685 Max: 49372111(ts:27164.368077) Min:1928(ts:27162.451416)
  Event: func: new_sync_read() (6009) Total: 1527333966 Avg: 254174 Max: 49371813(ts:27164.368076) Min:1891(ts:27162.503426)
  Event: func: ext4_file_read_iter() (6009) Total: 1523645073 Avg: 253560 Max: 49371449(ts:27164.368076) Min:1703(ts:27162.503426)
  Event: func: generic_file_read_iter() (6009) Total: 1519828915 Avg: 252925 Max: 49371191(ts:27164.368076) Min:1547(ts:27162.503426)
  Event: func: ondemand_readahead() (3073) Total: 755070718 Avg: 245711 Max: 2425224(ts:27164.891754) Min:282(ts:27162.906376)
  Event: func: __do_page_cache_readahead() (3073) Total: 752847053 Avg: 244987 Max: 2424918(ts:27164.891753) Min:91(ts:27162.906375)
  Event: func: page_cache_sync_readahead() (2484) Total: 406033723 Avg: 163459 Max: 1685271(ts:27162.480326) Min:477(ts:27162.906376)
  Event: func: ext4_readpages() (2966) Total: 365560885 Avg: 123250 Max: 1564052(ts:27162.480212) Min:19321(ts:27162.503496)

...

--profile對數據進行了統計和排序。它按照進程爲單位,對每個進程的監控函數vfs_read()及其子函數的調用時間進行了統計,有調用次數總時長平均時長最大/小時長,並且默認按照總時長進行了排序。

需要注意的是,上述的統計並沒有呈現出函數的調用關係,所以它們的時長可能是相互包含的。另外因爲任務切換的發生,數據中還記錄了一些非追蹤函數vfs_read()子函數以外的函數。但是以上的統計數據,對於排查重點函數還是非常有幫助的。

3.3 使用kernelshark圖形化分析數據

trace-cmd report --profile主要是使用統計的方式來找出熱點。如果要看vfs_read()一個具體的調用過程,除了使用上一節的trace-cmd report命令,還可以使用kernelshark圖形化的形式來查看。

下圖是使用kernelshark打開trace.dat文件,並且逐個分析vfs_read()子函數調用的示意圖:

在這裏插入圖片描述

關於kernelshark的詳細使用可以參考kernelshark guid

參考文檔:

1.Linux ftrace
2.build kernelshark
3.kernelshark guid
4.ftrace利器之trace-cmd和kernelshark
5.通過trace-cmd和kernelshark簡化Ftrace的使用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章