书接上回 ,本文讲解对高性能 XDP ACL 的开源项目 xdp_acl 的优化内容:
- 按需开启 eBPF 中的 debug 日志
- 动态调整 bitmap 大小
- 使用 PERCPU ARRAY 优化规则 bpf map
- 动态增删 ACL 规则的处理
以下代码片段均来自重构后的 xdp_acl。
按需开启 eBPF 中的 debug 日志
这个比较简单,只是 eBPF CO-RE 的简单应用。
1,在 eBPF C 代码中,使用如下代码:
1
2
3
4
5
6
7
|
static volatile const __u32 XDPACL_DEBUG = 0;
#define bpf_debug_printk(fmt, ...) \
do { \
if (XDPACL_DEBUG) \
bpf_printk(fmt, ##__VA_ARGS__); \
} while (0)
|
即可使用 bpf_debug_printk()
打印日志了。
2,在 Go 代码中重写 XDPACL_DEBUG
常量:
1
2
3
4
5
6
7
|
rc := map[string]interface{}{
"XDPACL_DEBUG": x.debugMode,
"XDPACL_BITMAP_ARRAY_SIZE_LIMIT": uint32(getBitmapArraySizeLimit(ruleNum)),
}
if err := x.bpfSpec.RewriteConstants(rc); err != nil {
return fmt.Errorf("failed to rewrite constants: %v: %w", rc, err)
}
|
其中的 debugMode
变量就可以通过命令行参数 --debug
传入了。
动态调整 bitmap 大小
其实,并没有办法在加载阶段去变更 bpf map 的 value size 而不出问题;因为我做了这个尝试,总是在 bpf 校验阶段报错。
这是因为 eBPF 汇编指令里包含了 bpf map value 的相关处理,而在加载阶段无法修正相关的 eBPF 汇编指令。
退而求其次,只能在编译的时候,编译多几份不同 bitmap 大小的 eBPF 程序。而后,在加载的时候按需使用所需要的 bitmap 大小的 eBPF 程序。
编译 eBPF 程序的脚本如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
build_bpf() {
num="$1"
cat >${BPFFILE} <<EOF
#ifndef __LIBXDP_GENERATED_H_
#define __LIBXDP_GENERATED_H_
#define BITMAP_ARRAY_SIZE ${num}
#endif // __LIBXDP_GENERATED_H_
EOF
go run github.com/cilium/ebpf/cmd/bpf2go -cc=clang "XDPACL${num}" ./ebpf/xdp_acl.c -- -D__TARGET_ARCH_x86 -I./ebpf/headers -nostdinc -Wall -o3
}
main() {
for x in {8,16,32,64,128,160,256}; do
build_bpf $x
done
}
main
|
在 Go 代码中,根据 ACL 规则数量使用合适 bitmap 大小的 eBPF 程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func (x *xdp) loadSpec() error {
var err error
switch n := x.bitmapArraySize; n {
case 8:
x.bpfSpec, err = LoadXDPACL8()
case 16:
x.bpfSpec, err = LoadXDPACL16()
case 32:
x.bpfSpec, err = LoadXDPACL32()
case 64:
x.bpfSpec, err = LoadXDPACL64()
case 128:
x.bpfSpec, err = LoadXDPACL128()
case 160:
x.bpfSpec, err = LoadXDPACL160()
case 256:
x.bpfSpec, err = LoadXDPACL256()
default:
err = fmt.Errorf("no bpf spec for %d", n)
}
return err
}
|
使用 PERCPU ARRAY 优化规则 bpf map
在 eBPF Talk: 再论高性能 eBPF ACL 中讲解了 eBPF C 代码中的优化实现,是时候讲解一下 Go 代码中搭配使用的优化实现。
- 将 ACL 规则按照优先级进行排序。参考:sortRules()。
- 按照数组索引替换 ACL 规则的优先级。参考:sortRules()。
- 按照数组索引顺序将 ACL 规则的 action 更新到 bpf map 中。参考:storeActions()。
此处就不粘贴相关源代码了,请阅读原文去查看源代码吧。
动态增删 ACL 规则的处理
必须得支持动态增删 ACL 规则。但是,该怎么将增删 ACL 的处理对正在运行中的 XDP 程序的影响降到最低呢?
eBPF / XDP based firewall and packet filtering 和 Securing Linux with a Faster and Scalable Iptables 这两篇论文皆已提点:
不是去操作已有的 ACL 相关的 bpf map,而是使用新的 bpf map、新的 bpf prog。
-
将变更后的 ACL 数据更新到新的 bpf map 中。
-
新的 bpf prog 使用新的 bpf map。
-
将新的 bpf prog 更新到一个类型为 BPF_MAP_TYPE_PROG_ARRAY 的 bpf map 中。
关键的 eBPF C 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
struct {
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
__type(key, __u32);
__type(value, __u32);
__uint(max_entries, 1);
} progs SEC(".maps");
SEC("xdp_acl")
int xdp_acl_func_imm(struct xdp_md *ctx) {
// 该程序就是需要被动态更新的程序。
return xdp_acl_ipv4(ctx);
}
SEC("xdp_acl")
int xdp_acl_func(struct xdp_md *ctx) {
// ...
if (bpf_htons(ETH_P_IP) == l3_proto) {
// 此处需要使用 bpf_tail_call(),因为目标函数会在运行中被更新。
bpf_tail_call_static(ctx, &progs, 0);
}
return XDP_PASS;
}
|
其中,函数 xdp_acl_func_imm()
就是需要更新到 BPF_MAP_TYPE_PROG_ARRAY bpf map 中的程序。
关键的 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
|
// https://github.com/Asphaltt/xdp_acl/blob/main/xdp.go#L219
func (x *xdp) reload(r *rule.Rules, prevHitcount map[uint32]uint64) error {
ts := time.Now()
// 增删 ACL 规则后,需要重新处理一下。
r.FixRules()
// 根据 ACL 规则数量,按需调整 bitmap 大小;调整的代码逻辑在 r.FixRules() 里。
x.bitmapArraySize = uint32(r.BitmapArraySize)
// 重新获取 bpf spec,因为不同 bitmap 大小会使用不同的 bpf spec。具体代码请参考前面的 **动态调整 bitmap 大小**。
if err := x.loadSpec(); err != nil {
return fmt.Errorf("failed to load bpf spec while reloading: %w", err)
}
o := x.updatableObjs
x.updatableObjs = new(XDPACLObjects)
// 生成新的 bpf map 和 bpf prog
if err := x.loadObj(x.updatableObjs); err != nil {
return fmt.Errorf("failed to load xdp obj while reloading: %w", err)
}
// ...
// 将 ACL 数据更新到新的 bpf map 中
if err := x.storeRules(r, prevHitcount); err != nil {
x.closeUpdatableObjs()
x.updatableObjs = o
return fmt.Errorf("failed to store rules while reloading: %w", err)
}
// 更新 BPF_MAP_TYPE_PROG_ARRAY bpf map 中的函数 `xdp_acl_func_imm()`。
if err := x.persistentObjs.Progs.Put(uint32(0), x.updatableObjs.XdpAclFuncImm); err != nil {
x.closeUpdatableObjs()
x.updatableObjs = o
return fmt.Errorf("failed to update progs bpf map while reloading: %w", err)
}
// ...
return nil
}
|
小结
一套组合拳打下去,一个高性能 XDP ACL 方案就出来了,轻松面对百万 pps 场景。
总结
纸上得来终觉浅,绝知此事要躬行。
重造轮子,才是掌握轮子的正确办法;还能优化轮子呢。