乐趣区

关于边缘计算:Cilium-原理解析网络数据包在内核中的流转过程

上一篇文章,咱们分享了《eBPF 完满搭档:连贯云原生网络的 Cilium》,介绍作为第一个通过 eBPF 实现了 kube-proxy 所有性能的网络插件,Cilium 诞生的背景、倒退演进的过程以及具体的应用示例。本文将重点关注 Cilium 网络的相干知识点,具体介绍 Cilium 是如何在网络流转的门路中做拦挡解决的原理与过程。

  1. 网络分层的宏观视角
  2. Linux 网络协议栈
  3. Linux 接管网络包的流程

01 网络分层的宏观视角

想必大家都应该筹备过这样一道面试题:从输出 URL 到收到申请响应,两头产生了什么事件,笔者当年校招时就常常被问到这个题目。

这个过程讲简单了,恐怕讲个一天一夜也讲不完。此处咱们长话短说,简要形容下大体流程,建设个宏观视角。

首先,来温习下网络分层模型。

如下,左图为 OSI 的规范七层网络模型,这套模型只是停留在概念上的,实现起来太简单了。左边是业界规范的 TCP/IP 模型,Linux 零碎中正是依照 TCP/IP 模型开发的网络协议栈。

接下来回到上文的问题,从输出 URL 到收到申请响应,两头产生了什么事件?

此处简要形容下流程,限于篇幅不一一开展了,当然如果小伙伴对其中某些知识点感兴趣的话,能够自行搜寻相干资料持续深入研究。

  1. 客户端发动网络申请,用户态的应用程序(浏览器)会生成 HTTP 申请报文、并通过 DNS 协定查找到对应的远端 IP 地址。
  2. 用户态的应用程序(浏览器) 会委托操作系统内核协定栈中的上半局部,也就是 TCP/UDP 协定发动连贯申请。此处封装 TCP 头(或 UDP 头)。
  3. 而后经由协定栈下半局部的 IP 协定进行封装,交给上层协定。此处封装 IP 头。
  4. 通过 MAC 层解决,找到接管方的指标 MAC 地址。此处封装 MAC 头。
  5. 最终数据包在通过网卡转化成电信号通过交换机、路由器发送到服务端,服务端通过解决拿到数据,再通过各种网络协议顺次把封装的头解封装,把数据响应给客户端。
  6. 客户端拿到数据进行渲染。

02 Linux 网络协议栈

下面讲述了网络分层原理以及各层的封包解包流程,上面介绍下 Linux 网络协议栈,其实 Linux 网络协议栈就相似于 TCP/IP 的四层构造:

图片取自《你不好奇 Linux 网络发包过程吗?》([3])

通过上图能够看到:

  • 应用程序须要通过零碎调用,来跟 Socket 层进行数据交互;
  • Socket 层的上面就是传输层、网络层和网络接口层;
  • 最上面的一层,则是网卡驱动程序和硬件网卡设施;

03 Linux 接管网络包的流程

同样的,先来个宏观视角,而后再一一介绍,防止一开始就陷入细节无法自拔。

图片取自《你不好奇 Linux 网络发包过程吗?》([3])

能够看到上图比之前介绍的网络封包解包相比,多了上面网卡相干的内容。是的,因为咱们要介绍的是 Cilium 相干的网络根底,所以须要理解数据包是如何穿过 network datapath 的:包含从硬件到内核,再到用户空间。图中有 Cilium logo 的中央,都是 datapath 上 Cilium 重度应用 BPF 程序的中央。

上面将分层介绍。

一个申明,以下图片参考:
[1] Understanding and Troubleshooting the eBPF Datapath in Cilium – Nathan Sweet, DigitalOceanhttps://kccncna19.sched.com/e…
2 深刻了解 Cilium 的 eBPF 收发包门路(datapath)(KubeCon, 2019)http://arthurchiao.art/blog/u…

3.1 L1 -> L2(物理层 -> 数据链路层)

