0%

eBPF XDP 子系统

使用 BPF 的内核子系统也是 BPF 基础设施的一部分,像网络子系统、追踪子系统都可以 attch BPF 程序,这篇博客 中讲述了 tc 和 XDP 在 Cilium 整个数据链路的位置。XDP 为 Linux 内核提供了高性能、可编程的网络数据路径,XDP BPF 程序会被 attach 到网络驱动的最早阶段,驱动收到包之后就会触发 BPF 程序的执行。由于网络包在还未进入网络协议栈之前就处理,它给 Linux网络带来了巨大的性能提升,甚至性能比DPDK还要高。

XDP 原理

XDP基于一系列的技术来实现高性能和可编程性,包括

  • 基于eBPF
  • Capabilities negotiation:通过协商确定网卡驱动支持的特性,XDP尽量利用新特性,但网卡驱动不需要支持所有的特性
  • 在网络协议栈前处理
  • 无锁设计
  • 批量I/O操作
  • 轮询式
  • 直接队列访问
  • 不需要分配skbuff
  • 支持网络卸载
  • DDIO
  • XDP程序快速执行并结束,没有循环
  • Packeting steering

XDP hook 位于网络驱动的快速路径上,XDP 程序直接从接收缓冲区(receive ring)中将包拿下来,无需执行任何耗时的操作,例如分配 skb 然后将包推送到网络协议栈,或者将包推送给 GRO 引擎等等。因此,只要有 CPU 资源,XDP BPF 程序就能够在最早的位置执行处理。

特点:

  • 内核态,并不会 bypass 内核
    • XDP 可以复用所有上游开发的内核网络驱动、用户空间工具,以及其他一些可用的内核基础设施,例如 BPF 辅助函数在调用自身时可以使用系统路由表、socket 等等。
    • 因为驻留在内核空间,因此 XDP 在访问硬件时与内核其他部分有相同的安全模型
    • 无需跨内核/用户空间边界,因为正在被处理的包已经在内核中,因此可以灵活地将 其转发到内核内的其他实体,例如容器的命名空间或内核网络栈自身。Meltdown 和 Spectre 漏洞尤其与此相关(Spectre 论文中一个例子就是用 ebpf 实现的,译者注 )。\
    • 将包从 XDP 送到内核中非常简单,可以复用内核中这个健壮、高效、使用广泛的 TCP/IP 协议栈,而不是像一些用户态框架一样需要自己维护一个独立的 TCP/IP 协 议栈。
    • 基于 BPF 可以实现内核的完全可编程,保持 ABI 的稳定,保持内核的系统调用 ABI “永远不会破坏用户空间的兼容性”(never-break-user-space)的保证。而且,与内核 模块(modules)方式相比,它还更加安全,这来源于 BPF 校验器,它能保证内核操作 的稳定性。
    • XDP 轻松地支持在运行时(runtime)原子地创建(spawn)新程序,而不会导致任何网络流量中断,甚至不需要重启内核/系统。
    • XDP 允许对负载进行灵活的结构化(structuring of workloads),然后集成到内核。例 如,它可以工作在“不停轮询”(busy polling)或“中断驱动”(interrupt driven)模 式。不需要显式地将专门 CPU 分配给 XDP。没有特殊的硬件需求,它也不依赖 hugepage(大页)。
    • XDP 不需要任何第三方内核模块或许可(licensing)。它是一个长期的架构型解决方案(architectural solution),是 Linux 内核的一个核心组件,而且是由内核社区开发的。

包处理逻辑

如下图所示,基于内核的eBPF程序处理包,每个 RX 队列分配一个CPU,且以每个网络包一个Page(packet-page)的方式避免分配skbuff。

XDP 输入参数

作为一个在驱动中运行 BPF 的框架,XDP 还保证了包是线性放置并且可以匹配到单个 DMA 页面,这个页面对 BPF 程序来说是可读和可写的。BPF 允许以 direct packet access 的方式访问包中的数据,这意味着程序直接将数据的指针放到了寄存器中,然后将内容加载到寄存器,相应地再将内容从寄存器写到包中。数据包在 XDP 中的表示形式是 xdp_buff,这也是传递给 BPF 程序的结构体(BPF 上下文):

