veth 设备在 Linux 容器网络里被广泛使用,像其它网络设备一样都支持运行 XDP 程序。

与此同时,veth 设备还支持 driver 模式的 XDP 程序。

如果在往 veth 设备上挂载 XDP 程序时没有指定 XDP 运行模式,默认会优先采用 driver 模式。

如果在往 veth 设备上挂载 driver 模式的 XDP 程序时,报告了 No space left on device 错误,会是什么原因呢?

问题背景

因业务需求需要使用 veth 设备,而且需要往 veth 设备上挂载 XDP 程序。挂载 XDP 程序时需要指定使用 generic 模式。

且为了更好的性能,需要给 veth 设备设置 RPS。

问题排查

在用户态程序里,使用 cilium/ebpf 库来挂载 XDP 程序。

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// ${EBPF}/link/xdp.go

// AttachXDP links an XDP BPF program to an XDP hook.
func AttachXDP(opts XDPOptions) (Link, error) {
    // ...

    rawLink, err := AttachRawLink(RawLinkOptions{
        Program: opts.Program,
        Attach:  ebpf.AttachXDP,
        Target:  opts.Interface,
        Flags:   uint32(opts.Flags),
    })

    return rawLink, err
}

// ${EBPF}/link/link.go

// AttachRawLink creates a raw link.
func AttachRawLink(opts RawLinkOptions) (*RawLink, error) {
    // ...

    attr := sys.LinkCreateAttr{
        TargetFd:    uint32(opts.Target),
        ProgFd:      uint32(progFd),
        AttachType:  sys.AttachType(opts.Attach),
        TargetBtfId: uint32(opts.BTF),
        Flags:       opts.Flags,
    }
    fd, err := sys.LinkCreate(&attr)
    if err != nil {
        return nil, fmt.Errorf("create link: %w", err)
    }

    return &RawLink{fd, ""}, nil
}

// ${EBPF}/internal/sys/types.go

