云原生网络利器--Cilium 之 eBPF 篇
在上一篇《 云原生网络利器--Cilium 总览篇》的文章中,整体的介绍了一下 Cilium 中常见技术术语和总体的架构介绍。接下来的篇幅会重点介绍 eBPF 的一些关键特性,以及在 Cilium 中使用 eBPF 做了哪些工作,并介绍基本的原理。
总述
eBPF 是一种可以在不改变 Kernel 的前提下,开发 Kernel 相关能力的一种技术。开发 eBPF 的程序需要使用 C 语言,因为 Kernel 是 C 语言开发的,eBPF 在开发的过程中会依赖 Kernel 的 uapi 以及 Linux 的 Hepler 方法来完成处理。Linux 是基于事件模型的系统,也支持了 eBPF 类型 Hook,在一些 Hook 点执行挂载的 eBPF 程序。在不同的挂载点,支持不同类型的 eBPF 程序类型。
为什么 eBPF 会有类型?主要考虑的是,Linux 本身的能力非常多,而提供给外部可调用的方法是很少的,那就需要为不同的场景和能力,开放不同的系统调用能力,哪些能力在哪些类型下可以被调用,就跟 eBPF 的程序类型有关系了。
举例,对于处理网络的 eBPF 程序,不需要给这类型的程序开放处理存储的能力。对于一个程序而言,主要就包含了数据结构和算法两个部分。
在 eBPF 中也是一样,程序自己的数据结构是可以自由定义的,但是有一 些和内核相关的能力以及 eBPF 内置提供的数据处理,就必须要依赖特定的数据结构,这里的数据结构就是 eBPF 的不同类型的 Maps。
接下来就是算法,eBPF 的算法是有一定的限制的,对于要调用内核的能力的,需要通过 Linux Hepler 的方式来完成调用,而 Linux Hepler 是跟着 Linux 的内核版本走的,当需要 Linux 开放一些新的能力给外部,就需要开发 Linux 的 Helper,合并到 Kernel 之后,eBPF 的程序才可以使用。
还有一部分的算法就是 eBPF 程序自己的业务逻辑了,同时在 eBPF 的算法部分中,有一个很重要的部分,就是 eBPF 程序之间互相调用的能力,这里有一个专业术语叫 Tail Call (尾部调用)。可以理解成,eBPF A 程序,调用 eBPF B 程序的时候,是经过 Tail Call 的方式,这种方式的调用,程序调用不会有返回,而是直接进入到另外一个程序,但是不同 eBPF 程序之间是可以通过 Pin 的机制共享这些数据结构的。
Cilium 中使用了大量得 eBPF 技术,实现了 K8s 的容器网络的能力。
02
技术术语
为了更好的理解 Cilium 实现的数据平面的能力,首先需要对一些常见的 eBPF 的相关概念,有一个初步的认识,这样能更好的帮助理解原理。以下整理一些技术术语,不包含所有的,只是列出来一些主要的点。
eBPF 的命令:
对 eBPF 的常见操作是通过系统调用 SYSCALL 完成的。常见的操作:对 eBPF Map 的增删改查操作;Object 对象的 Pin 和 Get 操作;eBPF 程序的挂载和卸载。
eBPF 的 Map 类型:
这里描述了 eBPF Maps 支持哪些类型的 Maps,不同类型 Map 的使用方式和场景会不一样。
eBPF 的程序类型:
以下描述了 eBPF 程序的类型种类,不同种类的 eBPF 会完成不同的能力。举例,如果是和 XDP 相关的能力,程序的类型就是 BPF_PROG_TYPE_XDP;如果是 tc 相关的能力,程序的类型就是 BPF_PROG_TYPE_SCHED_CLS 和 BPF_PROG_TYPE_SCHED_ACT。什么样的 Linux Helper 被加载,是由 eBPF 的程序类型决定的。
eBPF 的挂载类型:
不同的 eBPF 程序类型,就会有不同的挂载类型。
eBPF Tail Call:
顾名思义就是尾部调用,主要的特点就是 eBPF 的程序从一个调用到另一个,不像一般的程序语言的方法调用是有返回值,Tail Call 不会有返回值的,而是从一个 eBPF 直接就执行到下一个 eBPF 程序,两个 eBPF 之间是同级的,不是方法调用的关系。
eBPF Section:
可以简单理解成 eBPF 程序的执行入口,可以在程序中指定 Section。举例:__section("from-netdev")。如果不指定的话,会有默认的 Section。在学习 Cilium 的 eBPF 程序的时候,推荐阅读的入口就是以每一种 eBPF 的 C 语言程序的 Section 部分为学习入口。
eBPF Object Pin:
正如字面意思,指的是将 eBPF Map Pin 到 Linux 的 bpffs 这个虚拟的文件系统中。主要的作用是让 eBPF 的 Maps,可以被 eBPF 程序或者用户态的应用读取和共享,在通过 Obj Get 之前,是需要先完成 Pin 动作,才可以被 Get。
Linux Helper:
从 Linux 系统的角度,提供一些对外访问 Linux 系统能力的方法,供外部程序去使用。外部程序要使用 Kernel 的接口,那就只能通过 Linux Helper 来完成。
不同的 Kernel 版本支持的 Linux Helper 是不同的,版本越高,支持的能力越多,高级的能力也越多,使用这些高级能力的实现时,网络性能也会越高,这也是为什么很多 Cilium 的高级能力都需要特定版本的 Kernel。更多详情请参加 bpf-helpers 。
XDP:
XDP 返回码:
tc:
Traffic Control,每一个 Linux 识别的网络设备都支持 tc 的能力,包括物理的和虚拟的。在容器网络相关的网络设备的进出口的地方,可以利用 tc 的能力来完成对网络数据包的控制能力。
同时, tc 是支持两个方向的,一个是 ingress 的方向,一个 egress 的方向。而 XDP 只支持 ingress 的方向。可以理解成使用了 tc 的能力,可以在数据包进入一个网络设备的时候,进行相关处理,在出去的时候,也可以进行数据包的处理。而使用 XDP 的时候,只能在进入网络设备的时候,进行数据包的处理。
挂载 tc 的 eBPF 程序 (这里的 egress 就是方向,sec 就是代表 eBPF 的 Section):
tc 返回码:
返回码代表的是在处理过程中,根据不同的逻辑需要对数据包进行不同的处理行为。举例,当觉得数据包需要被丢弃的时候,就需要在 eBPF 程序中返回 TC_ACT_SHOT / TC_ACT_STOLEN,当觉得数据包被重定向出去的时候,可以返回 TC_ACT_REDIRECT;当觉得数据包需要继续进入到内核的时候,可以返回 TC_ACT_OK。
Socket Redirect:
这是一种可以直接将一个 Socket 的数据包 redirect 到另一个 Socket 的能力,无需经过内核的网络协议栈,就可以加速本地的网络通信能力。注意这里是只在本地 Socket 之间数据包的通信,可以使用这种机制。这个能力也是 Kernel 提供的,属于 eBPF 范畴能力的体现。
具体的使用场景主要包含 Pod 内容器之间的通信,以及主机上的 Socket 网络之间的通信,以及所有可以本地通过 Socket 互相通信的程序之间。不局限于容器,二进制运行的主机进程也可以。
下图提供一种从 Pod 内两个容器的 Socket Redirect 加速场景为例,介绍一下 Socket Redirect 的能力。可以看到 Socket Redirect 大概的使用方式,帮助理解,但是 Socket Redirect 不仅仅局限于这个场景,只要是本地的 Socket 都是可以使用这种特性。
CTX Redirect:
NodePort Acceleration:
基于 XDP 的技术,在南北向 NodePort 访问 K8s 服务的时候,进行网络加速的能力。包括直接 redirect 到本地 Pod;包括 redirect 到当前物理网卡或其它物理网卡的方式,forward 数据包去真正运行 Pod 的机器,这种情况下,会使用到 XDP_REDIRECT 或者 XDP_TX,具体取决于当前使用的 Cilium 版本是不是使用了支持 XDP 多网卡的 Feature。
不管是本地的 Pod,还是其它机器的 Pod,都是使用的 eBPF 的网络技术完成数据包的处理能力,缩短了数据包在整个网络上的传输路径。如上图,物理网卡上的数据包的路径部分的箭头。
03
原理概述
这里的原理概述主要是,从 Cilium 中使用到的网络设备,以及网络设备上被挂载的 eBPF 程序,来理解 Cilium 在数据路径中,实现了哪些有特色的网络能力,以及从中可以看出来实现的大概原理。
cilium_host/cilium_net:
Cilium Agent 在启动的时候,会初始化这一对虚拟网络设备。这是一对 veth pair 的虚拟的网络设备。其中 cilium_net 是 cilium_host 的 parent,而且没有 ip 地址,它是一种 netdevice,可以看成和物理网卡类似的设备。cilium_host 有设置 ip 地址,这个 ip 地址会作为 Pod 的网关,可以查看 Pod 的路由信息,看到对应的网关地址就是 cilium_host 的 ip 地址。
那是不是会理解成容器 Pod 里的数据包要想出去,就先将数据包发往 cilium_host?其实不是,通过 arp 的命令查看对应的 mac 地址,可以看到对应的 mac 地址,其实是 lxc-xxx 的地址,lxc-xxx 是什么,会在下面提到。所以 Pod 出来的数据包的第一跳,真正要经过的路径是 lxc-xxx。
那数据包到达 lxc-xxx 之后会发生什么?这个就要提到挂载在 lxc-xxx 的 tc ingress 的 from-container,这个 Section 对应的 eBPF 程序。那 cilium_net 是负责处理什么?cilium_net 的网络设备上也会挂载 eBPF 程序,这个程序是 bpf_host_cilium_net.o 文件中的 to-host Section,挂载的方向是 tc ingress。主要完成 ctx 的 encrypt 相关的 mark 操作。
在 Kernel 和 Cilium 版本比较低的时候,数据包要进入 Pod,都需要经过 cilium_host 设备,但是在高内核版本和比较新的版本 Cilium 版本中,cilium_host 就显得不那么重要了,主要用来处理本地流量访问 Pod 的情况。cilium_host 挂载的 eBPF 程序通过 tc 的方式完成,包括 from-host 和 to-host。cilium_net 挂载的 eBPF 程序通过 tc 的方式完成,包括 to-host。
cilium_vxlan:
lxc-xxx/eth0:
每一个 Pod 都会有的一对 veth pair。这也是容器网络中最常见 Linux 提供的虚拟网络设备。一端在主机的网络空间,一端在容器的网络空间。
其中 eth0 是容器端的,lxc-xxx 是主机端的。eth0 有自己的 ip 地址,lxc-xxx 是没有 ip 地址的。对于容器的出口流量,使用了 tc ingress 的方式,在 lxc-xxx 主机端的设备上挂载了 eBPF 程序,程序的 Section 是 from-container,具体 from-container 是负责什么,会在下面有相关说明。
每一个 Pod 的对应的 eBPF 程序是在创建 Pod 的时候,由 CNI 调用 Cilium Agent 的 create endpoint 的接口,由这个接口完成 Pod 相关 eBPF 程序的编译和挂载。
现在有了 from-container 来处理 Pod 的出口流量,那是不是应该也有一个 to-container 来负责处理 Pod 的入口流量,同时也挂载在 lxc-xxx 的 tc 上?的确是有 to-container,但是却不是挂载在 lxc-xxx 的 tc 上的,而是保存在 eBPF 的一种 Map 中,这种 Map 是专门用来保存 eBPF 程序的 (BPF_MAP_TYPE_PROG_ARRAY) ,见图中的 POLICY_CALL_MAP 这个 map 中的 item。
tc ingress/egress:
XDP ingress:
eBPF Maps:
XDP from-netdev:
这是一个 eBPF Section,挂载在物理网卡的 tc ingress 上的 eBPF 程序。主要的作用是,完成主机数据包到达主机,对数据包进行处理。
只有当开启了 ENABLE_NODEPORT_ACCELERATION 能力,才会在 XDP 阶段直接接手处理 NodePort 的访问。如果是没有开启的,数据包会直接进入内核,由 tc 的 from-netdev 来完成 NodePort 的处理。
当开启了之后,主要还是和 tc 的 from-netdev 类似,调用 nodeport_lb4 完成 nodeport 类型的 LB 操作。可以处理外部通过 K8s NodePort 的服务访问方式,访问服务,包括 dnat、lb、ct 等操作,将外部的访问流量打到本地的 Pod,或者通过 XDP 的 redirect 的方式,将数据流量转发到相同网卡或者不同网卡,然后最终将数据流量转发到提供服务的 Pod 所在的主机。
除了上述能力,对于 XDP 的 from-netdev,如果开启了 ENABLE_PREFILTER,还会负责 Pre-Filter,可以理解成类似 ddos 的场景,当不被允许的访问流量到达之后,可以进行一个快速的验证,如果不被允许的,会快速的被 Drop 掉。
tc from-netdev:
这是一个 eBPF Section,挂载在物理网卡的 tc ingress 上的 eBPF 程序。主要的作用是,完成主机数据包到达主机,对数据包进行处理。
这里主要有两个场景会涉及到 from-netdev,第一个就是开启了主机的防火墙,这里的防火墙不是 iptables 实现的,而是基于 eBPF 实现的 Host Network Policy,用于处理什么样的数据包是可以访问主机的;第二个就是开启了 NodePort,可以处理外部通过 K8S NodePort 的服务访问方式,访问服务,包括 dnat、lb、ct 等操作,将外部的访问流量打到本地的 Pod,或者通过 tc 的 redirect 的方式,将数据流量转发到 to-netdev 进行 snat 之后,再转发到提供服务的 Pod 所在的主机。
to-netdev:
这是一个 eBPF Section,挂载在物理网卡的 tc egress 上的 eBPF 程序。主要的作用是,完成主机数据包出主机,对数据包进行处理。
这里主要有两个场景会涉及到 to-netdev,第一个就是开启了主机的防火墙,这里的防火墙不是 iptables 实现的,而是基于 eBPF 实现的 Host Network Policy,用于处理什么样的数据包是可以出主机的,通过 handle_to_netdev_ipv4 方法完成;第二个就是开启了 NodePort 之后,在需要访问的 Backend Pod 不在主机的时候,会在这里完成 snat 操作,通过 handle_nat_fwd 方法和 nodeport_nat_ipv4_fwd 方法完成 snat。
from-host:
这是一个 eBPF Section。挂载在 cilium_host 的 tc egress 上 eBPF 程序。主要完成数据流量导入到 Cilium-managed 的 network 中去。举例,如本地进程访问本地主机的 Pod。
具体完成的事情大致包含:
3. 通过 rewrite_dmac_to_host(ctx, secctx) 完成 dst 的 mac 地址的设置,将其 dst 的 mac 设置成,cilium-host 的主机端的 cilium_net 的 mac 地址,也就是下一跳,当经过 cilium-host 的 packet,需经过下面步骤查询到的 endpoint 是 local host 的 endpoint,这个时候就需要回到 host,那回到 host 的时候,就需要设置 packet 的目的 mac 地址为 cilium_net 的 mac 地址,这样 packet 就会被发送到 Kernel Stack 去,回到主机上;
4. 通过 lookup_ip4_endpoint 找到要访问的 endpoint,然后使用 ipv4_local_delivery 方法,完成数据包的转发处理。在处理的过程中,会根据是不是有 Network Policy 来决定是不是要做验证。具体 ipv4_local_delivery 的作用如下:ipv4_local_delivery 这个方法的主要作用,是处理本地的 packet 到 Pod 的核心方法。不管是 Pod 到 Pod 的,还是 cilium_host 到 Pod 的,还是 overlay 到 Pod 的,还是物理网卡到 Pod 的,只要是确定了是本地的 endpoint,那 packet 要进入 Pod 就是要这个方法来处理的。
主要完成的事情包括如下:
to-container:
这是一个 eBPF Section,这是一个比较特别的 eBPF 的程序。它和 Pod 相关,但是却不是挂载在 tc egress 上的,而是以 eBPF 程序,保存在 eBPF 中的 map 中的,eBPF 中有一种 map 类型,是可以保存 eBPF 程序的 (BPF_MAP_TYPE_PROG_ARRAY) ,同时 map 中的程序都有自己的 id 编号,可以用来在 tail call 中直接调用,to-container 就是其中之一,而且是每一个 Pod 都有自己独立的 to-container,和其它和 Pod 相关的 eBPF 程序一样。
它的主要作用就是,在数据包进入 Pod 进入之前,完成 Policy 的验证,如果验证通过了会使用 Linux 的 redirect 方法,将数据包转发到 Pod 的虚拟网络设备上。至于是 lxc-xxx 还是 eth0,看内核的版本支持不支持 redirect peer,如果支持 redirect peer,就会直接转发到 Pod 的 eth0,如果不支持就会转发到 Pod 的主机侧的 lxc-xxx 上。
from-overlay:
这是一个 eBPF Section,在选择使用 Cilium 的 overlay 网络模型时,会使用到和 overlay 相关的虚拟网络设备,这些设备是 Agent 在启动的时候,在主机空间创建出来的,基于 vxlan 的技术完成 overlay 网络。
from-overlay 的挂载点,就是挂载在 cilium_vxlan 这个网络设备的 tc ingress 上的,主要处理的数据包是进入 Pod 的,在进入 Pod 之前会先经过 cilium_vxlan 的 from-overlay 处理。
1. 包括 ipsec 安全相关的处理;
2. 包括将数据包通过 ipv4_local_delivery 方法,传递到后端服务在本机的 Pod 中的 case,其中也会经过 to-container;
3. 通过 NodePort 访问服务时,服务不在本地的时候,使用和 from-netdev 类似的方式,去处理 NodePort 的请求,其中包括 dnat、ct 等操作,最后通过,ep_tail_call(ctx, CILIUM_CALL_IPV4_NODEPORT_NAT) 或者 ep_tail_call(ctx, CILIUM_CALL_IPV4_NODEPORT_DSR) 方法完成数据包的处理,具体是哪种,取决于 NodePort 的 LB 策略,是 snat 还是 dsr。
4. 数据包都是从物理网卡到达主机的,那是怎样到达 cilium_vxlan 的?这个是由挂载在物理网卡的 from-netdev ,根据隧道类型,将数据包通过 redirect 的方式,传递到 cilium_vxlan 的。参见:from-netdev 的 encap_and_redirect_with_nodeid 方法,其中主要有两个方法,一是__encap_with_nodeid(ctx, tunnel_endpoint, seclabel, monitor) 方法完成 encapsulating ,二是 ctx_redirect(ctx, ENCAP_IFINDEX, 0) 方法完成将数据包 redirect 到 cilium_vxlan 对应的 ENCAP_IFINDEX 这个 ifindex。
to-overlay:
这是一个 eBPF Section,在选择使用 Cilium 的 overlay 网络模型时,会使用到和 overlay 相关的虚拟网络设备,这些设备是 Cilium Agent 在启动的时候在主机空间创建出来的,基于 vxlan 的技术完成 overlay 网络。
to-overlay 的挂载点就是挂载在 cilium_vxlan 这个网络设备的 egress 上的。主要完成的工作包括:通过 nodeport_nat_ipv4_fwd 和 snat_v4_process 完成 egress 方向的 snat 操作,snat 的作用是跨主机访问的时候,需要将源地址改成主机的地址。
to-overlay 处理的数据包是通过 lxc-xxx 的 from-container 程序 redirect 过来的数据包,这说明 to-overlay 处理的是 Pod 通过 vxlan 出去的数据包。
from-network:
这是一个 eBPF Section,挂载在物理网络的设备中 tc ingress,如物理机器的 eth0 网卡,但是这个 eBPF 的程序是可选的,只有当设置了 EncryptInterface 的时候,才会编译和挂载这个程序。
主要完成的工作是对数据包的解密操作,如果没有使用 ipsec 的加密,也就是 ENABLE_IPSEC 为 false,这里不会对数据包有任何修改,直接将数据包放回 Kernel。如果使用了 IPSEC,解密的时候,才会使用到这个程序,如果没有使用到,这个程序是不需要编译和挂载的。
主要实现的方式是设置 ctx 的 mark 为 MARK_MAGIC_DECRYPT(0x0D00) ,然后将数据包放回 Kernel Stack,由 Kernel Stack 完成 IPsec decryption。
cgroup/connect,cgroup/bind,cgroup/post_bind,cgroup/sendmsg,cgroup/recvmsg,cgroup/getpeername:
这是一些 eBPF Section,在 Cilium 中,使用了和 cgroup 相关的 eBPF 的能力,主要完成的事情是监测 socket 的 ops。在 socket 的操作过程中,执行相关的 eBPF 程序来完成 Socket LB 能力。
举例,当要创建连接的时候,会先做一次 dnat,找到真正的后端的 Pod 的真实 ip 地址,用真实的 ip 地址来完成客户端和服务端的连接,而不是通过中间一层代理的方式,这样数据通信的效率得到提升,也是上文提到的 Host Reachable Service 的能力体现。包括在创建连接,指定 bind 的地址,以及发送和接受数据包的过程中,都可以做一些特殊的处理,来提升网络性能。
sk_msg / sockops:
这是一些 eBPF Section,在 Cilium 中支持 socket 加速的特性,可以完成本地 socket 和 socket 之间数据包的 redirect,而不需要经过内核底层的网络协议栈。
举例:在一个 Pod 中有两个容器,这两个容器之间会基于 socket 进行通信,这个场景是可以基于这个特性完成加速,当然不仅仅支持这一种 case,只要是本地的 socket 之间,都是可以完成加速,因为加速的范围是基于 cgroup 监听的所有主机上的 socket。
主要实现的方式是依赖于 eBPF 中的 sockmap 和 msg_redirect_hash。sockmap 中保存的是 socket,msg_redirect_hash 是内核提供的,配合 sockmap 完成 socket 到 socket 的数据包的直接 redirect。
具体哪些 socket 需要被处理,通过使用 sockops 中的能力,监测已经建立连接的 socket,将状态为 BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB 的 socket 放到 sockmap 中。sockmap 具体对应的 eBPF 的 map 类型可以是 BPF_MAP_TYPE_SOCKHASH。
04
总结
Cilium 是一个使用 eBPF 的技术实现数据路径,在不同的网络设备中 redirect 数据包,缩短数据路径,达到加速的效果。
特点是高级的特性对 Kernel 的要求比较高,而且不同的 Kernel 版本,会导致不同的数据路径。
要想达到最好的网络性能,推荐最低的 Kernel 的最低版本为 5.10。如果只是想让 Cilium 可以运行起来,那最低的 Kernel 的最低版本是 4.9.17。
本文作者
「DaoCloud 道客」技术合伙人
云原生技术专家
相关链接:
https://cilium.io/
https://docs.cilium.io/en/stable/bpf/
https://www.man7.org/linux/man-pages/man7/bpf-helpers.7.html
DaoCloud 公司简介
「DaoCloud 道客」云原生领域的创新领导者,成立于 2014 年底,拥有自主知识产权的核心技术,致力于打造开放的云操作系统为企业数字化转型赋能。产品能力覆盖云原生应用的开发、交付、运维全生命周期,并提供公有云、私有云和混合云等多种交付方式。成立迄今,公司已在金融科技、先进制造、智能汽车、零售网点、城市大脑等多个领域深耕,标杆客户包括交通银行、浦发银行、上汽集团、东风汽车、海尔集团、屈臣氏、金拱门(麦当劳)等。目前,公司已完成了 D 轮超亿元融资,被誉为科技领域准独角兽企业。公司在北京、武汉、深圳、成都设立多家分公司及合资公司,总员工人数超过 400 人,是上海市高新技术企业、上海市“科技小巨人”企业和上海市“专精特新”企业,并入选了科创板培育企业名单。网址:www.daocloud.io
邮件:info@daocloud.io
电话:400 002 6898