1
2
3
4
5
6
7
8
9
struct xdp_buff {
void *data; // 指向 page 中包数据的起始位置
void *data_end; // 指向 page 中包数据的结尾位置
void *data_meta; // 开始时指向与 data 相同的位置
void *data_hard_start; // 指向页面中最大可能的 headroom 开始位置
struct xdp_rxq_info *rxq;
struct xdp_txq_info *txq;
u32 frame_sz; /* frame size to deduce data_hard_end/reserved tailroom*/
};

XDP 还提供了额外的 256 字 节 headroom 给 BPF 程序,

  • 利用 bpf_xdp_adjust_head() 辅助函数实现自定义封装头
    • 当对包进行封装(加 header)时,data 会逐渐向 data_hard_start 靠近
    • 该辅助函数还支持解封装(去 header)
  • 通过 bpf_xdp_adjust_meta() 在包前面添加自定义元数据
    • bpf_xdp_adjust_meta() 能够将 data_mata 朝着 data_hard_start 移动,这样可以给自定义元数据提供空间,这个空间对内核网络栈是不可见的,但对 tc BPF 程序可见,因为 tc 需要将它从 XDP 转移到 skb
    • bpf_xdp_adjust_meta() 也可以将 data_meta 移动到离 data_hard_start 比较远的位 置,这样就可以达到删除或缩小这个自定义空间的目的。
    • data_meta 还可以单纯用于在尾调用时传递状态,和 tc BPF 程序中用 skb->cb[] 控制块(control block)类似。

这样,我们就可以得到这样的结论,对于 struct xdp_buff 中数据包的指针,有: data_hard_start <= data_meta <= data < data_end.

rxq 字段指向某些额外的、和每个接收队列相关的元数据:

1
2
3
4
5
6
7
8
9
10
struct xdp_rxq_info {
struct net_device *dev;
u32 queue_index;
u32 reg_state;
struct xdp_mem_info mem;
} ____cacheline_aligned; /* perf critical, avoid false-sharing */

struct xdp_txq_info {
struct net_device *dev;
};

这些元数据是在缓冲区设置时确定的(并不是在 XDP 运行时)。BPF 程序可以从 netdevice 自身获取 queue_index 以及其他信息,例如 ifindex

XDP 输出参数

XDP BPF 程序执行结束后会返回一个判决结果,告诉驱动接下来如何处理这个包。在系统头文件 linux/bpf.h 中列出了所有的判决类型。

1
2
3
4
5
6
7
enum xdp_action {
XDP_ABORTED = 0,
XDP_DROP,
XDP_PASS,
XDP_TX,
XDP_REDIRECT,
};
  • XDP_DROP 表示立即在驱动层将包丢弃。这样可以节省很多资源,对于 DDoS mitigation 或通用目的防火墙程序来说这尤其有用。
  • XDP_PASS 表示允许将这个包送到内核网络栈。同时,当前正在处理这个包的 CPU 会 分配一个 skb,做一些初始化,然后将其送到 GRO 引擎。这和没有 XDP 时默认的包处理行为是一样的。
  • XDP_TX 是 BPF 程序的一个高效选项,能够在收到包的网卡上直接将包再发送出去。对于实现防火墙+负载均衡的程序来说这非常有用,因为这些部署了 BPF 的节点可以作为一 个 hairpin (发卡模式,从同一个设备进去再出来)模式的负载均衡器集群,将收到的包在 XDP BPF 程序中重写之后直接发送回去。
  • XDP_REDIRECTXDP_TX 类似,但是通过另一个网卡将包发出去。另外, XDP_REDIRECT 还可以将包重定向到一个 BPF cpumap,即,当前执行 XDP 程序的 CPU 可以将这个包交给某个远端 CPU,由后者将这个包送到更上层的内核栈,当前 CPU 则继续在这个网卡执行接收和处理包的任务。这XDP_PASS 类似,但当前 CPU 不用去做将包送到内核协议栈的准备工作(分配 skb,初始化等等),这部分开销还是很大的
  • XDP_ABORTED 表示程序产生异常,其行为和 XDP_DROP,但 XDP_ABORTED 会经过 trace_xdp_exception tracepoint,因此可以通过 tracing 工具来监控这种非正常行为。

