eBPF Talk: 实战经验之 bpf FD 泄漏分析
The following article is from eBPF Talk Author Leon Hwang
不经意间,基于 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 行代码里有一半多都是非核心业务逻辑、辅助运维运营等目的的代码。