func LinkCreate(attr *LinkCreateAttr) (*FD, error) {
    fd, err := BPF(BPF_LINK_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
    if err != nil {
        return nil, err
    }
    return NewFD(int(fd))
}

// ${EBPF}/internal/sys/syscall.go

// BPF wraps SYS_BPF.
//
// Any pointers contained in attr must use the Pointer type from this package.
func BPF(cmd Cmd, attr unsafe.Pointer, size uintptr) (uintptr, error) {
    // ...

    for {
        r1, _, errNo := unix.Syscall(unix.SYS_BPF, uintptr(cmd), uintptr(attr), size)
        runtime.KeepAlive(attr)

        // ...

        return r1, err
    }
}

AttachXDP() 函数最终是调用 BPF 系统调用的 BPF_LINK_CREATE 命令。继续深入扒一下 BPF 系统调用的源代码。

1
2
3
4
5
6
7
8
BPF_CALL_3(bpf_sys_bpf, int, cmd, union bpf_attr *, attr, u32, attr_size)   // ${KERNEL}/kernel/bpf/syscall.c
|-->__sys_bpf()
    |-->link_create()
        |-->bpf_xdp_link_attach()                                           // ${KERNEL}/net/core/dev.c
            |-->bpf_link_init()                                             // ${KERNEL}/kernel/bpf/syscall.c
            |-->bpf_link_prime()
            |-->dev_xdp_attach_link()                                       // ${KERNEL}/net/core/dev.c
            |-->bpf_link_settle()                                           // ${KERNEL}/kernel/bpf/syscall.c

既然是 AttachXDP() 返回了 No space left on device 错误,那肯定是 link_create() 时返回了 -ENOSPC 错误码。

是时候祭出屠龙刀 retsnoop,trace 一下 link_create() 函数里出错的地方。

 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
# ./retsnoop -T -e '*sys_bpf' -a 'bpf_link_*' -a 'dev_xdp_*'
09:41:44.427064 -> 09:41:44.427087 TID/PID 39601/38991 (xxx/xxx):

FUNCTION CALL TRACE                     RESULT                DURATION
-------------------------------------   --------------------  --------
→ __x64_sys_bpf
    → __sys_bpf
        ↔ bpf_link_init                 [void]                 0.885us
        ↔ bpf_link_prime                [0]                    4.639us
        → dev_xdp_attach
            ↔ dev_xdp_install           [-ENOSPC]              1.162us
        ← dev_xdp_attach                [-ENOSPC]              8.162us
        → bpf_link_cleanup
            ↔ bpf_link_free_id.part.0   [0xffffffff80000000]   0.732us
        ← bpf_link_cleanup              [void]                 2.427us
    ← __sys_bpf                         [-ENOSPC]             29.009us
← __x64_sys_bpf                         [-ENOSPC]             31.859us

                    entry_SYSCALL_64_after_hwframe+0x44
                    kretprobe_trampoline+0x0
                    kretprobe_trampoline+0x0
!   31us [-ENOSPC]  __x64_sys_bpf
!   29us [-ENOSPC]  __sys_bpf
!*   8us [-ENOSPC]  dev_xdp_attach
!*   1us [-ENOSPC]  dev_xdp_install

屠龙刀 retsnoop 已屠龙,是 dev_xdp_attach() 返回了 -ENOSPC 错误码。

好了,接着看下 dev_xdp_attach_link() 会在什么情况下会返回 -ENOSPC

 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
dev_xdp_attach_link()                   // ${KERNEL}/net/core/dev.c
|-->dev_xdp_attach()
    |-->mode = dev_xdp_mode(dev, flags);
    |       static enum bpf_xdp_mode dev_xdp_mode(struct net_device *dev, u32 flags)
    |       {
    |           if (flags & XDP_FLAGS_HW_MODE)
    |               return XDP_MODE_HW;
    |           if (flags & XDP_FLAGS_DRV_MODE)
    |               return XDP_MODE_DRV;
    |           if (flags & XDP_FLAGS_SKB_MODE)
    |               return XDP_MODE_SKB;
    |           return dev->netdev_ops->ndo_bpf ? XDP_MODE_DRV : XDP_MODE_SKB;
    |       }
    |-->bpf_op = dev_xdp_bpf_op(dev, mode);
    |       static bpf_op_t dev_xdp_bpf_op(struct net_device *dev, enum bpf_xdp_mode mode)
    |       {
    |           switch (mode) {
    |           case XDP_MODE_SKB:
    |               return generic_xdp_install;
    |           case XDP_MODE_DRV:
    |           case XDP_MODE_HW:
    |               return dev->netdev_ops->ndo_bpf;
    |           default:
    |               return NULL;
    |           }
    |       }
    |-->err = dev_xdp_install(dev, mode, bpf_op, extack, flags, new_prog);
        |-->err = bpf_op(dev, &xdp);    // 因为没有指定 XDP 模式,所以会调用 veth 设备的 ndo_bpf 函数
       /
      /
     /
    |-->veth_xdp()                      // ${KERNEL}/drivers/net/veth.c
        |-->veth_xdp_set()
            if (dev->real_num_rx_queues < peer->real_num_tx_queues) {
                NL_SET_ERR_MSG_MOD(extack, "XDP expects number of rx queues not less than peer tx queues");
                err = -ENOSPC;
                goto err;
            }

从源代码可以看出,因为当前 veth 设备的 real_num_rx_queues 小于对端 veth 设备的 real_num_tx_queues,所以才会出现 No space left on device 错误。

如此,是时候祭出另一把屠龙刀 drgn,用来确认 veth 设备的 real_num_tx_queuesreal_num_tx_queues

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# drgn
drgn 0.0.21 (using Python 3.10.6, elfutils 0.187, with libkdumpfile)
For help, type help(drgn).
>>> import drgn
>>> from drgn import NULL, Object, cast, container_of, execscript, offsetof, reinterpret, sizeof
>>> from drgn.helpers.common import *
>>> from drgn.helpers.linux import *
>>> netdev_get_by_name(prog, "veth_xxx0").real_num_rx_queues
(unsigned int)14
>>> netdev_get_by_name(prog, "veth_yyy0").real_num_tx_queues
(unsigned int)48

屠龙刀 drgn 也已屠龙,确认了的确是 dev->real_num_rx_queues < peer->real_num_tx_queues

问题解决

其实,当看到 dev_xdp_mode() 函数时,我便察觉到了问题的根因:没有指定 generic 模式。

所以,当指定 generic 模式挂载 XDP 程序时,就没有出现 No space left on device 错误了。

小结

retsnoopdrgn 两把屠龙刀一起用来排查内核网络问题,简直无敌。