与DPDK对比

相对于DPDK,XDP具有以下优点

  • 无需第三方代码库和许可
  • 同时支持轮询式和中断式网络
  • 无需分配大页
  • 无需专用的CPU
  • 无需定义新的安全网络模型

缺点

注意XDP的性能提升是有代价的,它牺牲了通用型和公平性

  • XDP不提供缓存队列(qdisc),TX设备太慢时直接丢包,因而不要在RX比TX快的设备上使用XDP
  • XDP程序是专用的,不具备网络协议栈的通用性

XDP 使用案例

本节列出了 XDP 的几种主要使用案例。这里列出的并不全,而且考虑到 XDP 和 BPF 的可 编程性和效率,人们能容易地将它们适配到其他领域。

  • DDoS 防御、防火墙

    XDP BPF 的一个基本特性就是用 XDP_DROP 命令驱动将包丢弃,由于这个丢弃的位置非常早,因此这种方式可以实现高效的网络策略,平均到每个包的开销非常小。这对于那些需要处理任何形式的 DDoS 攻击的场景来说是非常理想的,而且由于其通用性,使得它能够在 BPF 内实现任何形式的防火墙策略,开销几乎为零, 例如,

    • 作为 standalone 设备(例如通过 XDP_TX 清洗流量)
    • 广泛部署在节点上,保护节点的安全(通过 XDP_PASS 或 cpumap XDP_REDIRECT 允许「好流量」经 过)
    • Offloaded XDP 更进一步,将本来就已经很小的 per-packet cost 全部下放到网卡以线速(line-rate)进行处理
  • 包转发和负载均衡:通过 XDP_TXXDP_REDIRECT 动作实现

    • XDP 层运行的 BPF 程序能够任意修改(mangle)数据包,即使是 BPF 辅助函数都能增加或减少包的 headroom,这样就可以在将包再次发送出去之前,对包进行任何的封装/解封装。
  • 利用 XDP_TX 能够实现 hairpinned(发卡)模式的负载均衡器,这种均衡器能够在接收到包的网卡再次将包发送出去,而 XDP_REDIRECT 动作能够将包转发到另一个 网卡然后发送出去。

    • XDP_REDIRECT 返回码还可以和 BPF cpumap 一起使用,对那些目标是本机协议栈、 将由 non-XDP 的远端(remote)CPU 处理的包进行负载均衡。
  • 栈前过滤与处理

    除了策略执行,XDP 还可以用于加固内核的网络栈,这是通过 XDP_DROP 实现的。 这意味着,XDP 能够在可能的最早位置丢弃那些与本节点不相关的包,这个过程发生在内核网络栈看到这些包之前。例如假如我们已经知道某台节点只接受 TCP 流量,那任 何 UDP、SCTP 或其他四层流量都可以在发现后立即丢弃。

    这种方式的好处是包不需要再经过各种实体(例如 GRO 引擎、内核的 flow dissector 以及其他的模块),就可以判断出是否应该丢弃,因此减少了内核的受攻击面。正是由于 XDP 的早期处理阶段,这有效地对内核网络栈「假装」这些包根本 就没被网络设备看到。

    另外,如果内核接收路径上某个潜在 bug 导致 ping of death 之类的场景,那我们能够利用 XDP 立即丢弃这些包,而不用重启内核或任何服务。而且由于能够原子地替换程序,这种方式甚至都不会导致宿主机的任何流量中断。

    栈前处理的另一个场景是:在内核分配 skb 之前,XDP BPF 程序可以对包进行任意 修改,而且对内核「假装」这个包从网络设备收上来之后就是这样的。对于某些自定义包修改(mangling)和封装协议的场景来说比较有用,在这些场景下,包在进入 GRO 聚合之前会被修改和解封装,否则 GRO 将无法识别自定义的协议,进而无法执行任何形 式的聚合。

    XDP 还能够在包的前面 push 元数据(非包内容的数据)。这些元数据对常规的内核栈是不可见的(invisible),但能被 GRO 聚合(匹配元数据),稍后可以和 tc ingress BPF 程序一起处理,tc BPF 中携带了 skb 的某些上下文,例如,设置了某些 skb 字段。

  • 流抽样和监控

    XDP 还可以用于包监控、抽样或其他的一些网络分析,例如作为流量路径中间节点的一部分;或运行在终端节点上,和前面提到的场景相结合。对于复杂的包分析,XDP 提供了设施来高效地将网络包(截断的或者是完整的 payload)或自定义元数据 push 到 perf 提供的一个快速、无锁、per-CPU 内存映射缓冲区,或者是一 个用户空间应用。

    这还可以用于流分析和监控,对每个流的初始数据进行分析,一旦确定是正常流量,这个流随后的流量就会跳过这个监控。感谢 BPF 带来的灵活性,这使得我们可以实现任何形式 的自定义监控或采用。

