因为 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 里的 M
,M
因 mutex lock
不能被 runtime scheduler 调度而不断创建新的线程。
庆幸的是,在 XDP ACL 里启动了几百个 goroutine
,但它们并没有并发地操作一个 PROG_ARRAY
bpf map。不过,既然了解到该性能缺陷,就得优化一下它。
优化办法:使用 Go sync.Mutex
替代内核里的 mutex lock
。将被 block 的对象从线程变为 goroutine
(M
=> 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 系统调用的耗时统计。