Published at 2021-08-01 | Last Update 2021-08-01
本文翻译自 2018 年 ACM CoNEXT
大会上的一篇文章:
The eXpress Data Path: Fast Programmable Packet Processing in the Operating System Kernel
作者阵容豪华,包括来自 Cilium 的 Daniel Borkmann、John Fastabend 等。
论文引用信息:
Toke Høiland-Jørgensen, Jesper Dangaard Brouer, Daniel Borkmann, John Fastabend, Tom Herbert, David Ahern, and David Miller. 2018. The eXpress Data Path: Fast Programmable Packet Processing in the Operating System Kernel. In CoNEXT ’18: International Conference on emerging Networking EXperiments and Technologies, December 4–7, 2018, Heraklion, Greece. ACM, New York, NY, USA, 13 pages. https://doi.org/10.1145/3281411.3281443
由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。
以下是译文。
近些年业界流行通过内核旁路(kernel bypass)的方式实现 可编程的包处理过程(programmable packet processing)。实现方式是 将网络硬件完全交由某个专门的用户空间应用(userspace application) 接管,从而避免内核和用户态上下文切换的昂贵性能开销。
内核收包时,硬中断(Interrupt)和软中断(SoftIRQ)相关的逻辑,见: (译) Linux 网络栈监控和调优:接收数据(2016)。 译注。
但是,操作系统被旁路(绕过)之后,它的应用隔离(application isolation) 和安全机制(security mechanisms)就都失效了;一起失效的还有各种经过已经 充分测试的配置、部署和管理工具。
为解决这个问题,我们提出一种新的可编程包处理方式:eXpress Data Path (XDP)。
为展示 XDP 灵活的编程模型,本文还将给出三个程序示例,
软件实现高性能包处理的场景,对每个包的处理耗时有着极高的要求。通用目的操作系统中 的网络栈更多是针对灵活性的优化,这意味着它们花在每个包上 的指令太多了,不适合网络高吞吐的场景。
因此,随后出现了一些专门用于包处理的软件开发工具,例如 Data Plane Development Kit (DPDK) [16]。这些工具一般都会完全绕过内核,将网络硬件直接交 给用户态的网络应用,并需要独占一个或多个 CPU。
内核旁路方式可以显著提升性能,但缺点也很明显:
对此,本文提供了另一种解决方案:给内核网络栈添加可编程能力。这使得我们能在 兼容各种现有系统、复用已有网络基础设施的前提下,仍然实现高速包处理。 这个框架称为 XDP,
XDP 已经在过去的几个内核 release 中逐渐合并到内核,但在本文之前,还没有关于 XDP 系统的 完整架构介绍。本文将对 XDP 做一个高层介绍。
我们的测试结果显示 XDP 能取得 24Mpps/core
的处理性能,这虽然与 DPDK 还有差距,
但相比于后者这种 kernel bypass 的方式,XDP 有非常多的优势。
本文是 2018 年的测试结果,更新的一些性能(及场景)对比可参考 (译) 为容器时代设计的高级 eBPF 内核特性(FOSDEM, 2021)。 译注。
具体地,XDP:
与内核网络栈协同工作,将硬件的控制权完全留在内核范围内。带来的好处:
无需任何特殊硬件特性,任何有 Linux 驱动的网卡都可以支持, 现有的驱动只需做一些修改,就能支持 XDP hooks。
可以选择性地复用内核网络栈中的现有功能,例如路由表或 TCP/IP 协议栈,在保持配置接口不变的前提下,加速关键性能路径(critical performance paths)。
保证 eBPF 指令集和 XDP 相关的编程接口(API)的稳定性。
与常规 socket 层交互时,没有从用户态将包重新注入内核的昂贵开销。
对应用透明。这创造了一些新的部署场景/方式,例如直接在应用所 在的服务器上部署 DoS 防御(而非中心式/网关式 DoS 防御)。
服务不中断的前提下动态重新编程(dynamically re-program), 这意味着可以按需加入或移除功能,而不会引起任何流量中断,也能动态响应系统其他部分的的变化。
无需预留专门的 CPU 做包处理,这意味着 CPU 功耗与流量高低直接相关,更节能。
接下来的内容介绍 XDP 的设计,并做一些性能分析。结构组织如下:
XDP 当然不是第一个支持可编程包处理的系统 —— 这一领域在过去几年发展势头良好, 并且趋势还在持续。业内已经有了几种可编程包处理框架,以及基于这些框架的新型应用,包括:
要基于通用(Common Off The Shelf,COTS)硬件实现高性能包处理,就必须解决 网卡(NIC)和包处理程序之间的所有瓶颈。由于性能瓶颈主要来源于内核和用户态应用之 间的接口(系统调用开销非常大,另外,内核功能丰富,但也非常复杂), 低层(low-level)框架必须通过这样或那样的方式来降低这些开销。
现有的一些框架通过几种不同的方式实现了高性能,XDP 构建在其中一些技术之上。 接下来对 XDP 和它们的异同做一些比较分析。
在现有的所有框架中,内核旁路方式性能是最高的 [18];但如引言中指 出,这种方式在管理、维护和安全方面都存在不足。
XDP 采用了一种与内核旁路截然相反的方式:相比于将网络硬件的控制权上移到用户空间, XDP 将性能攸关的包处理操作直接放在内核中,在操作系统的网络栈之前执行。
但仍然由内核来管理硬件,因此保留了操作系统提供的管理接口和安全防护能力。
这里的主要创新是:使用了一个虚拟执行环境,它能对加载的 程序进行校验,确保它们不会对内核造成破坏。
在 XDP 之前,以内核模块(kernel module)方式实现包处理功能代价非常高, 因为程序执行出错时可能会导致整个系统崩溃,而且内核的内部 API 也会随着时间发生变化。 因此也就不难理解为什么只有很少的系统采用了这种方式。其中做的比较好包括
这几个系统都支持灵活的配置,适用于多种场景,取得比较小的平摊代价。
XDP 通过:
提供一个安全的执行环境,以及内核社区支持,提供与那些暴露到用户空间一样稳定的内核 API
极大地降低了那些将处理过程下沉到内核的应用(applications of moving processing into the kernel)的成本。
此外,XDP 程序也能够完全绕过内核网络栈(completely bypass), 与在内核网络栈中做 hook 的传统内核模块相比,性能也更高。
XDP 除了能将处理过程下沉到内核以获得最高性能之外,还支持在程序中执行重定向 (redirection)操作,完全绕过内核网络栈,将包送到特殊类型的用户空间 socket; 甚至能工作在 zero-copy 模式,进一步降低开销。
内核模块方式的另一个例子是 Packet I/O engine,这是 PacketShader [19] 的组成部分, 后者专用于 Arrakis [43] and ClickOS [36] 之类的特殊目的操作系统。
可编程硬件设备也是一种实现高性能包处理的方式。
某种意义上来说,XDP 可以认为是一种 offload 方式:
XDP 提供了一种高性能包处理方式,与已有方式相比,在性能、与现有系统的集成、灵活性 等方面取得了更好的平衡。
接下来介绍 XDP 是如何取得这种平衡的。
XDP 的设计理念:
这种与内核的深度集成显然会给设计带来一些限制,在 XDP 组件合并到 Linux 的过程中,我们也收到了许多来自社区的反馈,促使我们不断调整 XDP 的设计,但 这些设计反思不在本文讨论范围之内。
图 1 描绘了整个 XDP 系统,四个主要组成部分:
Fig 1. XDP 与 Linux 网络栈的集成。这里只画了 ingress 路径,以免图过于复杂。
上图是 ingress 流程。网卡收到包之后,在处理包数据(packet data)之前,会先执行 main XDP hook 中的 eBPF 程序。 这段程序可以选择:
放行(allow)这个包,如果后面没有其他原因导致的 drop,这个包就会进入常规的内核网络栈。
如果是这种情况,也就是放行包进入内核网络栈,那接下来在将包放到发送队列之前(before packets are queued for transmission), 还有一个能执行 BPF 程序的地方:TC BPF hook。
更多信息,可参考 (译) [论文] 迈向完全可编程 tc 分类器(cls_bpf)(NetdevConf,2016)。 译注。
此外,图 1 中还可以看出,不同的 eBPF 程序之间、eBPF 程序和用户空间应用之间,都能够通过 BPF maps 进行通信。
XDP 程序在网络设备驱动中执行,网络设备每收到一个包,程序就执行一次。
相关代码实现为一个内核库函数(library function),因此程序直接 在设备驱动中执行,无需切换到用户空间上下文。
回到上面图 1 可以看到:程序在网卡收到包之后最早能处理包的位置
执行 —— 此时内核还没有为包分配 struct sk_buff
结构体,
也没有执行任何解析包的操作。
下图是一个典型的 XDP 程序执行流:
Fig 2. 典型 XDP 程序的执行流。
网卡收到一个包时,XDP 程序依次执行:
提取包头中的信息(例如 IP、MAC、Port、Proto 等),
执行到程序时,系统会传递给它一个上下文对象(context object)作为参赛
(即 struct xdp_md *ctx
,后面有例子),其中包括了指向原
始包数据的指针,以及描述这个包是从哪个网卡的哪个接口接收上来的等元数据字段。
读取或更新一些资源的元信息(例如更新统计信息);
解析包数据之后,XDP 程序可以读取 ctx
中的包元数据(packet metadata)
字段,例如从哪个网卡的哪个接口收上来的(ifindex)。除此之外,ctx
对象还允许
程序访问与包数据毗邻的一块特殊内存区域(cb, control buffer),
在包穿越整个系统的过程中,可以将自定义的数据塞在这里。
除了 per-packet metadata,XDP 程序还可以通过 BPF map 定义和访问自己的持久数据 ,以及通过各种 helper 函数访问内核基础设施。
如果有需要,对这个包进行 rewrite header 操作,
程序能修改包数据的任何部分,包括添加或删除包头。这使得 XDP 程序能执行封装/接封装操作,以及重写(rewrite)地址字段然后转发等操作。
内核 helper 函数各有不同用途,例如修改一个包之后,计算新的校验和(checksum)。
进行最后的判决(verdict),确定接下来对这个包执行什么操作;
判决结果包括:
重定向功能的用途:
这些不同的路径,在图 1 对应的是几条实线。
将重定向判决(verdict)与重定向目标(target)分开,使得重定向目标类型很容易扩展; 另外,由于重定向参数(目标)是通过 BPF map 查询的,因此无需修 改 XDP 程序,就能动态修改重定向目标。
程序还能通过尾调用(tail call),将控制权交给另一个 XDP 程序; 通过这种方式,可以将一个大程序拆分成几个逻辑上的小程序(例如,根据 IPv4/IPv6)。
由于 XDP 程序可包含任意指令,因此前三步(读取包数据、处理元数据、重写包数据) 顺序可以是任意的,而且支持多层嵌套。 但实际中为了获得高性能,大部分情况下还是将执行结构组织成这顺序的三步。
XDP 程序在 Extended BPF (eBPF) 虚拟机中执行。 eBPF 是早期 BSD packet filter (BPF) [37] 的扩展,后者在过去的几十年中广泛 应用于各种包处理工具。
BPF 使用 基于寄存器的(register-based) virtual machine 来描述 过滤动作(filtering actions)。
(其他几段,略)。
这一小节的内容有些已经过滤,限于篇幅,描述也并不全面。
更多关于 eBPF 虚拟机和指令架构的内容,可参考: (译) Cilium:BPF 和 XDP 参考指南(2021) 译注。
eBPF 虚拟机支持动态加载(loading)和重加载(re-loading)程序,内核管理所有 BPF 程序的生命周期。
eBPF 程序在触发内核事件时执行(例如,触发 XDP 程序执行的,是收包事件)。 程序每次执行时,初始状态都是相同的(即程序是无状态的),它们无法直接访问 内核中的持久存储(BPF map)。为此,内核提供了访问 BPF map 的 helper 函数。
BPF map 是 key/value 存储,在加载 eBPF 程序时定义(defined upon loading an eBPF program)。
BPF map 的类型和一些细节实现,可参考:
译注。
用途:
bpf()
系统调用由于 eBPF 代码直接运行在内核地址空间,因此它能直接访问 —— 也可
能是破坏 —— 任何内存。为防止这种情况发生,内核规定只能通过唯一入口(
bpf()
系统调用)加载 BPF 程序。
加载 BPF 程序时,位于内核中的校验器首先会对字节码程序进行静态分析,以确保
程序会终止(terminate)。通过下面这两点来实现:
校验器的工作原理:首先根据程序的控制流构建一个有向无环图(DAG), 然后对 DAG 执行如下校验:
首先,对 DAG 进行一次深度优先搜索(depth-first search),以 确保它是无环的(acyclic),例如,没有循环,也不包含不支持或无法执行到的指令。
然后,再扫描一遍,这次会遍历 DAG 的所有可能路径。这次扫描的目的是:
程序执行 load
或 call
指令时,如果参数不合法,就会在这里被拒绝。参数合法
性是通过在程序执行期间跟踪所有寄存器和栈变量的状态(states of
registers and stack variables)来实现的。
这种跟踪寄存器状态的机制是为了在无法预知内存边界的情况下,仍然确保程序 的内存访问不会越界。无法预知内存边界是因为:
为解决这个问题,校验器会检查已加载的程序自身是否会做如下检查:
这种方式将处理逻辑中的安全检查和遇到错误时如何处理的控制权都 交给了 BPF 程序的编写者。
为跟踪数据访问,校验器会跟踪
程序开始时,
struct xdp_md *ctx
),接下来程序每执行一步,寄存器状态就会更新一次。当寄存器中存入一个新值时,这个寄存器 还会继承与这个值相关的状态变量(inherits the state variables from the source of the value)。
算术操作会影响标量类型的值的范围(value ranges of scalar types),以及指针类型的 offset。
可能的最大范围(max possible range)存储在状态变量中,例如往寄存器中 load 一个字节时,
这个寄存器的可能值范围就设置为 0~255。指令图(instruction graph)中的
各逻辑分支就会根据操作结果更新寄存器状态。例如,比较操作 R1 > 10
,
if R1 > 10
中会将 R1 最小值设为 11,else
分支中将其最大值设为 10。利用状态变量中存储的范围信息,校验器就能预测每个 load 指令能访问的所有 内存范围,确保它执行的都是合法内存访问。
data_end
变量做比较;只要校验器无法证明某个操作是安全,该 BPF 程序在加载时(load time)就会被拒绝。 除此之外,校验器还会利用范围信息确保内存的对齐访问(enforce aligned memory access)。
需要说明的是,校验器的目的是避免将内核内部(the internals of the kernel )暴露给恶意或有缺陷的 eBPF 程序,而非确保程序中函数的实现已经是最高效的。
换句话说,如果 XDP 程序中处理逻辑过多,也可能会导致机器变慢 ;如果代码写的有问题,也可能会破坏包数据。出于这些原因,加载 BPF 程序需要 管理员权限(root)。避免这些 bug 的责任在程序员,但选择将哪些程序加载 到系统的权限在管理员。
下面是一个简单的 XDP 程序,展示了前面介绍的一些特性。 程序会解析包数据,判断如果是 UDP 包,直接交换源和目的 MAC 地址,然后将包从相同网卡再发送回去,
虽然这是一个非常简单的例子,但真实世界中的 XDP 程序用到的组件和特性,这里基本都具备了。
// 从内核 BPF 代码示例 xdp2_kern.c 修改而来。
1 // 用于统计包数
2 struct bpf_map_def SEC("maps") rxcnt = {
3 .type = BPF_MAP_TYPE_PERCPU_ARRAY,
4 .key_size = sizeof(u32), // IP 协议类型,即 IPv4/IPv6
5 .value_size = sizeof(long), // 包数
6 .max_entries = 256,
7 };
8
9 // 直接操作包数据(direct packet data access),交换 MAC 地址
10 static void swap_src_dst_mac(void *data)
11 {
12 unsigned short *p = data;
13 unsigned short dst[3];
14 dst[0] = p[0]; dst[1] = p[1]; dst[2] = p[2];
15 p[0] = p[3]; p[1] = p[4]; p[2] = p[5];
16 p[3] = dst[0]; p[4] = dst[1]; p[5] = dst[2];
17 }
18
19 static int parse_ipv4(void *data, u64 nh_off, void *data_end)
20 {
21 struct iphdr *iph = data + nh_off;
22 if (iph + 1 > data_end)
23 return 0;
24 return iph->protocol;
25 }
26
27 SEC("xdp1") // marks main eBPF program entry point
28 int xdp_prog1(struct xdp_md *ctx)
29 {
30 void *data_end = (void *)(long)ctx->data_end;
31 void *data = (void *)(long)ctx->data;
32 struct ethhdr *eth = data; int rc = XDP_DROP;
33 long *value; u16 h_proto; u64 nh_off; u32 ipproto;
34
35 nh_off = sizeof(*eth);
36 if (data + nh_off > data_end)
37 return rc;
38
39 h_proto = eth->h_proto;
40
41 /* check VLAN tag; could be repeated to support double-tagged VLAN */
42 if (h_proto == htons(ETH_P_8021Q) || h_proto == htons(ETH_P_8021AD)) {
43 struct vlan_hdr *vhdr;
44
45 vhdr = data + nh_off;
46 nh_off += sizeof(struct vlan_hdr);
47 if (data + nh_off > data_end)
48 return rc;
49 h_proto = vhdr->h_vlan_encapsulated_proto;
50 }
51
52 if (h_proto == htons(ETH_P_IP))
53 ipproto = parse_ipv4(data, nh_off, data_end);
54 else if (h_proto == htons(ETH_P_IPV6))
55 ipproto = parse_ipv6(data, nh_off, data_end);
56 else
57 ipproto = 0;
58
59 /* lookup map element for ip protocol, used for packet counter */
60 value = bpf_map_lookup_elem(&rxcnt, &ipproto);
61 if (value)
62 *value += 1;
63
64 /* swap MAC addrs for UDP packets, transmit out this interface */
65 if (ipproto == IPPROTO_UDP) {
66 swap_src_dst_mac(data);
67 rc = XDP_TX;
68 }
69 return rc;
70 }
具体地:
struct xdp_md *ctx
中有包数据的 start/end 指针,可用于直接访问包数据。data_end
比较,确保内存访问不会越界。htons()
。将这段程序安装到网卡接口上时,它首先会被编译成 eBPF 字节码,然后经受校验器检查。 这里的检查项包括:
XDP 系统由四个主要部分组成:
这四部分加在一起,创造了一个编写自定义包处理应用的强大环境,它能加速包处理的关键 路径,同时还与内核及现有基础设施密切集成。
接下来看一下 XDP 应用的性能。
DPDK 是目前性能最高的包处理框架 [18],因此本文将 XDP 与 DPDK 及 Linux 内核网络 栈的性能做一个对比。测试机器环境:
在测试中,我们主要关心三个 metric:
直接弃包(packet drop)性能。
为展示最高的包处理性能,我们将用最简单的操作 —— 丢弃接收到的包 —— 来测试。 这个测试能有效测量系统的整体开销,也是真正的包处理应用能达到的性能上限。
CPU 使用量。
如引言中指出,XDP 的优点之一是 CPU 使用量与流量大小是正相关的,而无需预留专 门的 CPU 给它用。我们通过测量 CPU 利用率随网络负载的变化来量化这个指标。
包转发性能。
转发的复杂性要更高一些,例如,涉及到与多块网卡的交互、重写二层头等等。 这里会将转发延迟也考虑进去。
我们已经验证,使用 MTU(1500 字节)包时,我们的系统单核就能达到线速(100 Gbps), 而且 CPU 有 50% 是空闲的。显然,真正的挑战在于 PPS,而非带宽,其他一些测试也已经指出了这一点 [46]。 出于这个原因,我们用最小包(64 字节)测试,衡量指标是 PPS。
对于 XDP 和 Linux 内核网络栈的测试,由于它们没有显式指定某些 CPU 来处理网络包的方式,因此我们通过配置硬件 RSS(Receive Side Scaling)来讲流量定向到指定 CPU。
对网卡、内核的一些配置调优,见代码仓库 [22]。
Fig 3. 直接弃包(packet drop)性能。DPDK 需要预留一个 CPU 运行控制任务,因此只剩下 5 个 CPU 做包处理。
上图是性能与 CPU 数量的关系。
24Mpps/core
,DPDK 是 43.5Mpps/core
。再看图中 Linux 网络栈在两种配置下的性能:
conntrack 模式达到了 1.8Mpps/core,raw 模式是 4.8Mpps/core ;这两种模式均未达到硬件瓶颈。 最终的性能,XDP 比常规网络栈的最快方式快了 5 倍。
Linux raw mode test 中,我们还测量了 XDP 程序不丢弃包,而是更新包数统计然后将包 送到内核网络栈的场景。 这种情况下,XDP 单核的处理性能会下降到 4.5Mpps/core,有 13.3ns 处理延迟。 图中并未给出这个测试结果,因为这个开销太小了。
Fig 4. 直接弃包(packet drop)场景下的 CPU 利用率。
用系统提供的 mpstat
命令测量 CPU 利用率。结果如图 4 。
XDP 和 Linux 内核网络栈都是随流量平滑增长:前面一小段是非线性的,后面基本是线性的。
前面非线性主要是硬中断带来的固定开销,在流量很小时这一部分占比较大。
这个测试中,转发应用执行非常简单的 MAC 地址重写:直接交换源和目的 MAC 地址,然后转发。 这是转发场景下最精简的步骤了,因此结果代表了所有真实转发应用的性能上限。
Fig 5. 转发性能。在同一网卡接口上收发会占用同一 PCI port 的带宽, 这意味着在 70Mpps XDP same-nic 组就已经达到了 PCI 总线的瓶颈
如图 5 所示,性能随 CPU 数量线性扩展,直到达到全局性能瓶颈。 XDP 在同网卡转发的性能远高于 DPDK 异网卡性能,原因是内存处理方式不同:
表 1. 转发延迟。机器网卡的两个接口直连,在转发速率分别为 100pps 和 1Mpps 的条件下,持续 50s 测量端到端延迟
高 pps 场景下,XDP 的延迟已经接近 DPDK。但在低 pps 场景下,XDP 延迟比 DPDK 大的多,原因是 XDP 是基于中断的,中断处理时间( interrupt processing time)此时占大头;而 DPDK 是轮询模式,延迟相对比较固定。
上一节已经看到,XDP 相比于常规 Linux 网络栈性能有了显著提升。但对于大部分 XDP 场景来说,性能还是与 DPDK 有差距。我们认为,这是主要是因为 DPDK 做了相当多的底层 代码优化。举个例子来解释,考虑 packet drop 例子:
41.6ns/packet
22.9ns/packet
多出来的 18.7ns
在我们的 3.6GHz 机器上对应 67 个时钟周期。因此,很显然
每个很小的优化在这里都会产生很大的影响。例如,我们测量出在测试
机器上,每次函数调用需要 1.3ns。 mlx5 驱动处理每个包都有 10 次
函数调用,总计就是 13ns。
另外,在 Linux 这样的通用目的操作系统中,某些开销是不可避免的, 因为设备驱动或子系统的组织方式是为了实现更好的扩展和配置,而非极致性能。
但是,我们认为有些优化还是有必要的。例如,我们尝试将内核中与测试网卡无关的 DMA 函数调用删掉,
这样将前面提到的 10 个函数调用降低到了 6 个,测试结果显示这将单核性能提升到了 29Mpps/core。
依此推测的话,将另外 6 个函数调用也优化掉,能将 XDP 的性能提升到 37.6Mpps
。
实际上我们不可能将 6 个全部去掉,但去掉其中几个,再加上一些其他优化,我
们相信 XDP 和 DPDK 的性能差距将越来越小。
其他驱动的测试结果也是类似的,例如 i40e driver for 40 Gbps Intel cards。
基于以上讨论,我们相信未来 XDP 与 DPDK 的性能差距将越来越小。
另一方面,考虑到 XDP 在灵活性和与内核集成方面的优势, XDP 已经是很多实际场景中的非常有竞争力的方式。下文给出几个例子。
本节给出三个例子来具体展示 XDP 在真实世界中的应用。 这几个案例都是已经真实在用的,但本文出于解释目的,将使用简化的版本。 同时也建议读者参考 [38],后者是独立的文章,介绍使用 XDP 解决实际工作中网络服务所面临的一些挑战。
本节目的是展示真实 XDP 方案的可行性,因此不会将重点放在与业界最新的实现做详尽性能对比上。 我们会拿常规的 Linux 内核网络栈的性能作为 baseline,来对比 XDP 应用的性能。
Linux 内核实现了一个功能完整的路由表,作为数据平面,支持
对于控制平面,Bird [10] 或 FRR [17] 这样的路由守护进程( routing daemons)实现了多种路由控制平面协议。Linux 提供的这套生态系统功能如此丰富 ,因此再在另一个包处理框架中重新实现一套类似的路由栈代价将非常高, 更实际的方式是对 Linux 内核的数据平面进行优化。
XDP 非常适合做这件事情,尤其是它提供了一个 helper 函数,能从 XDP 程序中直接查询内核路由表。
为展示 XDP 路由的性能,我们用 Linux 内核代码中的 XDP routing 例子 [1],与常规 Linux 内核网络栈的性能做对比。 两组测试:
路由表中有从 routeviews.org 中 dump 而来的全球 BGP 路由表(global BGP routing table)。 包含 752,138 条路由。随机生成 4000 个目的 IP 地址,以确保能充分利用到这种路由表。
如果目的 IP 地址少于 4000 个,实际用到的路由表部分会较小,能够保存在 CPU 缓存中,使得结果不准确。 增大 IP 数量至 4000 个以上,不会对转发性能造成影响,但可以避免缓存导致的结果不准问题。
对于两组测试,下一跳 MAC 地址都是与我们的发送网卡直接相关的接口的地址。
Fig 6. 软件路由的性能。由于性能随核数线性增加,这里只给出单核的结果。
测试结果如上图所示。
这说明,XDP 路由程序 + 单核 + 10Gbps 网卡 的软硬件配置,就能 处理整张全球 BGP 路由表(保守估计每个包平均 300 字节)。
DoS 攻击还是像瘟疫一样纠缠着互联网,现在通常的方式是:通过已经入侵的大量设备发起分布式(DDoS)攻击。
有了 XDP 之后,我们能直接在应用服务器(application servers)上 部署包过滤程序来防御此类攻击(inline DoS mitigation), 无需修改应用代码。如果应用是部署在虚拟机里,那 XDP 程序还可以 部署在宿主机(hypervisor)上,这样单个程序就能保护机器上所有的虚拟机。
为展示工作原理,我们用 XDP 作为过滤机制,模拟 Cloudflare 的 DDoS 防御架构 [6]。 他们的 Gatebot architecture ,首先在各 PoP 点机器上采样,然后统一收起来做分析, 根据分析结果生成防御规则。
防御规则的形式是对包数据(payload)进行一系列简单检查, 能直接编译成 eBPF 代码然后分发到 PoP 点的所有服务器上。这里说的代码是 XDP 程序 ,它会将匹配到规则的所有流量丢弃,同时将统计信息更新到 BPF map。
为验证这种方案的性能,我们编写一个 XDP 程序,它
我们用 netperf 做性能压测 [26]。
用 netperf TCP round-trip benchmark,单个 TCP 连接来回小的 request/reply,统计 transactions/second。
模拟的是交互式应用,例如小的远程过程调用(RPC)。
实验在单核上进行,模拟多个流量(正常流量+攻击流量)竞争同一物理资源的场景。
在 beseline 35K 业务 TPS(transactions per second)基础上,打少量 UDP 流量作为攻击流量。逐渐加大攻击流量,观察 TPS 的变化。
Fig 7. DDoS 性能。业务吞吐(TPS)随攻击流量的变化。
结果如上图所示,
以上结果表明,XDP 防御 DDoS 攻击在实际中是完全可行的,单核就能轻松处理 10Gbps 的、都是最小包(minimum-packet)的 DoS 流量。 这种 DDoS 防御的部署更加灵活,无需硬件或应用做任何改动。
负载均衡的场景,我们用 Facebook 开源的 Katran 作为例子 [15]。 Katran 的工作原理是对外通告服务的 IP,这样目标是这个 IP 的流量就会被路由到 XDP 实现的负载均衡器。
在这个过程中,XDP 程序负责哈希、封装以及将包从接收网卡再发出去的任务。 配置信息存储在 BPF map 中,整个封装逻辑是完全在 eBPF 中实现的。
为测试性能,我们给 Katran XDP 程序配置几个固定的目标机器。 对照组是 IPVS,它是 Linux 内核的一部分。性能如表 2 所示,随 CPU 数量线性增长, XDP 比 IPVS 性能高 4.3 倍。
表 2. 负载均衡器性能(Mpps)
配置:1 VIP/core, 100 DstIPs/VIP.
XDP 已经能用于解决真实问题,但作为 Linux 内核的一部分,XDP 还在快速开发过程中。
前面提到,加载到 eBPF 虚拟机的程序必须保证其安全性(不会破坏内核),因此对 eBPF 程序作了一下限制,归结为两方面:
由于校验器的首要职责是保证内核的安全,因此其校验逻辑比较保守, 凡是它不能证明为安全的,一律都拒绝。有时这会导致假阴性(false negatives), 即某些实际上是安全的程序被拒绝加载;这方面在持续改进。
相比于用户空间 C 程序,eBPF 程序的另一个限制是缺少标准库,包括 内存分配、线程、锁等等库。
这个限制其实也是可以绕过的:将 XDP 程序组织成程序数组,通过尾 调用,根据包上下文在程序之间跳转,或者是将几个程序做 chaining。
XDP 程序运行在内核,因此常规的用户空间 debug 工具是用不了的,但内核自带的 debug 和 introspection 功能是可以用在 XDP (及其他 eBPF 程序)上的。 包括:
但不熟悉内核生态系统的开发者可能会对这些工具感到非常陌生,难以使用。因此,也出 现了一些更方便普通开发者的工具,包括 BCC [50]、bpftool [8]、libbpf 函数库 [30] 等等。
设备要支持 XDP,需要实现内核核心网络栈暴露出的一个 API。 写作本文时 Linux 4.18 已经有 12 种驱动支持 XDP,包括了大部分高速网卡。 最新列表见 [2]。
随着 XDP 系统的不断成熟,核心代码逐渐上移到内核中,驱动需要维护的代码越 来越少。例如,redirection action 支持新的 target 时,无需驱动做任何改动。
最后,对于那些不支持 XDP 的驱动,内核提供了 Generic XDP feature [39],这是软件实现的 XDP,性能会低一些, 在实现上就是将 XDP 的执行上移到了核心网络栈(core networking stack)。
XDP 在内核收包函数 receive_skb() 之前,
Generic XDP 在 receive_skb() 之后,
更多关于 Generic XDP,可参考参考: (译) 深入理解 Cilium 的 eBPF 收发包路径(datapath)(KubeCon, 2019)。 译注。
XDP 和 DPDK 之间还有一些性能差距,一些改进工作正在进行中:
当前,XDP 还没有任何 QoS 机制。 尤其是,如果对端已经过载(例如两端的网络速度或特性不匹配),XDP 程序是收不到任何背压(back-pressure)的,
虽然 XDP 中缺少 QoS,但 Linux 内核网络栈中却有很多业界最佳的 Active Queue Management (AQM) 特性和 packet scheduling algorithms [23]。 这些特性中,部分并不适用于 XDP,但我们相信能够 以一种对包处理应用完全透明的方式,选择其中部分集成到 XDP。 我们计划对这一方向进行更深入研究。
我们已经证明 XDP 能在保留操作系统原有功能的前提下,集成到操作系统中,实现高速包数据。
目前的 XDP 还是用于无状态包处理(stateless packet processing) ,如果将这个模型扩展到有状态传输层协议(stateful transport protocols),例如 TCP,它能给依赖可靠/有状态传输的应用提供类似的性能提升。
实际上,已经有一些研究证明,相比于操作系统的协议栈,accelerated transport protocols 能显著提升性能[5, 25, 35, 52]。其中的一个解决方案 [52] 表明,在保留内 核 TCP 协议栈的的前提下,原始包处理性能(raw packet processing)存在巨大的提升 空间。
XDP 非常适用于这种场景,目前也已经有一些关于如何实现的初步讨论 [21], 虽然离实际使用还很远,但仍然是一个令人振奋的、扩展 XDP 系统 scope 的方向。
3.1 小节提到,XDP 程序能将数据包重定向到用户空间应用(userspace application)打 开的特殊类型 socket。这可以用于加速客户端和服务端在同一台机器 的网络密集型应用(network-heavy applications running on the local machine)。
更多信息可参考: (译) 利用 ebpf sockmap/redirection 提升 socket 性能(2020)。 这里使用的是 BPF 而非 XDP,但核心原理是一样的,只是程序执行的位置(hook)不同。 译注。
但在目前的实现中,这种方式在底层仍然需要拷贝包数据,因此性能会打折扣。
目前已经有工作在进行,通过 AF_XDP 实现真正的数据零拷贝。但这项工作需要 对网络设备的内存处理过程有一些限制,因此需要设备驱动的显式支持。 第一个支持这个功能的 patch 已经合并到 4.19 内核,更多驱动的支持 正在添加中。初步的性能测试结果还是很乐观的,显示能达到 20Mpps/core 的内核到用户 空间传递(transfer)速度。
正如 DPDK 用于高层包处理框架的底层构建模块(例如 [31]),XDP 有望成为高层应用的运行时环境 (runtime environment for higher-level applications)。
实际上,我们看到一些基于 XDP 的应用和框架已经出现了。包括
甚至还有人尝试将 XDP 作为 DPDK 的一种底层驱动 [53]。
本文描述了 XDP,一个安全、快速、可编程、集成到操作系统内核的包处理框架。 测试结果显示,XDP 能提供 24Mpps/core 的高处理性能,这一数字虽然与基于 kernel bypass 的 DPDK 仍有差距,但提供了其他一些非常有竞争力的优势:
相比于 kernel bypass 这种非此即彼、完全绕开内核的方式,我们相信 XDP 有更广阔的的应用前景。 Facebook、Cloudflare 等公司实际落地的 XDP 应用,更加增强了我们的这种信心。
最后,XDP 系统还在快速发展,前面也列出了一些正在未来可能会做的开发/优化工作。
XDP has been developed by the Linux networking community for a number of years, and the authors would like to thank everyone who has been involved. In particular,
We also wish to extend our thanks to the anonymous reviewers, and to our shepherd Srinivas Narayana, for their helpful comments.