书接上回 ,本文讲解对高性能 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 代码中搭配使用的优化实现。

  1. 将 ACL 规则按照优先级进行排序。参考:sortRules()
  2. 按照数组索引替换 ACL 规则的优先级。参考:sortRules()
  3. 按照数组索引顺序将 ACL 规则的 action 更新到 bpf map 中。参考:storeActions()

此处就不粘贴相关源代码了,请阅读原文去查看源代码吧。

动态增删 ACL 规则的处理

必须得支持动态增删 ACL 规则。但是,该怎么将增删 ACL 的处理对正在运行中的 XDP 程序的影响降到最低呢?

eBPF / XDP based firewall and packet filteringSecuring Linux with a Faster and Scalable Iptables 这两篇论文皆已提点:

不是去操作已有的 ACL 相关的 bpf map,而是使用新的 bpf map、新的 bpf prog。

  1. 将变更后的 ACL 数据更新到新的 bpf map 中。

  2. 新的 bpf prog 使用新的 bpf map。

  3. 将新的 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 场景。

总结

纸上得来终觉浅,绝知此事要躬行。

重造轮子,才是掌握轮子的正确办法;还能优化轮子呢。