\
网卡收包简要流程:

  1. 网卡驱动初始化。
  2. 网卡取得一块物理内存,作用收发包的缓冲区(ring-buffer)。这种形式称为 DMA(间接内存拜访)。
  3. 驱动向内核 NAPI(New API)注册一个轮询(poll)办法。
  4. 网卡从网络中收到一个包,通过 DMA 形式将包放到 Ring Buffer,这是一个环形缓冲区。
  5. 如果此时 NAPI 没有在执行,网卡就会触发一个硬件中断(HW IRQ),通知处理器 DMA 区域中有包期待解决。
  6. 收到硬中断信号后,处理器开始执行 NAPI。
  7. NAPI 执行网卡注册的 poll 办法开始收包。

对于 NAPI poll 机制:

  • Linux 内核在 2.6 版本中引入了 NAPI 机制,它是混合「中断和轮询」的形式来接管网络包,它的外围概念就是不采纳中断的形式读取数据,而是首先采纳中断唤醒数据接管的服务程序,而后 poll 的办法来轮询数据。
  • 驱动注册的这个 poll 是一个主动式 poll(active poll),执行 poll 办法的是运行在某个或者所有 CPU 上的内核线程(kernel thread),一旦执行就会继续解决,直到没有数据可供解决,而后进入 idle 状态。
  • 比方,当有网络包达到时,网卡发动硬件中断,于是会执行网卡硬件中断处理函数,中断处理函数解决完须要「临时屏蔽中断」,而后唤醒「软中断」来轮询解决数据,一直从驱动的 DMA 区域内接管数据包直到没有新数据时才复原中断,这样一次中断解决多个网络包,于是就能够升高网卡中断带来的性能开销。
  • 之所以会有这种机制,是因为硬件中断代价太高了,因为它们比零碎上简直所有货色的优先级都要高。

NAPI 驱动的 poll 机制将数据从 DMA 区域读取进去,对数据做一些筹备工作,而后交给比它更上一层的内核协定栈。

3.2 L2 数据链路层

此处不会过多展现驱动层做的事件,次要关注 Cilium 波及到的流程,即内核及以上的流程,次要包含:

  • 调配 socket buffers(skb)
  • BPF
  • iptables
  • 将包送到网络栈(network stack)和用户空间

Step 1:NAPI poll

首先,NAPI poll 机制一直调用驱动实现的 poll 办法,后者解决 RX 队列内的包,并最终 将包送到正确的程序。

Step 2:XDP 程序处理

XDP 全称为 eXpress Data Path,是 Linux 内核网络栈的最底层。它只存在于 RX(接收数据)门路上,容许在网络设备驱动外部网络堆栈中数据起源最早的中央进行数据包解决,在特定模式下能够在操作系统分配内存(skb)之前就曾经实现解决。

插播一下 XDP 的工作模式:

XDP 有三种工作模式,默认是 native(原生)模式,当探讨 XDP 时通常隐含的都是指这种模式。

  • Native XDP: XDP 程序 hook 到网络设备的驱动上,它是 XDP 最原始的模式,因为还是先于操作系统进行数据处理,它的执行性能还是很高的,当然须要网卡驱动反对。大部分宽泛应用的 10G 及更高速的网卡都曾经反对这种模式。
  • Offloaded XDP: XDP 程序间接 hook 到可编程网卡硬件设施上,与其余两种模式相比,它的解决性能最强;因为处于数据链路的最前端,过滤效率也是最高的。如果须要应用这种模式,须要在加载程序时明确申明。
  • Generic XDP: 对于还没有实现 native 或 offloaded XDP 的驱动,内核提供了一个 generic XDP 选项,这是操作系统内核提供的通用 XDP 兼容模式,它能够在没有硬件或驱动程序反对的主机上执行 XDP 程序。在这种模式下,XDP 的执行是由操作系统自身来实现的,以模仿 native 模式执行。益处是,只有内核够高,人人都能玩 XDP;毛病是因为是仿真执行,须要调配额定的套接字缓冲区(SKB),导致解决性能降落,跟 native 模式在 10 倍左右的差距。