XDP BPF 在生产环境使用的一个例子是 Facebook 的 SHIV 和 Droplet 基础设施,实现了 它们的 L4 负载均衡和 DDoS 测量。从基于 netfilter 的 IPV(IP Virtual Server)迁移到 XDP BPF 使它们的生产基础设施获得了 10x 的性能提升。这方面的工作最早在 netdev 2.1 大会上做了分享:

另一个例子是 Cloudflare 将 XDP 集成到它们的 DDoS 防御流水线中,替换了原来基于 cBPF 加 iptables 的 xt_bpf 模块所做的签名匹配(signature matching)。 基于 iptables 的版本在发生攻击时有严重的性能问题,因此它们考虑了基于用户态、 bypass 内核的一个方案,但这种方案也有自己的一些缺点,并且需要不停轮询网卡,并且在将某些包重新注入内核协议栈时代价非常高。迁移到 eBPF/XDP 之后,两种方案的优点都可以利用到,直接在内核中实现了高性能、可编程的包处理过程:

XDP 工作模式

XDP 有三种工作模式,分别是:

  • xdpdrv:默认模式,当讨论 XDP 时通常隐含的都是指这种模式。
  • xdpoffload:在这种模式中,XDP BPF 程序直接 offload 到网卡,而不是在主机的 CPU 上执行
  • xdpgeneric:用于给那些还没有原生支持 XDP 的驱动进行试验性测试。

可以通过ip link命令查看已经安装的XDP模式,generic/SKB (xdpgeneric), native/driver (xdp), hardware offload (xdpoffload),如下xdpgeneric即generic模式。

1
2
3
4
5
6
# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 00:16:3e:00:2d:67 brd ff:ff:ff:ff:ff:ff
prog/xdp id 101 tag 3b185187f1855c4c jited

虚拟机上的设备可能无法支持native模式。在阿里云ecs上运行下文的例子时出现了错误:libbpf: Kernel error message: virtio_net: Too few free TX rings available,且无权限使用ethtool -G eth0 tx 4080修改tx buffer的大小。建议使用物理机。

可以使用ethtool查看经XDP处理的报文统计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ethtool -S eth0
NIC statistics:
rx_queue_0_packets: 547115
rx_queue_0_bytes: 719558449
rx_queue_0_drops: 0
rx_queue_0_xdp_packets: 0
rx_queue_0_xdp_tx: 0
rx_queue_0_xdp_redirects: 0
rx_queue_0_xdp_drops: 0
rx_queue_0_kicks: 20
tx_queue_0_packets: 134668
tx_queue_0_bytes: 30534028
tx_queue_0_xdp_tx: 0
tx_queue_0_xdp_tx_drops: 0
tx_queue_0_kicks: 127973

Native XDP

