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的使用