对于在生产环境应用 XDP,举荐要么抉择 native 要么抉择 offloaded 模式。这两种模式须要网卡驱动的反对,对 于那些不反对 XDP 的驱动,内核提供了 Generic XDP,这是软件实现的 XDP,性能会低一些,在实现上就是将 XDP 的执行上移到了外围网络栈。

持续回来介绍,分两种状况:native/offloaded 模式、general 模式。

(1) native/offloaded 模式:XDP 在内核收包函数 receive_skb() 之前。

(2) Generic XDP 模式:XDP 在内核收包函数 receive_skb() 之后。

XDP 程序返回一个裁决后果给驱动,能够是 PASS, TRANSMIT, 或 DROP。

  • TRANSMIT 十分有用,有了这个性能,就能够用 XDP 实现一个 TCP/IP 负载均衡器。XDP 只适宜对包进行较小批改,如果是大动作批改,那这样的 XDP 程序的性能可能并不会很高,因为这些操作会升高 poll 函数解决 DMA ring-buffer 的能力。
  • 如果返回的是 DROP,这个包就能够间接原地抛弃了,而无需再穿梭前面简单的协定栈而后再在某个中央被抛弃,从而节俭了大量资源。在业界最闻名的一个利用场景就是 Facebook 基于 XDP 实现高效的防 DDoS 攻打,其本质上就是实现尽可能早地实现「丢包」,而不去耗费系统资源创立残缺的网络栈链路,即「early drop」。
  • 如果返回是 PASS,内核会持续沿着默认门路解决包,如果是 native/offloaded 模式,后续达到 clean_rx() 办法;如果是 Generic XDP 模式,将导到 check_taps()上面的 Step 6 持续解说。

Step 3:clean_rx():创立 skb

如果 XDP 返回是 PASS,内核会持续沿着默认门路解决包,达到 clean_rx() 办法。这个办法创立一个 socket buffer(skb)对象,可能还会更新一些统计信息,对 skb 进行硬件校验和查看,而后将其交给 gro_receive() 办法。

Step 4:gro_receive()

GRO([4]) (Generic Receive Offload) 是一种硬件个性的软件实现,能够简略了解成:将包交给网络协议栈之前,把相干的小包合并成一个大包。目标是缩小传送给网络栈的包数,这有助于缩小 CPU 的使用量,进步吞吐量。

  1. 如果 GRO 的 buffer 相比于包太小了,它可能会抉择什么都不做。
  2. 如果以后包属于某个更大包的一个分片,调用 enqueue_backlog 将这个分片放到某个 CPU 的包队列。当包重组实现后,会交给 receive_skb() 办法解决。
  3. 如果以后包不是分片包,间接调用 receive_skb(),进行一些网络栈最底层的解决。

Step 5:receive_skb()

receive_skb() 之后会再次进入 XDP 程序点。

3.3 L2 -> L3(数据链路层 -> 网络层)

Step 6:通用 XDP 解决(gXDP)

上文说过,如果不反对 Native 或 Offloaded 模式的 XDP 的话,将通过 General XDP 来解决,就是此处的 (g)XDP。此处 XDP 的行为 跟 Step 2 中统一,此处不再赘述。

Step 7:Tap 设施解决

图中有个 check_taps 框,但其实并没有这个办法:receive_skb() 会轮询所有的 socket tap,将包放到正确的 tap 设施的缓冲区。

tap 设施监听的是二层协定(L2 protocols)。如果 tap 设施存在,它就能够操作这个 skb 了。Tun/Tap 设施:

  • Tun 设施是一个三层设施,从 /dev/net/tun 字符设施上读取的是 IP 数据包,写入的也只能是 IP 数据包,因而不能进行二层操作,如发送 ARP 申请和以太网播送。
  • Tap 设施是三层设施,解决的是二层 MAC 层数据帧,从 /dev/net/tun 字符设施上读取的是 MAC 层数据帧,写入的也只能是 MAC 层数据帧。从这点来看,Tap 虚构设施和实在的物理网卡的能力更靠近。