在这种模式中,BPF 程序直接在驱动的接收路径上运行,理论上这是软件层最早可以处理包的位置。这是常规的 XDP 模式,需要驱动实现对 XDP 的支持,目前 Linux 内核中主流的 10G/40G 网卡都已经支持。下面列出了一些支持 Native XDP 的驱动:

  • Broadcom
    • bnxt
  • Cavium
    • thunderx
  • Intel
    • ixgbe
    • ixgbevf
    • i40e
  • Mellanox
    • mlx4
    • mlx5
  • Netronome
    • nfp
  • Others
    • tun
    • virtio_net
  • Qlogic
    • qede
  • Solarflare
    • sfc (XDP for sfc available via out of tree driver as of kernel 4.17, but will be upstreamed soon)

Offloaded XDP

在这种模式中,XDP BPF 程序直接 offload 到网卡,而不是在主机的 CPU 上执行。 因此,本来就已经很低的 per-packet 开销完全从主机下放到网卡,能够比运行在 native XDP 模式取得更高的性能。这种 offload 通常由智能网卡(例如支持 Netronome’s nfp 驱动的网卡)实现,这些网卡有多线程、多核流处理器(flow processors),一个位于内核中的 JIT 编译器( in-kernel JIT compiler)将 BPF 翻译成网卡的原生指令。

虽然在这种模式中某些 BPF map 类型 和 BPF 辅助函数是不能用的。BPF 校验器检测到这种情况时会直接报错,告诉用户哪些东西是不支持的。除了这些不支持的 BPF 特性之外,其他方面与 native XDP 都是一样的。

Generic XDP

对于还没有实现 native 或 offloaded XDP 的驱动,内核提供了一个 generic XDP 选项,这种模式不需要任何驱动改动。

generic XDP hook 位于内核协议栈的主接收路径上,接受的是 skb 格式的包,但由于 这些 hook 位于 ingress 路径的很后面,因此与 native XDP 相比性能有明显下降。因此,xdpgeneric 大部分情况下只能用于试验目的,很少用于生产环境。

加载 XDP BPF 对象

本节展示 如何使用 iproute2 加载对象文件,以及加载器的一些通用机制

1
2
3
4
$ ip link set dev DEVICE 
[ { xdp | xdpgeneric | xdpdrv | xdpoffload } { off |
object FILE [ section NAME ] [ verbose ] |
pinned FILE } ]

给定一个为 XDP 编译的 BPF 对象文件 prog.o,可以用 ip 命令加载到支持 XDP 的 netdevice em1

1
$ ip link set dev em1 xdp obj prog.o

以上命令假设程序代码存储在默认的 section,在 XDP 的场景下就是 prog section。如果是在其他 section,例如 foobar,那就需要用如下命令:

1
$ ip link set dev em1 xdp obj prog.o sec foobar

注意,我们还可以将程序加载到 .text section。修改程序,从 xdp_drop 入口去掉 __section() 注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <linux/bpf.h>

#ifndef __section
# define __section(NAME) \
__attribute__((section(NAME), used))
#endif

int xdp_drop(struct xdp_md *ctx)
{
return XDP_DROP;
}

char __license[] __section("license") = "GPL";

然后通过如下命令加载:

1
$ ip link set dev em1 xdp obj prog.o sec .text

默认情况下,如果 XDP 程序已经 attach 到网络接口,那再次加载会报错,这样设计是为了防止程序被无意中覆盖。要强制替换当前正在运行的 XDP 程序,必须指定 -force 参数:

1
$ ip -force link set dev em1 xdp obj prog.o

今天,大部分支持 XDP 的驱动都支持在不会引起流量中断的前提下原子地替换运行中的程序。出于性能考虑,支持 XDP 的驱动只允许 attach 一个程序 ,不支持程序链(a chain of programs)。如果有必要的话,可以通过尾调用来对程序进行拆分,以达到与程序链类似的效果。

如果一个接口上有 XDP 程序 attach,ip link 命令会显示一个 xdp 标记。因 此可以用 ip link | grep xdp 查看所有有 XDP 程序运行的接口。ip -d link 可以查 看进一步信息;另外,bpftool 指定 BPF 程序 ID 可以获取 attached 程序的信息,其中程序 ID 可以通过 ip link 看到。

要从接口删除 XDP 程序,执行下面的命令:

1
$ ip link set dev em1 xdp off

