eBPF Talk: 實戰經驗之 bpf FD 泄漏分析
不經意間,基於 XDP 的網關已寫了 1w 行 Go 代碼;特別是其中 ACL 模塊較爲複雜。
因而,擔心因複雜性而帶來的一些資源管理隱患,特別是不好管理的 FD 資源,專門打造了一個工具用來分析 bpf 相關的 FD 是否泄漏了。
能分析 FD 泄漏的前提是:應用程序裏將所有 bpf obj 都 pin 到 bpffs;以 bpffs pinned bpf obj 爲基準判斷進程內的 FD 是否泄漏了。
分析 FD
衆所周知,FD 屬於進程內獨享的資源;所以進程外的工具無法分析 FD 的具體內容。
不過,可以在進程內提供 HTTP API 獲取 FD 相關信息。
# ll /proc/${PID}/fd
total 0
dr-x------ 2 root root 20 Mar 7 13:26 .
dr-xr-xr-x 9 root root 0 Mar 7 13:26 ..
lrwx------ 1 root root 64 Mar 7 13:26 0 -> /dev/pts/2
lrwx------ 1 root root 64 Mar 7 13:26 1 -> /dev/pts/2
lrwx------ 1 root root 64 Mar 7 13:26 10 -> anon_inode:bpf-prog
lrwx------ 1 root root 64 Mar 7 13:26 11 -> anon_inode:bpf-map
lrwx------ 1 root root 64 Mar 7 13:26 12 -> 'anon_inode:[eventfd]'
lrwx------ 1 root root 64 Mar 7 13:26 13 -> anon_inode:bpf-prog
lrwx------ 1 root root 64 Mar 7 13:26 14 -> anon_inode:bpf-prog
lrwx------ 1 root root 64 Mar 7 13:26 15 -> 'anon_inode:[perf_event]'
lrwx------ 1 root root 64 Mar 7 13:26 16 -> 'anon_inode:[perf_event]'
lrwx------ 1 root root 64 Mar 7 13:26 17 -> 'anon_inode:[perf_event]'
lrwx------ 1 root root 64 Mar 7 13:26 18 -> 'anon_inode:[perf_event]'
lrwx------ 1 root root 64 Mar 7 13:26 19 -> anon_inode:bpf-map
lrwx------ 1 root root 64 Mar 7 13:26 2 -> /dev/pts/2
lrwx------ 1 root root 64 Mar 7 13:26 3 -> anon_inode:bpf-map
lrwx------ 1 root root 64 Mar 7 13:26 4 -> 'anon_inode:[eventpoll]'
lr-x------ 1 root root 64 Mar 7 13:26 5 -> 'pipe:[33676]'
l-wx------ 1 root root 64 Mar 7 13:26 6 -> 'pipe:[33676]'
lrwx------ 1 root root 64 Mar 7 13:26 7 -> 'anon_inode:[eventpoll]'
lrwx------ 1 root root 64 Mar 7 13:26 8 -> anon_inode:bpf-map
lrwx------ 1 root root 64 Mar 7 13:26 9 -> anon_inode:bpf-prog
而在分析 FD 的時候,需要注意以下兩個地方。
注意 1: FD 的 bpf 信息
由 ll /proc/${PID}/fd
可知,每個 FD 都是一個 symbol link。對於 bpf FD 而言:
# ll /proc/${PID}/fd
10 -> anon_inode:bpf-prog
11 -> anon_inode:bpf-map
19 -> anon_inode:bpf-link
# readlink /proc/${PID}/fd/11
anon_inode:bpf-map
所以,通過 readlink
可以知道當前 FD 是 bpf prog、bpf map 還是 bpf link。
注意 2: cilium/ebpf 從 FD 讀取 bpf obj 信息
對於 bpf prog 和 bpf map,cilium/ebpf
分別提供了 NewProgramFromFD()
和 NewMapFromFD()
。
P.S. 對於 bpf link,cilium/ebpf
沒有提供 GetLinkInfoFromFD()
這樣的函數。
根據 readlink
得到的 anon_inode:bpf-xxx
信息,再按需調 NewProgramFromFD()
和 NewMapFromFD()
去獲取 bpf obj 的信息。
不過,留意一下這兩個函數的 doc,都有提示 "You should not use fd after calling this function."。
所以,爲了不破壞原有的 FD,可以用 syscall.Dup()
復刻一個 FD;然後使用復刻出來的 FD 讀取 bpf obj 信息。
因爲 NewProgramFromFD()
和 NewMapFromFD()
會產生新的 FD,所以需要在分析 FD 前獲取目錄 /proc/${PID}/fd
下的所有文件名稱,避免死循環產生無限的 FD。
遍歷 bpffs
在遍歷 bpffs 獲取 pinned bpf obj 時,參考 bpftool prog show pinned /path/to/pinned/bpf/obj
和 bpftool map show pinned /path/to/pinned/bpf/obj
的實現方式,通過 pinned bpf obj 的文件路徑獲取 bpf obj 信息。
問題是:bpftool
是怎麼區分 pinned bpf obj 的文件路徑對應的 bpf obj 的類型的?
# bpftool map show pinned /sys/fs/bpf/trace
Error: incorrect object type: prog
翻看 bpftool
源代碼,其中判斷 bpf obj 類型的代碼如下:
// ${KERNEL}/tools/bpf/bpftool/common.c
int open_obj_pinned_any(const char *path, enum bpf_obj_type exp_type)
{
enum bpf_obj_type type;
int fd;
fd = open_obj_pinned(path, false);
if (fd < 0)
return -1;
type = get_fd_type(fd);
if (type < 0) {
close(fd);
return type;
}
if (type != exp_type) {
p_err("incorrect object type: %s", get_fd_type_name(type));
close(fd);
return -1;
}
return fd;
}
int get_fd_type(int fd)
{
char path[PATH_MAX];
char buf[512];
ssize_t n;
snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
n = readlink(path, buf, sizeof(buf));
// ...
if (strstr(buf, "bpf-map"))
return BPF_OBJ_MAP;
else if (strstr(buf, "bpf-prog"))
return BPF_OBJ_PROG;
else if (strstr(buf, "bpf-link"))
return BPF_OBJ_LINK;
return BPF_OBJ_UNKNOWN;
}
看到了麼?readlink()
。這不就是從 FD 裏獲取到的麼?
因而,在 Go 裏可以這麼處理:
linkname, e := readLinkname(fpath)
// ...
switch {
case strings.HasSuffix(linkname, "bpf-prog"):
pfd, e := readBPFProgInfo(fpath)
// ...
pfd.Path = fpath
progFDs = append(progFDs, pfd)
case strings.HasSuffix(linkname, "bpf-map"):
mfd, e := readBPFMapInfo(fpath)
// ...
mfd.Path = fpath
mapFDs = append(mapFDs, mfd)
case strings.HasSuffix(linkname, "bpf-link"):
lfd, e := readBPFLinkInfo(fpath)
// ...
lfd.Path = fpath
lfd.Prog.Path = fpath
linkFDs = append(linkFDs, lfd)
default:
// failure, invalid filepath
err = fmt.Errorf("%s is not a bpf prog or a bpf map or a bpf link", fpath)
return
}
func readLinkname(fpath string) (string, error) {
p, err := ebpf.LoadPinnedProgram(fpath, nil)
if err != nil {
return "", fmt.Errorf("failed to load pinned prog from %s: %w", fpath, err)
}
defer p.Close()
return fd.ReadLink(fmt.Sprintf("/proc/self/fd/%d", p.FD()))
}
func readBPFProgInfo(fpath string) (*fd.ProgFDInfo, error) {
p, err := ebpf.LoadPinnedProgram(fpath, nil)
if err != nil {
return nil, fmt.Errorf("failed to load pinned prog from %s: %w", fpath, err)
}
defer p.Close()
return fd.GetBPFProgInfo(p)
}
func readBPFMapInfo(fpath string) (*fd.MapFDInfo, error) {
m, err := ebpf.LoadPinnedMap(fpath, nil)
if err != nil {
return nil, fmt.Errorf("failed to load pinned map from %s: %w", fpath, err)
}
defer m.Close()
return fd.GetBPFMapInfo(m)
}
func readBPFLinkInfo(fpath string) (*fd.LinkFDInfo, error) {
l, err := link.LoadPinnedLink(fpath, nil)
if err != nil {
return nil, fmt.Errorf("failed to load pinned link from %s: %w", fpath, err)
}
defer l.Close()
return fd.GetBPFLinkInfo(l)
}
bpf FD 泄漏分析
分析起來就比較簡單了,因爲每個 bpf obj 都有一個唯一的 ID。
-
不在 bpffs 下的 bpf obj ID,都是泄漏的 bpf obj。
-
有多個 FD 指向同一個 bpf obj ID,這些 FD 有泄漏的可能。
效果如下:
# ./xdp-tool leak bpf
bpf map:
Sure leak:
Possible leak:
/sys/bpf/fs/global_ep
ID=210414 FD=11 Map(name=global_ep type=Hash keySize=8 valueSize=12 maxEntries=1000000 flags=5)
ID=210414 FD=26 Map(name=global_ep type=Hash keySize=8 valueSize=12 maxEntries=1000000 flags=5)
/sys/bpf/fs/acl_progs
ID=210416 FD=15 Map(name=acl_progs type=ProgramArray keySize=4 valueSize=4 maxEntries=1024 flags=4)
ID=210416 FD=32 Map(name=acl_progs type=ProgramArray keySize=4 valueSize=4 maxEntries=1024 flags=4)
/sys/bpf/fs/delay
ID=210417 FD=16 Map(name=delay type=Hash keySize=8 valueSize=1 maxEntries=1000000 flags=5)
ID=210417 FD=33 Map(name=delay type=Hash keySize=8 valueSize=1 maxEntries=1000000 flags=5)
/sys/bpf/fs/delay_cidr
ID=210418 FD=17 Map(name=delay_cidr type=Array keySize=4 valueSize=240 maxEntries=1 flags=4)
ID=210418 FD=34 Map(name=delay_cidr type=Array keySize=4 valueSize=240 maxEntries=1 flags=4)
bpf prog:
No leak!
總結
bpf FD 泄漏分析,有助於瞭解泄漏的、可能泄漏的 bpf obj 的具體信息;比如 pinned 路徑、FD、bpf map 的 name、type 等定義信息、bpf prog 的 name、tag 等信息。從而快速定位存在 FD 泄漏的代碼位置。
你看,那 1w 行代碼裏有一半多都是非核心業務邏輯、輔助運維運營等目的的代碼。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/w-UPzNPah8j1O-_q6_pZEA