一、上節回顧
上一節,我以 ksoftirqd CPU 使用率高的問題為例,帶你一起學習了內核線程 CPU 使用率高時的分析方法。先簡單回顧一下。
當碰到內核線程的資源使用異常時,很多常用的進程級性能工具,並不能直接用到內核線程上。這時,我們就可以使用內核自帶的 perf 來觀察它們的行為,找出熱點函數,進一步定位性能瓶
頸。不過,perf 產生的匯總報告並不直觀,所以我通常也推薦用火焰圖來協助排查。
其實,使用 perf 對系統內核線程進行分析時,內核線程依然還在正常運行中,所以這種方法也被稱為動態追蹤技術。
動態追蹤技術,通過探針機制,來采集內核或者應用程序的運行信息,從而可以不用修改內核和應用程序的代碼,就獲得豐富的信息,幫你分析、定位想要排查的問題。
以往,在排查和調試性能問題時,我們往往需要先為應用程序設置一系列的斷點(比如使用GDB),然后以手動或者腳本(比如 GDB 的 Python 擴展)的方式,在這些斷點處分析應用程
序的狀態。或者,增加一系列的日志,從日志中尋找線索。
不過,斷點往往會中斷應用的正常運行;而增加新的日志,往往需要重新編譯和部署。這些方法雖然在今天依然廣泛使用,但在排查復雜的性能問題時,往往耗時耗力,更會對應用的正常運行
造成巨大影響。
此外,這類方式還有大量的性能問題。比如,出現的概率小,只有線上環境才能碰到。這種難以復現的問題,亦是一個巨大挑戰。
而動態追蹤技術的出現,就為這些問題提供了完美的方案:它既不需要停止服務,也不需要修改應用程序的代碼;所有一切還按照原來的方式正常運行時,就可以幫你分析出問題的根源。
同時,相比以往的進程級跟蹤方法(比如 ptrace),動態追蹤往往只會帶來很小的性能損耗(通常在 5% 或者更少)。
既然動態追蹤有這么多好處,那么,都有哪些動態追蹤的方法,又該如何使用這些動態追蹤方法呢?今天,我就帶你一起來看看這個問題。由於動態追蹤涉及的知識比較多,我將分為上、下兩
篇為你講解,先來看今天這部分內容。
二、動態追蹤
說到動態追蹤(Dynamic Tracing),就不得不提源於 Solaris 系統的 DTrace。DTrace 是動態追蹤技術的鼻祖,它提供了一個通用的觀測框架,並可以使用 D 語言進行自由擴展。
DTrace 的工作原理如下圖所示。它的運行常駐在內核中,用戶可以通過 dtrace 命令,把 D 語言編寫的追蹤腳本,提交到內核中的運行時來執行。DTrace 可以跟蹤用戶態和內核態的所有事
件,並通過一些列的優化措施,保證最小的性能開銷。
雖然直到今天,DTrace 本身依然無法在 Linux 中運行,但它同樣對 Linux 動態追蹤產生了巨大的影響。很多工程師都嘗試過把 DTrace 移植到 Linux 中,這其中,最著名的就是 RedHat 主推的 SystemTap。
同 DTrace 一樣,SystemTap 也定義了一種類似的腳本語言,方便用戶根據需要自由擴展。不過,不同於 DTrace,SystemTap 並沒有常駐內核的運行時,它需要先把腳本編譯為內核模塊,
然后再插入到內核中執行。這也導致 SystemTap 啟動比較緩慢,並且依賴於完整的調試符號表。
總的來說,為了追蹤內核或用戶空間的事件,Dtrace 和 SystemTap 都會把用戶傳入的追蹤處理函數(一般稱為 Action),關聯到被稱為探針的檢測點上。這些探針,實際上也就是各種動態追
蹤技術所依賴的事件源。
三、動態追蹤的事件源
根據事件類型的不同,動態追蹤所使用的事件源,可以分為靜態探針、動態探針以及硬件事件等三類。它們的關系如下圖所示:
其中,硬件事件通常由性能監控計數器 PMC(Performance Monitoring Counter)產生,包括了各種硬件的性能情況,比如 CPU 的緩存、指令周期、分支預測等等。
靜態探針,是指事先在代碼中定義好,並編譯到應用程序或者內核中的探針。這些探針只有在開啟探測功能時,才會被執行到;未開啟時並不會執行。常見的靜態探針包括內核中的跟蹤點
(tracepoints)和 USDT(Userland Statically Defined Tracing)探針。
- 跟蹤點(tracepoints),實際上就是在源碼中插入的一些帶有控制條件的探測點,這些探測點允許事后再添加處理函數。比如在內核中,最常見的靜態跟蹤方法就是 printk,即輸出日志。Linux 內核定義了大量的跟蹤點,可以通過內核編譯選項,來開啟或者關閉。
- USDT 探針,全稱是用戶級靜態定義跟蹤,需要在源碼中插入 DTRACE_PROBE() 代碼,並編譯到應用程序中。不過,也有很多應用程序內置了 USDT 探針,比如 MySQL、PostgreSQL等。
動態探針,則是指沒有事先在代碼中定義,但卻可以在運行時動態添加的探針,比如函數的調用和返回等。動態探針支持按需在內核或者應用程序中添加探測點,具有更高的靈活性。常見的動
態探針有兩種,即用於內核態的 kprobes 和用於用戶態的 uprobes。
- kprobes 用來跟蹤內核態的函數,包括用於函數調用的 kprobe 和用於函數返回的kretprobe。
- uprobes 用來跟蹤用戶態的函數,包括用於函數調用的 uprobe 和用於函數返回的uretprobe。
注意,kprobes 需要內核編譯時開啟 CONFIG_KPROBE_EVENTS;而 uprobes則需要內核編譯時開啟 CONFIG_UPROBE_EVENTS。
四、動態追蹤機制
而在這些探針的基礎上,Linux 也提供了一系列的動態追蹤機制,比如 ftrace、perf、eBPF 等。
ftrace 最早用於函數跟蹤,后來又擴展支持了各種事件跟蹤功能。ftrace 的使用接口跟我們之前提到的 procfs 類似,它通過 debugfs(4.1 以后也支持 tracefs),以普通文件的形式,向用戶
空間提供訪問接口。
這樣,不需要額外的工具,你就可以通過掛載點(通常為 /sys/kernel/debug/tracing 目錄)內的文件讀寫,來跟 ftrace 交互,跟蹤內核或者應用程序的運行事件。
perf 是我們的老朋友了,我們在前面的好多案例中,都使用了它的事件記錄和分析功能,這實際上只是一種最簡單的靜態跟蹤機制。你也可以通過 perf ,來自定義動態事件(perf probe),
只關注真正感興趣的事件。
eBPF 則在 BPF(Berkeley Packet Filter)的基礎上擴展而來,不僅支持事件跟蹤機制,還可以通過自定義的 BPF 代碼(使用 C 語言)來自由擴展。所以,eBPF 實際上就是常駐於內核的運行
時,可以說就是 Linux 版的 DTrace。
除此之外,還有很多內核外的工具,也提供了豐富的動態追蹤功能。最常見的就是前面提到的SystemTap,我們之前多次使用過的 BCC(BPF Compiler Collection),以及常用於容器性能
分析的 sysdig 等。
而在分析大量事件時,使用我們上節課提到的火焰圖,可以將大量數據可視化展示,讓你更直觀發現潛在的問題。
接下來,我就通過幾個例子,帶你來看看,要怎么使用這些機制,來動態追蹤內核和應用程序的執行情況。以下案例還是基於 Ubuntu 18.04 系統,同樣適用於其他系統。
注意,以下所有命令都默認以 root 用戶運行,如果你用普通用戶身份登陸系統,請運行 sudo su root 命令切換到 root 用戶。
五、ftrace
我們先來看 ftrace。剛剛提到過,ftrace 通過 debugfs(或者 tracefs),為用戶空間提供接口。所以使用 ftrace,往往是從切換到 debugfs 的掛載點開始。
cd /sys/kernel/debug/tracing $ ls README instances set_ftrace_notrace trace_marker_raw available_events kprobe_events set_ftrace_pid trace_options ...
如果這個目錄不存在,則說明你的系統還沒有掛載 debugfs,你可以執行下面的命令來掛載它:
mount -t debugfs nodev /sys/kernel/debug
ftrace 提供了多個跟蹤器,用於跟蹤不同類型的信息,比如函數調用、中斷關閉、進程調度等。具體支持的跟蹤器取決於系統配置,你可以執行下面的命令,來查詢所有支持的跟蹤器:
cat available_tracers hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop
這其中,function 表示跟蹤函數的執行,function_graph 則是跟蹤函數的調用關系,也就是生成直觀的調用關系圖。這便是最常用的兩種跟蹤器。
除了跟蹤器外,使用 ftrace 前,還需要確認跟蹤目標,包括內核函數和內核事件。其中,
- 函數就是內核中的函數名。
- 而事件,則是內核源碼中預先定義的跟蹤點。
同樣地,你可以執行下面的命令,來查詢支持的函數和事件:
cat available_filter_functions $ cat available_events
明白了這些基本信息,接下來,我就以 ls 命令為例,帶你一起看看 ftrace 的使用方法。
為了列出文件,ls 命令會通過 open 系統調用打開目錄文件,而 open 在內核中對應的函數名為do_sys_open。 所以,我們要做的第一步,就是把要跟蹤的函數設置為 do_sys_open:
echo do_sys_open > set_graph_function
接下來,第二步,配置跟蹤選項,開啟函數調用跟蹤,並跟蹤調用進程:
echo function_graph > current_tracer $ echo funcgraph-proc > trace_options
接着,第三步,也就是開啟跟蹤:
echo 1 > tracing_on
第四步,執行一個 ls 命令后,再關閉跟蹤:
ls $ echo 0 > tracing_on
實際測試代碼如下:
[root@luoahong tracing]# cat available_tracers blk function_graph wakeup_dl wakeup_rt wakeup function nop [root@luoahong tracing]# cat available_events|head kvmmmu:kvm_mmu_pagetable_walk kvmmmu:kvm_mmu_paging_element kvmmmu:kvm_mmu_set_accessed_bit kvmmmu:kvm_mmu_set_dirty_bit kvmmmu:kvm_mmu_walker_error kvmmmu:kvm_mmu_get_page kvmmmu:kvm_mmu_sync_page kvmmmu:kvm_mmu_unsync_page kvmmmu:kvm_mmu_prepare_zap_page kvmmmu:mark_mmio_spte [root@luoahong tracing]# echo do_sys_open > set_graph_function [root@luoahong tracing]# echo funcgraph-proc > trace_options -bash: echo: write error: Invalid argument [root@luoahong tracing]# echo function_graph > current_tracer [root@luoahong tracing]# echo funcgraph-proc > trace_options [root@luoahong tracing]# echo 1 > tracing_on [root@luoahong tracing]# ls available_events dynamic_events kprobe_events saved_cmdlines set_ftrace_pid timestamp_mode trace_stat available_filter_functions dyn_ftrace_total_info kprobe_profile saved_cmdlines_size set_graph_function trace tracing_cpumask available_tracers enabled_functions max_graph_depth saved_tgids set_graph_notrace trace_clock tracing_max_latency buffer_percent events options set_event snapshot trace_marker tracing_on buffer_size_kb free_buffer per_cpu set_event_pid stack_max_size trace_marker_raw tracing_thresh buffer_total_size_kb function_profile_enabled printk_formats set_ftrace_filter stack_trace trace_options uprobe_events current_tracer instances README set_ftrace_notrace stack_trace_filter trace_pipe uprobe_profile [root@luoahong tracing]# echo 0 > tracing_on
第五步,也是最后一步,查看跟蹤結果:
cat trace # tracer: function_graph # # CPU TASK/PID DURATION FUNCTION CALLS # | | | | | | | | | 0) ls-12276 | | do_sys_open() { 0) ls-12276 | | getname() { 0) ls-12276 | | getname_flags() { 0) ls-12276 | | kmem_cache_alloc() { 0) ls-12276 | | _cond_resched() { 0) ls-12276 | 0.049 us | rcu_all_qs(); 0) ls-12276 | 0.791 us | } 0) ls-12276 | 0.041 us | should_failslab(); 0) ls-12276 | 0.040 us | prefetch_freepointer(); 0) ls-12276 | 0.039 us | memcg_kmem_put_cache(); 0) ls-12276 | 2.895 us | } 0) ls-12276 | | __check_object_size() { 0) ls-12276 | 0.067 us | __virt_addr_valid(); 0) ls-12276 | 0.044 us | __check_heap_object(); 0) ls-12276 | 0.039 us | check_stack_object(); 0) ls-12276 | 1.570 us | } 0) ls-12276 | 5.790 us | } 0) ls-12276 | 6.325 us | } ...
實際測試代碼如下:
[root@luoahong tracing]# cat trace|head -n 100 # tracer: function_graph # # CPU TASK/PID DURATION FUNCTION CALLS # | | | | | | | | | 0) ls-9986 | | do_sys_open() { 0) ls-9986 | | getname() { ....... 0) ls-9986 | | dput() { 0) ls-9986 | | _cond_resched() { 0) ls-9986 | 0.184 us | rcu_all_qs(); 0) ls-9986 | 0.535 us | } 0) ls-9986 | 0.954 us | } 0) ls-9986 | 9.782 us | }
在最后得到的輸出中:
- 第一列表示運行的 CPU;
- 第二列是任務名稱和進程 PID;
- 第三列是函數執行延遲;
- 最后一列,則是函數調用關系圖。
你可以看到,函數調用圖,通過不同級別的縮進,直觀展示了各函數間的調用關系。
當然,我想你應該也發現了 ftrace 的使用缺點——五個步驟實在是麻煩,用起來並不方便。不過,不用擔心, trace-cmd 已經幫你把這些步驟給包裝了起來。這樣,你就可以在同一個命令行
工具里,完成上述所有過程。
你可以執行下面的命令,來安裝 trace-cmd :
# Ubuntu $ apt-get install trace-cmd # CentOS $ yum install trace-cmd
安裝好后,原本的五步跟蹤過程,就可以簡化為下面這兩步:
trace-cmd record -p function_graph -g do_sys_open -O funcgraph-proc ls $ trace-cmd report ... ls-12418 [000] 85558.075341: funcgraph_entry: | do_sys_open() { ls-12418 [000] 85558.075363: funcgraph_entry: | getname() { ls-12418 [000] 85558.075364: funcgraph_entry: | getname_flags() { ls-12418 [000] 85558.075364: funcgraph_entry: | kmem_cache_alloc() { ls-12418 [000] 85558.075365: funcgraph_entry: | _cond_resched() { ls-12418 [000] 85558.075365: funcgraph_entry: 0.074 us | rcu_all_qs(); ls-12418 [000] 85558.075366: funcgraph_exit: 1.143 us | } ls-12418 [000] 85558.075366: funcgraph_entry: 0.064 us | should_failslab(); ls-12418 [000] 85558.075367: funcgraph_entry: 0.075 us | prefetch_freepointer(); ls-12418 [000] 85558.075368: funcgraph_entry: 0.085 us | memcg_kmem_put_cache(); ls-12418 [000] 85558.075369: funcgraph_exit: 4.447 us | } ls-12418 [000] 85558.075369: funcgraph_entry: | __check_object_size() { ls-12418 [000] 85558.075370: funcgraph_entry: 0.132 us | __virt_addr_valid(); ls-12418 [000] 85558.075370: funcgraph_entry: 0.093 us | __check_heap_object(); ls-12418 [000] 85558.075371: funcgraph_entry: 0.059 us | check_stack_object(); ls-12418 [000] 85558.075372: funcgraph_exit: 2.323 us | } ls-12418 [000] 85558.075372: funcgraph_exit: 8.411 us | } ls-12418 [000] 85558.075373: funcgraph_exit: 9.195 us | } ...
實際測試代碼如下:
[root@luoahong ~]# trace-cmd report|head -n 100 CPU 0 is empty cpus=2 ls-10015 [001] 3232.717769: funcgraph_entry: 1.802 us | mutex_unlock(); ls-10015 [001] 3232.720548: funcgraph_entry: | do_sys_open() { ls-10015 [001] 3232.720549: funcgraph_entry: | getname() { ls-10015 [001] 3232.720549: funcgraph_entry: | getname_flags() { ls-10015 [001] 3232.720549: funcgraph_entry: | kmem_cache_alloc() { ls-10015 [001] 3232.720550: funcgraph_entry: | _cond_resched() { ls-10015 [001] 3232.720550: funcgraph_entry: 0.206 us | rcu_all_qs(); ls-10015 [001] 3232.720550: funcgraph_exit: 0.724 us | } ls-10015 [001] 3232.720550: funcgraph_entry: 0.169 us | should_failslab(); ls-10015 [001] 3232.720551: funcgraph_entry: 0.177 us | memcg_kmem_put_cache(); ls-10015 [001] 3232.720551: funcgraph_exit: 1.792 us | } ls-10015 [001] 3232.720551: funcgraph_entry: | __check_object_size() { ls-10015 [001] 3232.720552: funcgraph_entry: 0.167 us | check_stack_object(); ls-10015 [001] 3232.720552: funcgraph_entry: 0.207 us | __virt_addr_valid(); ls-10015 [001] 3232.720552: funcgraph_entry: 0.212 us | __check_heap_object(); ls-10015 [001] 3232.720553: funcgraph_exit: 1.286 us | } ls-10015 [001] 3232.720553: funcgraph_exit: 3.744 us | } ls-10015 [001] 3232.720553: funcgraph_exit: 4.134 us | }
你會發現,trace-cmd 的輸出,跟上述 cat trace 的輸出是類似的。
通過這個例子我們知道,當你想要了解某個內核函數的調用過程時,使用 ftrace ,就可以跟蹤到它的執行過程。
六、小結
今天,我帶你一起學習了常見的動態追蹤方法。所謂動態追蹤,就是在系統或應用程序正常運行時,通過內核中提供的探針來動態追蹤它們的行為,從而輔助排查出性能瓶頸。
而在 Linux 系統中,常見的動態追蹤方法包括 ftrace、perf、eBPF 以及 SystemTap 等。當你已經定位了某個內核函數,但不清楚它的實現原理時,就可以用 ftrace 來跟蹤它的執行過程。至於
其他動態追蹤方法,我將在下節課繼續為你詳細解讀。