目录
-
网络一直都是 I/O 密集型的,但直到最近,这件事情才变得尤其重要。
-
分布式任务(workloads)业界一直都在用,但直到近些年,这种模型才成为主流。虽然何时成为主流众说纷纭,但我认为最早不会早于 90 年代晚期。
-
公有云的崛起,我认为可能是网络成为瓶颈的最主要原因。
-
XDP 是 eXpress DataPath(特快数据路径)。
-
XDP 程序可以直接加载到网络设备上。
-
XDP 程序在数据包收发路径上很前面的位置就开始执行,下面会看到例子。
-
编写一段 BPF 程序
-
编译这段 BPF 程序
-
用一个特殊的系统调用将编译后的代码加载到内核
-
初始化以太帧结构体(ethernet packet)。
-
如果不是 ARP 包,直接退出,将包交给内核继续处理。
-
至此已确定是 ARP,因此初始化一个 ARP 数据结构,对包进行下一步处理。例 如,提取出 ARP 中的源 IP,去之前创建好的黑名单中查询该 IP 是否存在。
-
如果存在,返回丢弃判决(XDP_DROP);否则,返回允许通行判决( XDP_PASS),内核会进行后续处理。
-
快速(fast)
-
灵活(flexible)
-
数据与功能分离(separates data from functionality)
-
eBPF 程序本身并不比 iptables 快,但 eBPF 程序更短。
-
iptables 基于一个非常庞大的内核框架(Netfilter),这个框架出现在内核 datapath 的多个地方,有很大冗余。
-
Steven McCanne, et al, in 1993 – The BSD Packet Filter
-
Jeffrey C. Mogul, et al, in 1987 – first open source implementation of a packet filter.
-
网卡驱动初始化。
-
网卡获得一块物理内存,作用收发包的缓冲区(ring-buffer)。这种方式成为 DMA(直接内存访问)。
-
驱动向内核 NAPI(New API)注册一个轮询(poll )方法。
-
网卡从云上收到一个包,将包放到 ring-buffer。
-
如果此时 NAPI 没有在执行,网卡就会触发一个硬件中断(HW IRQ),告诉处理器 DMA 区域中有包等待处理。
-
收到硬中断信号后,处理器开始执行 NAPI。
-
NAPI 执行网卡注册的 poll 方法开始收包。
-
这是 Linux 内核中的一种通用抽象,任何等待不可抢占状态发生(wait for a preemptible state to occur)的模块,都可以使用这种注册回调函数的方式。
-
驱动注册的这个 poll 是一个主动式 poll(active poll),一旦执行就会持续处理,直到没有数据可供处理,然后进入 idle 状态。
-
在这里,执行 poll 方法的是运行在某个或者所有 CPU 上的内核线程(kernel thread)。虽然这个线程没有数据可处理时会进入 idle 状态,但如前面讨论的,在当前大部分分布 式系统中,这个线程大部分时间内都是在运行的,不断从驱动的 DMA 区域内接收数据包。
-
poll 会告诉网卡不要再触发硬件中断,使用软件中断(softirq)就行了。此后这些 内核线程会轮询网卡的 DMA 区域来收包。之所以会有这种机制,是因为硬件中断代价太 高了,因为它们比系统上几乎所有东西的优先级都要高。
-
分配 socket buffers(skb)
-
BPF
-
iptables
-
将包送到网络栈(network stack)和用户空间
-
Transmit 非常有用,有了这个功能,就可以用 XDP 实现一个 TCP/IP 负载均衡器。XDP 只适合对包进行较小修改,如果是大动作修改,那这样的 XDP 程序的性能 可能并不会很高,因为这些操作会降低 poll 函数处理 DMA ring-buffer 的能力。
-
更有趣的是 DROP 方法,因为一旦判决为 DROP,这个包就可以直接原地丢弃了,而 无需再穿越后面复杂的协议栈然后再在某个地方被丢弃,从而节省了大量资源。如果本次 分享我只能给大家一个建议,那这个建议就是:在 datapath 越前面做 tuning 和 dropping 越好,这会显著增加系统的网络吞吐。
-
如果返回是 PASS,内核会继续沿着默认路径处理包,到达 clean_rx() 方法。
-
GRO 给协议栈提供了一次将包交给网络协议栈之前,对其检查校验和修改协议头和发送应答包(ACK packets)的机会。
-
如果 GRO 的 buffer 相比于包太小了,它可能会选择什么都不做。
-
如果当前包属于某个更大包的一个分片,调用 enqueue_backlog 将这个分片放到某个 CPU 的包队列。当包重组完成后,会交给 receive_skb() 方法处理。
-
首先初始化一个 epoll 实例和一个 UDP socket,然后告诉 epoll 实例我们想 监听这个 socket 上的 receive 事件,然后等着事件到来。
-
当 socket buffer 收到数据时,其 wait queue 会被上一节的 sk_data_ready() 方法置位(标记)。
-
epoll 监听在 wait queue,因此 epoll 收到事件通知后,提取事件内容,返回给用户空间。
-
用户空间程序调用 recv 方法,它接着调用 udp_recv_msg 方法,后者又会 调用 cgroup eBPF 程序 —— 这是本文出现的第三种 BPF 程序。Cilium 利用 cgroup eBPF 实现 socket level 负载均衡,这非常酷:
-
一般的客户端负载均衡对客户端并不是透明的,即,客户端应用必须将负载均衡逻辑内置到应用里。
-
有了 cgroup BPF,客户端根本感知不到负载均衡的存在。
-
本文介绍的最后一种 BPF 程序是 sock_ops BPF,用于 socket level 整流(traffic shaping ),这对某些功能至关重要,例如客户端级别的限速(rate limiting)。
-
最后,我们有一个用户空间缓冲区,存放收到的数据。
-
前面只是非常简单地介绍了协议栈每个位置(Netfilter、iptables、eBPF、XDP)能执行的动作。
-
这些位置提供的处理能力是不同的。例如:
-
XDP 可能是能力最受限的,因为它只是设计用来做快速丢包(fast dropping)和 非本地重定向(non-local redirecting);但另一方面,它又是最快的程序,因为 它在整个 datapath 的最前面,具备对整个 datapath 进行短路处理(short circuit the entire datapath)的能力。
-
tc 和 iptables 程序能方便地 mangle 数据包,而不会对原来的转发流程产生显著影响。