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/objbpftool 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。

  1. 不在 bpffs 下的 bpf obj ID,都是泄漏的 bpf obj。

  2. 有多個 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=valueSize=12 maxEntries=1000000 flags=5)
ID=210414 FD=26 Map(name=global_ep type=Hash keySize=valueSize=12 maxEntries=1000000 flags=5)
/sys/bpf/fs/acl_progs
ID=210416 FD=15 Map(name=acl_progs type=ProgramArray keySize=valueSize=maxEntries=1024 flags=4)
ID=210416 FD=32 Map(name=acl_progs type=ProgramArray keySize=valueSize=maxEntries=1024 flags=4)
/sys/bpf/fs/delay
ID=210417 FD=16 Map(name=delay type=Hash keySize=valueSize=maxEntries=1000000 flags=5)
ID=210417 FD=33 Map(name=delay type=Hash keySize=valueSize=maxEntries=1000000 flags=5)
/sys/bpf/fs/delay_cidr
ID=210418 FD=17 Map(name=delay_cidr type=Array keySize=valueSize=240 maxEntries=flags=4)
ID=210418 FD=34 Map(name=delay_cidr type=Array keySize=valueSize=240 maxEntries=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