大部分网络数据的最终生产者和消费者都是应用程序,在一个计算机中,网络数据包需要经过网卡 <=> 系统内核 <=> 应用程序,才能完成传输。
Linux 有严格的内核和用户空间隔离,网络数据在内核和应用程序之间的传输需要频繁的进行上下文切换,随之带来额外的CPU cycle 开销。所以为了提升网络性能,在越来越多的SDN场景都采用了kernel-bypass的技术。其中具有代表性就是DPDK,不过DPDK 在带来性能提升的同时,也有一些问题:
- 首先,因为改变了现有操作系统的工作方式,很难与现有操作系统集成
- 因为网络路径中没有了操作系统,相关的网络应用程序需要重新实现之前由操作系统提供的一些功能,例如路由表,4-7层网络协议
- 一些由操作系统提供的熟悉的管理部署工具将不再可用,因为操作系统现在没有相关网络硬件的控制权
- 因为上面的原因带来的复杂性
- 破坏了原有操作系统内核提供了的安全性,这一点在容器场景尤其重要,因为在容器场景中,资源的抽象和隔离主要是由操作系统内核提供的
- 需要消耗1个或者多个CPU核来专门处理网络包
相对于DPDK,XDP具有以下优点
- 无需第三方代码库和许可
- 同时支持轮询式和中断式网络
- 无需分配大页
- 无需专用的CPU
- 无需定义新的安全网络模型
XDP(eXpress Data Path)是近年兴起的网络数据面技术,为Linux内核提供了高性能、可编程的网络数据通路。
不同于kernel-bypass技术,XDP 是在网络包在还未进入网络协议栈之前就处理,所以既没有内核-用户空间的切换开销,又没有保留了操作系统控制网络硬件的能力。
XDP(eXpress Data Path)提供了一个内核态、高性能、可编程 BPF 包处理框架。
XDP 的处理方式在内核的RX 路径上添加一个早期hook,让用户可以使用eBPF程序控制数据包。该hook 在中断处理之后放置在NIC驱动程序中,并且在网络堆栈本身的所有内存分配之前,因为内存分配可能是一项高成本的操作。由于这种设计,XDP 可以使用商用硬件每秒每核丢弃 2600 万个数据包。
XDP 系统由4个主要部分组成:
- XDP driver hook:这是XDP程序的接入点,当网络数据包从硬件中收到时会被执行。
- eBPF virtual machine:执行XDP 程序的字节码,并且JIT 编译到机器码
- BPF maps:key/value store,用来在整个XDP 系统中做数据的交互
- eBPF verifier:在程序加载到内核之前静态的分析、检查代码,以确保代码会Crash 或者损坏运行的内核。
GRO(Generic receive offload):通用receive offload,offload 详见XDP 硬件要求小节。
RPS/RFS(Receive Package Steering / Receive Flow Steering):用以在软件层面实现报文在多个cpu之间的负载均衡以及提高报文处理的缓存命中率。
Linux kernel 4.8 开始支持XDP,XDP 依赖于eBPF ,所以需求较新的内核支持eBPF,可以参考eBPF 基础架构及使用。
大部分支持 XDP 的驱动都支持在不会引起流量中断(traffic interrupt)的前提下原子地替换运行中的程序。出于性能考虑,支持 XDP 的驱动只允许 attach 一个程序 ,不支持程序链(a chain of programs)。如果有必要的话,可以通过尾调用来对程序进行拆分,以达到与程序链类似的效果。
使用XDP 对网卡有一些要求
- 支持多队列的网卡
- 一般的协议通用offload
- TX/RX checksum offload,即校验offload,利用网卡计算校验和,而不是。
- Receive Side Scaling,RSS 即接收端伸缩,是一种网络驱动程序技术,可在多处理器或多处理器核心之间有效分配接收到的网络数据包并处理。
- Transport Segmentation Offload,TSO,即,是一种利用网卡替代CPU对大数据包进行分片,降低CPU负载的技术。
- 最好支持LRO,aRFS
目前越来越多的网卡设备开始支持offload特性,以便提升网络收发和处理的性能。本文所描述的offload特性,主要是指将原本在协议栈中进行的IP分片、TCP分段、重组、checksum校验等操作,转移到网卡硬件中进行,降低系统CPU的消耗,提高处理性能。
XDP 总共支持三种工作模式(operation mode):
- xdpdrv
xdpdrv 表示 native XDP(原生 XDP), 意味着 BPF 程序直接在驱动的接收路 径上运行,理论上这是软件层最早可以处理包的位置(the earliest possible point)。这是常规/传统的 XDP 模式,需要驱动实现对 XDP 的支持,目前 Linux 内核中主流的 10G/40G 网卡都已经支持。
- xdpgeneric
xdpgeneric 表示 generic XDP(通用 XDP),用于给那些还没有原生支持 XDP 的驱动进行试验性测试。generic XDP hook 位于内核协议栈的主接收路径(main receive path)上,接受的是 skb 格式的包,但由于 这些 hook 位于 ingress 路 径的很后面(a much later point),因此与 native XDP 相比性能有明显下降。因 此,xdpgeneric 大部分情况下只能用于试验目的,很少用于生产环境。
- xdpoffload
最后,一些智能网卡(例如支持 Netronome’s nfp 驱动的网卡)实现了 xdpoffload 模式 ,允许将整个 BPF/XDP 程序 offload 到硬件,因此程序在网卡收到包时就直接在网卡进行处理。这提供了比native XDP 更高的性能,虽然在这种模式中某些 BPF map 类型和BPF 辅助函数是不能用的。BPF 校验器检测到这种情况时会直接报错,告诉用户哪些东西是不支持的。除了这些不支持的 BPF 特性之外,其他方面与 native XDP 都是一样的。
这三种模式 iproute2 都实现了,执行 ip link set dev em1 xdp obj [...] 命令时,内核会先尝试以 native XDP 模 式加载程序,如果驱动不支持再自动回退到 generic XDP 模式。如果显式指定了 xdpdrv 而不是 xdp,那驱动不支持 native XDP 时加载就会直接失败,而不再尝试 generic XDP 模式。
简单的说,XDP 就是在网卡驱动中的hook点,在该hook点处打入eBPF 程序,利用eBPF 的事件驱动来完成网络数据包处理:
- 高级语言程序设计,例如C 来完成。
- 编译成eBPF 字节码,llvm等工具已经支持了将C 语言编译成eBPF 字节码。
- 在加载到内核之前,会交给eBPF verifier 静态的分析代码的安全性。
- 加载到内核。
- 在收到网络包时,通过JIT(Just In Time)编译器翻译成机器指令并执行。
在XDP 程序的结束,需要对packet做出一个结论。结论有4+1 种可能:
- XDP_DROP:直接丢包
- XDP_ABORTED:也是丢包,不过会触发一个eBPF 程序错误,可以通过调试工具查看
- XDP_TX:将处理后的packet 发回给相同的网卡
- XDP_PASS:将处理后的packet 传递给内核协议栈
- XDP_REDIRECT 稍微复杂点,它会需要一个额外的参数来表明Redirect 的目的地,这个额外的参数是在XDP 程序返回之前通过一个helper 函数设置。这种方式使得Redirect 可以非常方便的扩展,增加新的Redirect目的地只需要再增加一个参数值即可。目前Redirect的目的地包含了以下几种可能:
- 将处理后的packet转发给一个不同的网卡,包括了转发给连接虚拟机或者容器的虚拟网卡
- 将处理后的packet转发给一个不同的CPU做进一步处理
- 将处理后的packet转发给一个特定的用户空间socket(AF_XDP),这种方式使得XDP也可以直接bypass网络协议栈,甚至进一步结合zero-copy技术降低包处理的overhead
利用eBPF ,嵌入eBPF 网络数据包处理程序至XDP hook 点。
所以重点是如何编写处理网络数据包的程序。
XDP hook 点在网络驱动中,基于eBPF 的事件驱动机制,当XDP 收到网络数据包时,我们的处理程序就会被执行。
传入eBPF 处理程序的ctx 其实就是XDP 元数据,没有sk_buff结构,只有一个 struct xdp_md 指针
Clang 编译生成对象文件,并加载
然后用iproute2 里面的 ip link 命令加载到某个NIC 上,如ens192
挂载BPF FS,允许BPF 程序从虚拟文件系统固定和获取map