要将驱动的工作模式从 non-XDP 切换到 native XDP ,或者相反,通常情况下驱动都需要重新配置它的接收(和发送)环形缓冲区,以保证接收的数据包在单个页面内是线性排列的, 这样 BPF 程序才可以读取或写入。一旦完成这项配置后,大部分驱动只需要执行一次原子的程序替换,将新的 BPF 程序加载到设备中。


执行 ip link set dev em1 xdp obj [...] 命令时,内核会先尝试以 native XDP 模 式加载程序,如果驱动不支持再自动回退到 generic XDP 模式。如果显式指定了 xdpdrv 而不是 xdp,那驱动不支持 native XDP 时加载就会直接失败,而不再尝试 generic XDP 模式。

一个例子:以 native XDP 模式强制加载一个 BPF/XDP 程序,打印链路详情,最后再卸载程序:

1
2
3
4
5
6
7
8
$ ip -force link set dev em1 xdpdrv obj prog.o
$ ip link show
[...]
6: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp qdisc mq state UP mode DORMANT group default qlen 1000
link/ether be:08:4d:b6:85:65 brd ff:ff:ff:ff:ff:ff
prog/xdp id 1 tag 57cd311f2e27366b
[...]
$ ip link set dev em1 xdpdrv off

还是这个例子,但强制以 generic XDP 模式加载(即使驱动支持 native XDP),另外用 bpftool 打印 attached 的这个 dummy 程序内具体的 BPF 指令:

1
2
3
4
5
6
7
8
9
10
11
$ ip -force link set dev em1 xdpgeneric obj prog.o
$ ip link show
[...]
6: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric qdisc mq state UP mode DORMANT group default qlen 1000
link/ether be:08:4d:b6:85:65 brd ff:ff:ff:ff:ff:ff
prog/xdp id 4 tag 57cd311f2e27366b <-- BPF program ID 4
[...]
$ bpftool prog dump xlated id 4 <-- Dump of instructions running on em1
0: (b7) r0 = 1
1: (95) exit
$ ip link set dev em1 xdpgeneric off

最后卸载 XDP,用 bpftool 打印程序信息,查看其中的一些元数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ip -force link set dev em1 xdpoffload obj prog.o
$ ip link show
[...]
6: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpoffload qdisc mq state UP mode DORMANT group default qlen 1000
link/ether be:08:4d:b6:85:65 brd ff:ff:ff:ff:ff:ff
prog/xdp id 8 tag 57cd311f2e27366b
[...]

$ bpftool prog show id 8
8: xdp tag 57cd311f2e27366b dev em1 <-- Also indicates a BPF program offloaded to em1
loaded_at Apr 11/20:38 uid 0
xlated 16B not jited memlock 4096B

$ ip link set dev em1 xdpoffload off

注意,每个程序只能选择用一种 XDP 模式加载,无法同时使用多种模式,例如 xdpdrvxdpgeneric

无法原子地在不同 XDP 模式之间切换,例如从 generic 模式切换到 native 模式。但 重复设置为同一种模式是可以的:

1
2
3
4
5
6
7
8
$ ip -force link set dev em1 xdpgeneric obj prog.o
$ ip -force link set dev em1 xdpoffload obj prog.o
RTNETLINK answers: File exists

$ ip -force link set dev em1 xdpdrv obj prog.o
RTNETLINK answers: File exists

$ ip -force link set dev em1 xdpgeneric obj prog.o <-- Succeeds due to xdpgeneric

在不同模式之间切换时,需要先退出当前的操作模式,然后才能进入新模式:

1
2
3
4
5
6
7
8
9
10
11
12
$ ip -force link set dev em1 xdpgeneric obj prog.o
$ ip -force link set dev em1 xdpgeneric off
$ ip -force link set dev em1 xdpoffload obj prog.o

$ ip l
[...]
6: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpoffload qdisc mq state UP mode DORMANT group default qlen 1000
link/ether be:08:4d:b6:85:65 brd ff:ff:ff:ff:ff:ff
prog/xdp id 17 tag 57cd311f2e27366b
[...]

$ ip -force link set dev em1 xdpoffload off

参考资料