不经意间,基于 XDP 的网关已写了 1w 行 Go 代码;特别是其中 ACL 模块较为复杂。

因而,担心因复杂性而带来的一些资源管理隐患,特别是不好管理的 FD 资源,专门打造了一个工具用来分析 bpf 相关的 FD 是否泄漏了。

能分析 FD 泄漏的前提是:应用程序里将所有 bpf obj 都 pin 到 bpffs;以 bpffs pinned bpf obj 为基准判断进程内的 FD 是否泄漏了。

分析 FD

众所周知,FD 属于进程内独享的资源;所以进程外的工具无法分析 FD 的具体内容。

不过,可以在进程内提供 HTTP API 获取 FD 相关信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 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 而言:

1
2
3
4
5
6
# 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/objbpftool map show pinned /path/to/pinned/bpf/obj 的实现方式,通过 pinned bpf obj 的文件路径获取 bpf obj 信息。

问题是:bpftool 是怎么区分 pinned bpf obj 的文件路径对应的 bpf obj 的类型的?

1
2
# bpftool map show pinned /sys/fs/bpf/trace
Error: incorrect object type: prog

翻看 bpftool 源代码,其中判断 bpf obj 类型的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ${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 里可以这么处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

        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。

  1. 不在 bpffs 下的 bpf obj ID,都是泄漏的 bpf obj。
  2. 有多个 FD 指向同一个 bpf obj ID,这些 FD 有泄漏的可能。

效果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# ./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 行代码里有一半多都是非核心业务逻辑、辅助运维运营等目的的代码。