因为 XDP ACL 需要不断操作数据面的 XDP,担心 Go 代码逻辑会导致一些性能问题,所以参考 syscalldist 开发了一个专门针对 BPF 系统调用的 bpfsyscalldist

性能场景

拉起几百个 goroutine 同时对一个 PROG_ARRAY bpf map 进行操作,会产生什么性能问题呢?

通常情况下,对 bpf map 进行增删处理,都可以先进行一次 lookup,lookup 后再看情况是否要进行增删操作。这是因为一次 lookup 的性能消耗非常低,即内核里的一次 RCU 读操作。

可是,PROG_ARRAY bpf map 就无法使用 lookup 进行优化,因为 PROG_ARRAY bpf map 不支持 lookup。

 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
// ${KERNEL}/kernel/bpf/arraymap.c

const struct bpf_map_ops prog_array_map_ops = {
    // ...
    .map_lookup_elem = fd_array_map_lookup_elem,
    .map_delete_elem = fd_array_map_delete_elem,
    // ...
};

static void *fd_array_map_lookup_elem(struct bpf_map *map, void *key)
{
    return ERR_PTR(-EOPNOTSUPP);
}

static int fd_array_map_delete_elem(struct bpf_map *map, void *key)
{
    struct bpf_array *array = container_of(map, struct bpf_array, map);
    void *old_ptr;
    u32 index = *(u32 *)key;

    if (index >= array->map.max_entries)
        return -E2BIG;

    if (map->ops->map_poke_run) {
        mutex_lock(&array->aux->poke_mutex);            // mutex lock here
        old_ptr = xchg(array->ptrs + index, NULL);
        map->ops->map_poke_run(map, index, old_ptr, NULL);
        mutex_unlock(&array->aux->poke_mutex);
    } else {
        old_ptr = xchg(array->ptrs + index, NULL);
    }

    if (old_ptr) {
        map->ops->map_fd_put_ptr(old_ptr);
        return 0;
    } else {
        return -ENOENT;
    }
}

更新 PROG_ARRAY bpf map 也会进行 mutex lock

 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
map_update_elem()                           // ${KERNEL}/kernel/bpf/syscall.c
|-->bpf_map_update_value()
    |-->bpf_fd_array_map_update_elem()
        |-->bpf_fd_array_map_update_elem()  // ${KERNEL}/kernel/bpf/arraymap.c

// ${KERNEL}/kernel/bpf/arraymap.c

int bpf_fd_array_map_update_elem(struct bpf_map *map, struct file *map_file,
                 void *key, void *value, u64 map_flags)
{
    struct bpf_array *array = container_of(map, struct bpf_array, map);
    void *new_ptr, *old_ptr;
    u32 index = *(u32 *)key, ufd;

    if (map_flags != BPF_ANY)
        return -EINVAL;

    if (index >= array->map.max_entries)
        return -E2BIG;

    ufd = *(u32 *)value;
    new_ptr = map->ops->map_fd_get_ptr(map, map_file, ufd);
    if (IS_ERR(new_ptr))
        return PTR_ERR(new_ptr);

    if (map->ops->map_poke_run) {
        mutex_lock(&array->aux->poke_mutex);                // mutex lock
        old_ptr = xchg(array->ptrs + index, new_ptr);
        map->ops->map_poke_run(map, index, old_ptr, new_ptr);
        mutex_unlock(&array->aux->poke_mutex);
    } else {
        old_ptr = xchg(array->ptrs + index, new_ptr);
    }

    if (old_ptr)
        map->ops->map_fd_put_ptr(old_ptr);
    return 0;
}

所以,如果真的有几百个 goroutine 同时操作一个 PROG_ARRAY bpf map,就会导致 Go 程序里的线程数量暴涨;因为线程对应 Go runtime 里的 MMmutex lock 不能被 runtime scheduler 调度而不断创建新的线程。

庆幸的是,在 XDP ACL 里启动了几百个 goroutine,但它们并没有并发地操作一个 PROG_ARRAY bpf map。不过,既然了解到该性能缺陷,就得优化一下它。

优化办法:使用 Go sync.Mutex 替代内核里的 mutex lock。将被 block 的对象从线程变为 goroutineM => G),因为 G 被 block 后可以被调度走,从而不占用 M

bpfsyscalldist

尽管说并没有看到 Go 程序线程数量暴涨,因担心性能问题,还是开发了一个 BPF 系统调用的分析工具 bpfsyscalldist

用法比较简单:

 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
# ./bpfsyscalldist -h
Usage of ./bpfsyscalldist:
      --pid uint32   filter pid
pflag: help requested
# ./bpfsyscalldist --pid 273496
2023/02/28 12:49:44 Attached kprobe(__sys_bpf)
2023/02/28 12:49:44 Attached kretprobe(__sys_bpf)
2023/02/28 12:49:44 Hit Ctrl-C to end.
^C
Histogram for syscall(BPF) cmd(BPF_PROG_LOAD) (sum 54):
     usecs               : count         distribution
         0 -> 1          : 0             |                                        |
         2 -> 3          : 0             |                                        |
         4 -> 7          : 0             |                                        |
         8 -> 15         : 0             |                                        |
        16 -> 31         : 0             |                                        |
        32 -> 63         : 0             |                                        |
        64 -> 127        : 0             |                                        |
       128 -> 255        : 0             |                                        |
       256 -> 511        : 0             |                                        |
       512 -> 1023       : 0             |                                        |
      1024 -> 2047       : 0             |                                        |
      2048 -> 4095       : 0             |                                        |
      4096 -> 8191       : 9             |***************                         |
      8192 -> 16383      : 10            |****************                        |
     16384 -> 32767      : 24            |****************************************|
     32768 -> 65535      : 8             |*************                           |
     65536 -> 131071     : 3             |*****                                   |

最终,便能非常直观地观测 BPF 系统调用的耗时统计。