bpftrace 在 Curve 使用總結
概述
Curve 是雲原生計算基金會 (CNCF) Sandbox 專案,是網易主導自研和開源的高效能、易運維、雲原生的分散式儲存系統,由塊儲存 CurveBS 和檔案系統 CurveFS 兩部分組成。
在版本開發過程中,避免不了出現各種各樣的問題。同時,問題出現的地方往往可能會缺乏日誌和監控等關鍵資訊。對於這些問題,通常的做法是新增必要的日誌、監控,然後重新進行復現。這樣的流程,延緩了故障定位修復的進度。為了規避這一問題,Curve 引入了一些故障定位的可觀測性工具和方法。下面對 bpftrace 進行簡要的介紹,以及使用中的實踐總結。
bpftrace 介紹
bpftrace 是基於 Linux 核心 eBPF 的高階跟蹤語言,利用 BCC 與核心 BPF 系統進行互動,同時也可以利用核心現有的跟蹤功能:kernel/user-level dynamic tracing、tracepoints等。語法類似於 awk 和 C,提供了豐富的功能,可以協助分析程式行為、效能瓶頸以及故障定位等。
bpftrace 基礎用法
下面以一個簡單的例子,介紹 bpftrace 的基礎用法:
#!/usr/bin/env bpftrace
kprobe:vfs_read {
@start[tid] = nsecs;
}
kretprobe:vfs_read / @start[tid] / {
@ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}
interval:s:3 {
print(@ns);
clear(@ns);
}
END {
clear(@ns);
clear(@start);
}
這個指令碼用於統計 read 系統呼叫的延遲,並以直方圖的形式顯示結果。
-
指令碼第3行的 kprobe:vfs_read 表示對核心中的 vfs_read 進行探測,在呼叫該函式時,會執行花括號中的 actions。這裡是記錄了一個時間戳,tid、 nsecs是 bpftrace 內建的變數,分別對應 thread id 和當前的納秒級別的時間戳。@start[tid] 定義了一個名為 start 的 map,key 是 thread id,value 為時間戳。
-
第5行的 kretprobe:vfs_read 表示在 vfs_read 呼叫返回處進行探測,在函式返回時,執行相應的 actions。花括號前的 / @start[tid] / 是一個過濾器,表示只有在 @start[tid] 有記錄時,才執行後續的 actions。這裡定義了一個名為 @ns 的變數,記錄的是 hist(nsecs - @start[tid]) 的返回結果。hist 是內建函式,將延遲以對數直方圖的形式儲存。delete(@start[tid]) 用於刪除對應的記錄。
-
第12行 interval:s:3 是一個定時器,表示每 3 秒執行一次。這裡是把 @ns 中記錄的延遲列印到控制檯,然後 clear(@ns) 清楚所有的記錄。
-
END 表示在最終結束時,比如 Ctrl+C 退出時執行。
將上面的指令碼儲存為 vfs_read_latency.bt,然後以 root 許可權執行 bpftrace vfs_read_latency.bt ,或給予可執行許可權後 ./vfs_read_latency.bt 可以看到如下的輸出結果:
Attaching 4 probes...
@ns:
[512, 1K) 297 |@@@@@ |
[1K, 2K) 3007 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[2K, 4K) 834 |@@@@@@@@@@@@@@ |
[4K, 8K) 58 |@ |
[8K, 16K) 13 | |
[16K, 32K) 12 | |
[32K, 64K) 2511 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[64K, 128K) 364 |@@@@@@ |
[128K, 256K) 976 |@@@@@@@@@@@@@@@@ |
[256K, 512K) 1 | |
@ns:
[256, 512) 70 | |
[512, 1K) 365 |@@@@@ |
[1K, 2K) 3645 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[2K, 4K) 371 |@@@@@ |
[4K, 8K) 89 |@ |
[8K, 16K) 106 |@ |
[16K, 32K) 45 | |
[32K, 64K) 2330 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[64K, 128K) 439 |@@@@@@ |
[128K, 256K) 996 |@@@@@@@@@@@@@@ |
[256K, 512K) 5 | |
@ns 後面就是每3秒的輸出結果,第一列是延遲的範圍,[512, 1K) 表示延遲在512ns到1ms之間。後面分別是在這個延遲範圍內的請求的個數,以及對應的直方圖顯示。
更詳細使用說明,請參考官方文件:bpftrace.adoc[1]、reference_guide[2]
動態追蹤 kprobes/uprobes 的工作方式
使用 kprobe 對核心進行動態探測的過程如下:
1. kprobe 註冊時,會儲存並替換被探測的指令(在 x86_64 上替換為int3)。
2. 當 CPU 執行到斷點時,斷點處理函式會將控制權轉交給 Kprobes,並執行與當前探測點相關的 pre_hander
3. Kprobes 會單步執行被探測的指令,然後執行與當前探測點相關的 post_handler
4. 繼續執行被探測點之後的指令
kretprobe 的過程比較類似,在註冊 kretprobe 時,會在函式入口處建立一個 kprobe。當函式被呼叫命中 kprobe 時,會儲存原有的函式返回地址,並替換為”蹦床“(trampoline)函式的地址。在函式呼叫返回時,控制權交給 trampoline,kretprobe 處理完成後,再次返回到之前被替換的地址。
下面講解 uprobe 探測前後的指令變化:
#include <stdio.h>
__attribute__((noinline)) void hello() {
printf("hello, world\n");
}
int main() {
getchar();
hello();
return 0;
}
#!/usr/bin/env bpftrace
uprobe:./a.out:hello {
printf("Hello\n");
}
在探測前,hello 函式的彙編指令如下:
(gdb) disass hello
Dump of assembler code for function hello:
0x00005630e9dbd145 <+0>: push %rbp
0x00005630e9dbd146 <+1>: mov %rsp,%rbp
0x00005630e9dbd149 <+4>: lea 0xeb4(%rip),%rdi # 0x5630e9dbe004
0x00005630e9dbd150 <+11>: callq 0x5630e9dbd030 <[email protected]>
0x00005630e9dbd155 <+16>: nop
0x00005630e9dbd156 <+17>: pop %rbp
0x00005630e9dbd157 <+18>: retq
End of assembler dump.
探測後的指令如下:
(gdb) disass hello
Dump of assembler code for function hello:
0x00005630e9dbd145 <+0>: int3
0x00005630e9dbd146 <+1>: mov %rsp,%rbp
0x00005630e9dbd149 <+4>: lea 0xeb4(%rip),%rdi # 0x5630e9dbe004
0x00005630e9dbd150 <+11>: callq 0x5630e9dbd030 <[email protected]>
0x00005630e9dbd155 <+16>: nop
0x00005630e9dbd156 <+17>: pop %rbp
0x00005630e9dbd157 <+18>: retq
End of assembler dump.
探測使用者態 C++ 程式
在探測 C 程式時,需要探測的函式名稱,就是程式碼中對應的函式名。對於一些簡單的 C++ 函式,這種方式也是可行的,即便存在函式過載(bpftrace v0.10.0 以上版本支援)。例如:
#include <iostream>
void hello() {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void hello(int i) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void hello(std::string name) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int main() {
hello();
hello(1);
hello("curve");
return 0;
}
$ bpftrace -e 'uprobe:./a.out:hello { printf("hello\n"); }'
Attaching 3 probes...
這裡的表示對 3 個函式添加了探測。但如果程式碼中存在自定義型別、名稱空間,或者需要探測成員函式時,上面的方式使用起來就會比較麻煩。此時,直接對函式重整後的名字進行探測會比較方便。
在 C++ 程式碼編譯的過程中,會對函式名連同其引數、名稱空間等進行一次名字重整(name mangling),使得不同的過載函式有不同的符號。
以上面的 demo 為例,使用 GCC 以 C 語言編譯時,函式名稱不會發生變化,
$ readelf -s a.out | grep hello
57: 0000000000001145 19 FUNC GLOBAL DEFAULT 14 hello
當以 C++ 語言進行編譯時,hello 就被修改為 _Z5hellov
$ readelf -s a.out | grep hello
67: 0000000000001145 19 FUNC GLOBAL DEFAULT 14 _Z5hellov
在進行過載後,也會有不同的符號:
void hello(); -> _Z5hellov
void hello(int a); -> _Z5helloi
void hello(double a); -> _Z5hellod
void hello(std::string name); -> _Z5helloNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
所以,我們需要找到重整後對應的符號,這裡會用到兩個工具 nm 和 c++filt,nm 是列出可執行檔案或動態連結庫中的符號,c++filt 可以恢復命名重整前的函式簽名。以下為例:
#include <iostream>
namespace curve {
struct Demo {
void hello(std::string name) {
std::cout << "hello, " << name << std::endl;
}
};
}
int main() {
curve::Demo demo;
demo.hello("curve");
return 0;
}
首先,使用 nm 列出可執行檔案中的所有符號,然後用 grep 過濾出對應的函式。其次,可以使用 c++filt 將符號轉換為函式對應的簽名。
$ nm a.out | grep Demo | grep hello # 查詢重整後的符號
00000000000012b4 W _ZN5curve4Demo5helloENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
$ c++filt 00000000000012b4 W _ZN5curve4Demo5helloENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE # 確認重整前的函式簽名
curve::Demo::hello(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
$ bpftrace -e 'uprobe:./a.out:_ZN5curve4Demo5helloENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE { printf("hello\n"); }'
Attaching 1 probe...
探測容器中執行的服務
Curve 檔案儲存目前使用 CurveAdm 進行容器化部署,所有的服務都執行在容器中,方便進行部署和運維操作。但是,也給使用 bpftrace 探測容器中執行的服務帶來了一些問題。
前面講到,在探測使用者態進行時,需要指定可執行檔案或者動態連結庫的物理路徑。由於容器中的環境與宿主機環境是相互隔離的,直接使用可執行檔案在容器內的路徑是不可行的。
一種方式是利用 docker inspect 命令檢視對應容器的 MergedDir 在宿主機上的路徑,然後把可執行檔案在容器內的路徑拼接到一起。例如,需要探測容器內 curve-fuse 程序中的 FuseOpWrite 函式,步驟如下:
$ docker inspect af19f9940 | grep "MergedDir"
"MergedDir": "/var/lib/docker/overlay2/32581ea24889ab2de49569e2c00c6d6f95cd9cc7016afcfbcfb7701e478681dd/merged",
$ bpftrace -e 'uprobe:/var/lib/docker/overlay2/32581ea24889ab2de49569e2c00c6d6f95cd9cc7016afcfbcfb7701e478681dd/merged/curvefs/client/sbin/curve-fuse:FuseOpWrite { printf("probe FuseOpWrite\n"); }'
Attaching 1 probe...
probe FuseOpWrite
另一種方式是,利用 docker inspect 檢視容器的 pid,/proc/${pid}/root 對應的就是上面的 MergedDir,
$ docker inspect af19f9940 | grep -m1 "Pid"
"Pid": 753858,
$ cd /proc/753858/root
$ df .
Filesystem 1K-blocks Used Available Use% Mounted on
overlay 1135634272 1040824488 37052960 97% /var/lib/docker/overlay2/32581ea24889ab2de49569e2c00c6d6f95cd9cc7016afcfbcfb7701e478681dd/merged
$ bpftrace -e 'uprobe:/proc/753858/root/curvefs/client/sbin/curve-fuse:FuseOpWrite { printf("probe FuseOpWrite\n"); }'
Attaching 1 probe...
probe FuseOpWrite
如果要在容器內直接執行 bpftrace,就不需要上述的步驟,但是需要在容器執行時,授予一些許可權。
具體可以參考:How to: run BpfTrace from a small alpine image, with least privileges. [3]
總結
本文簡單介紹了 bpftrace 的基本用法,以及探測 C++ 程式和容器中執行程序的方法。通過 bpftrace 動態追蹤的能力,能夠協助我們分析執行中程序的行為,加快了問題定位的進度。在實際的問題定位過程中,往往需要結合使用多種工具,例如,先通過日誌、監控等縮小問題的範圍,然後利用 bpftrace 對關鍵函式進行動態追蹤,從而確定問題的根源。
參考:
[1] https://github.com/iovisor/bpftrace/blob/master/man/adoc/bpftrace.adoc
[2] https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md
[3] https://itnext.io/how-to-run-bpftrace-from-a-small-alpine-image-and-with-least-privileges-379146fcfcf1?gi=e7f8d5499eb4
原創作者:吳漢卿,Curve Contributor
- 正式畢業!Apache Kyuubi 成為 Apache 基金會頂級專案!
- Curve 檔案儲存在 Elasticsearch 冷熱資料儲存中的應用實踐
- 新一代雲原生日誌架構 - Loggie的設計與實踐
- 揚州萬方:基於申威平臺的 Curve 塊儲存在高效能和超融合場景下的實踐
- bpftrace 在 Curve 使用總結
- Apache Kyuubi 高可用的雲原生實現
- 網易數帆首發資料生產力模型,加速沉澱企業資料資產
- Envoy 有狀態會話保持機制設計與實現
- 率先實現中臺與BI的天然協同,網易數帆正在上演一場“全數”突圍!丨資料猿專訪
- Apache Hudi X Apache Kyuubi,中國移動雲湖倉一體的探索與實踐
- Apache Kyuubi 在小米大資料平臺的應用實踐
- 基於Impala的高效能數倉建設實踐之虛擬數倉
- 開源流式湖倉服務Arctic詳解:並非另一套Table Format
- 從 Delta 2.0 開始聊聊我們需要怎樣的資料湖
- 入選愛分析·銀行數字化廠商全景報告,網易數帆助力金融數字化場景落地
- 網易數帆陳諤:雲原生“牽手”低程式碼,加速企業數字化轉型丨資料猿專訪
- 攜程 Spark 多租戶查詢服務演進,Apache Kyuubi 未來可期
- 國產開源儲存之光:Curve 通過信創認證
- Apache Kyuubi 在愛奇藝的時間:加速 Hive SQL 遷移 Spark
- Curve 替換 Ceph 在網易雲音樂的實踐