Step 8:tc(traffic classifier)解决 \
\
接下来是 TC (Traffic Control),也就是流量管制,TC 更专一于 packet scheduler,所谓的网络包调度器,调度网络包的提早、失落、传输程序和速度管制。和 XDP 一样,TC 的输入代表了数据包如何被处理的一种动作,最新的 Linux 内核中 ([5]) 定义的有 9 种动作:\

#define TC_ACT_OK  0#define TC_ACT_RECLASSIFY 1#define TC_ACT_SHOT  2#define TC_ACT_PIPE  3#define TC_ACT_STOLEN  4#define TC_ACT_QUEUED  5#define TC_ACT_REPEAT  6#define TC_ACT_REDIRECT  7#define TC_ACT_TRAP  8

注:Cilium 管制的网络设备,至多被加载了一个 tc eBPF 程序。

Step 9:Netfilter 解决

如果 tc BPF 返回 OK,包会再次进入 Netfilter。

Netfilter 也会对入向的包进行解决,这里包含 nftables 和 iptables 模块。

def_dev_protocol 框是二层过滤器(L2 net filter),因为 Cilium 没有用到任何 L2 filter,此处就不开展了。

Step 10:L3 协定层解决:ip_rcv()

最初,如果包没有被后面抛弃,就会通过网络设备的 ip_rcv() 办法进入协定栈的三层(L3)—— 即 IP 层 —— 进行解决。

接下来看 ip_rcv(),但这里须要揭示大家的是,Linux 内核也反对除了 IP 之 外的其余三层协定,它们的 datapath 会与此有些不同。

3.3 L3 -> L4(网络层 -> 传输层)

Step 11:Netfilter L4 解决

ip_rcv() 做的第一件事件是再次执行 Netfilter 过滤,因为咱们当初是从四层(L4)的视角来解决 socket buffer,因而,这里会执行 Netfilter 中的任何四层规定(L4 rules)。

Step 12:ip_rcv_finish() 解决

Netfilter 执行实现后,调用回调函数 ip_rcv_finish()。

ip_rcv_finish() 立刻调用 ip_routing() 对包进行路由判断。

Step 13:ip_routing() 解决

ip_routing() 对包进行路由判断,例如看它是否是在 lookback 设施上,是否能路由进来(egress),或者是否被路由,是否被 unmangle 到其余设施等等。

在 Cilium 中,如果没有应用隧道模式(tunneling),那就会用到这里的路由性能。相比隧道模式,路由模式会的 datapath 门路更短,因而性能更高。

Step 14:目标是本机:ip_local_deliver() 解决

依据路由判断的后果,如果包的目标端是本机,会调用 ip_local_deliver() 办法。

ip_local_deliver() 会调用 xfrm4_policy()。

Step 15:xfrm4_policy() 解决

xfrm4_policy() 实现对包的封装、解封装、加解密等工作。例如,IPSec 就是在这里实现的。

最初,依据四层协定的不同,ip_local_deliver() 会将最终的包送到 TCP 或 UDP 协定 栈。这里必须是这两种协定之一,否则设施会给源 IP 地址回一个 ICMP destination unreachable 音讯。

此处拿 UDP 协定作为例子,因为 TCP 状态机太简单了,不适宜这里用于了解 datapath 和数据流。但不是说 TCP 不重要,Linux TCP 状态机还是十分值得好好学习的。

3.4 L4 传输层,以 UDP 为例

Step 16:udp_rcv() 解决

udp_rcv() 对包的合法性进行验证,查看 UDP 校验和。而后,再次将包送到 xfrm4_policy() 进行解决。

Step 17:xfrm4_policy() 再次解决

这里再次对包执行 transform policies 是因为,某些规定能指定具体的四层协定,所以只 有到了协定层之后能力执行这些策略。

Step 18:将包放入 socket_receive_queue

这一步会拿端口(port)查找相应的 socket,而后将 skb 放到一个名为 socket_receive_queue 的链表。

Step 19:告诉 socket 收数据:sk_data_ready()

最初,udp_rcv() 调用 sk_data_ready() 办法,标记这个 socket 有数据待收。

实质上,一个 socket 就是 Linux 中的一个文件描述符,这个描述符有一组相干的文件操 作形象,例如 read、write 等等。

以上 Step 1~19 就是 Linux 网络栈下半局部的全部内容。接下来介绍几个内核函数,都是与过程上下文相干的。

3.5 L4 User Space

下图右边是一段 socket listening 程序,这里省略了谬误查看,而且 epoll 实质上也 是不须要的,因为 UDP 的 recv 办法曾经在执行 poll 操作了。

事实上当咱们调 用 recvmsg() 办法时,内核所做的事件就和下面这段代码差不多。对照左边的图:

  1. 首先初始化一个 epoll 实例和一个 UDP socket,而后通知 epoll 实例咱们想 监听这个 socket 上的 receive 事件,而后等着事件到来。
  2. 当 socket buffer 收到数据时,其 wait queue 会被上一节的 sk_data_ready() 办法置位(标记)。
  3. epoll 监听在 wait queue,因而 epoll 收到事件告诉后,提取事件内容,返回给用户空间。
  4. 用户空间程序调用 recv 办法,它接着调用 udp_recv_msg 办法,后者又会 调用 cgroup eBPF 程序 —— 这是本文呈现的第三种 BPF 程序。Cilium 利用 cgroup eBPF 实现 socket level 负载平衡:

    • 个别的客户端负载平衡对客户端并不是通明的,即,客户端利用必须将负载平衡逻辑内置到利用里。
    • 有了 cgroup BPF,客户端基本感知不到负载平衡的存在。
  5. 本文介绍的最初一种 BPF 程序是 sock_ops BPF,用于 socket level 整流(traffic shaping),这对某些性能至关重要,例如客户端级别的限速(rate limiting)。
  6. 最初,咱们有一个用户空间缓冲区,寄存收到的数据。

以上就是 Cilium 波及到网络数据包在内核流转的过程, 实际上内核 datapath 要远比这里讲的要简单。

  1. 后面只是非常简单地介绍了协定栈每个地位(Netfilter、iptables、eBPF、XDP)能执行的动作。
  2. 这些地位提供的解决能力是不同的。例如:

    • XDP 可能是能力最受限的,因为它只是设计用来做疾速丢包(fast dropping)和 非本地重定向(non-local redirecting);但另一方面,它又是最快的程序,因为 它在整个 datapath 的最后面,具备对整个 datapath 进行短路解决(short circuit the entire datapath)的能力。
    • tc 和 iptables 程序能不便地 mangle 数据包,而不会对原来的转发流程产生显著影响。

了解这些货色十分重要,因为这是 Cilium 乃至狭义 datapath 里十分外围的货色。如果遇到底层网络问题,或者须要做 Cilium/kernel 调优,必须要了解包的收发 / 转发门路,也心愿本文的分享能对大家有所帮忙。

参考资料:

[1] Understanding and Troubleshooting the eBPF Datapath in Cilium – Nathan Sweet, DigitalOcean:https://kccncna19.sched.com/e…
[2] [译] 深刻了解 Cilium 的 eBPF 收发包门路(datapath)(KubeCon, 2019):http://arthurchiao.art/blog/u…
[3] 你不好奇 Linux 网络发包过程吗:https://xie.infoq.cn/article/…
[4] GRO:https://zhuanlan.zhihu.com/p/…
[5] Linux 内核:https://elixir.bootlin.com/li…
[6] 25 张图,一万字,拆解 Linux 网络包发送过程
[7] 图解 Linux 网络包接管过程

退出移动版