在上一篇文章中,我们简要地解析了 eBPF 内核独立子系统的基本概念、发展历史、架构模型以及优缺点等,具体可参考:Linux eBPF解析。
随着云原生生态领域的逐渐完善,云操作系统,例如:Kubernetes ,正在吞噬着整个容器世界,越来越多的厂商在匆忙地布局着基于云操作系统的相关平台,以便尽可能在有限的时间及空间内获得一杯羹,而此时,在另一端,蜂拥而至的企业们也在不断尝试将自有业务迁移至容器平台,以便享受新技术带来的革命性红利,一时间,Kubernetes 仿佛成了云系统的代名词,火热的一发不可收拾。
基于此,我们可以清晰地看到:围绕不同的企业、不同的业务场景、不同的技术实现需求以及差异化的技术发展趋势,容器的部署密度越来越高,而伴随着容器的生命周期却越来越短。然而,随着业务场景的创新化,基于传统的技术或多或少在某种特定的场景下开始显得很鸡肋,已经开始无法支撑新的需求,于是,新的技术就应运而生,开始尝试解决这一业务痛点。
Cilium 是什么,我们为什么要关注它?这就是本篇文章所要探讨的。
在我们所了解的云原生编排系统中,基于不同的业务场景驱动,微服务的架构也随之进行着不断的演进,而此时,我们所依赖的云操作系统网络也因为各种诉求也随着更替,从最初的 Flannel 、Calico以及到现在的 Cilium 等。从本质上讲,Cilium 是一个基于 eBPF 之上的通用性抽象,即 Cilium 对 eBPF 进行了封装,并提供了一个更上层的 API ,使得其能够覆盖分布式系统的绝大多数场景。为什么 eBPF 如此强大,使得其能够获得青睐?主要取决于以下3方面,具体如下所示:
Fast-快速
eBPF技术无论是基于何种场景,几乎总是比 Iptables 快,相对于 Iptables,eBPF 程序较短。Iptables 基于一个非常庞大的内核框架(Netfilter),此框架出现在内核 Datapath 的多处,冗余性较高。因此,我们在实现类似 ARP Drop 这样相同的功能时,基于 Iptables 技术,冗余就会显得非常笨重,从而导致性能很低。
Flexible-灵活
基于 eBPF ,我们可以实现任何想要的需求,这或许也是最核心的原因。eBPF 基于内核提供的一组接口,运行 JIT 编译的字节码,并将计算结果返回给内核。在此过程中,内核只关心 XDP 程序的返回是 PASS、DROP 还是 REDIRECT。至于在 XDP 程序里做什么, 完全无需知晓。
Separates Data from Functionality-数据功能分离
在内核体系中,Nftables 和 Iptables 也可以将数据与功能进行分离,然而,经综合对比,2者功能并没有 eBPF 强大。例如,eBPF 可以使 用 Per-cpu 的数据结构,因此其能取得更极致、优越的性能体验。
从本质上讲,eBPF 真正的优势在于能够将 “数据与功能分离” 此项事情做的非常干净(Clean Separation)甚至完美。也就是说,可以在 eBPF 程序不中断的情况下修改它的运行方式。具体方式是修改它访 问的配置数据或应用数据,例如黑名单里规定的 IP 列表和域名。具体可参考如下:
我们来看下 Cilium 的相关概念及架构,基于 Cilium 机制,其通常要求运行于 Linux Kernel 4.8.0 及以上版本,官方建议 Kernel 版本至少在 4.9.17 以上,若运行环境为Ubuntu ,则建议其发行版中 Linux 内核版本一般为 4.12 以上,若基于 CentOS 7,则需要升级才能运行 Cilium。为方便起见,Cilium 一般跟容器编排调度器使用同一个 KV 存储数据库,例如在 Kubernetes 中使用 etcd 存储。
接下来,我们了解下Cilium 的组件示意图,Cilium 是位于 Linux Kernel 与容器编排系统的中间层。向上可以为容器配置网络,向下可以向 Linux 内核生成 BPF 程序来控制容器的安全性和转发行为。具体如下所示:
基于上述组件图,管理员通过 Cilium CLI 配置策略信息,这些策略信息将存储在 KV 数据库里,Cilium 使用插件(如 CNI)与容器编排调度系统交互,来实现容器间的联网和容器分配 IP 地址分配,同时 Cilium 还可以获得容器的各种元数据和流量信息,提供监控 API。
关于Cilium Agent ,其作为守护进程运行在每个节点上,与容器运行时如 Docker,和容器编排系统交互如 Kubernetes。通常是基于插件的形式(如 Docker Plugin)或遵从容器编排标准定义的网络接口(如 CNI)。具体地,Cilium Agent 的主要功能包含以下:
1、基于 Clang/LLVM 将 BPF 程序编译为字节码,在容器的虚拟以太网设备中的所有数据包上执行,并将它们传递给 Linux 内核。
2、与容器管理平台的网络插件交互,实现 IPAM 的功能,用于给容器分配 IP 地址,该功能与 Flannel、Calico 网络插件类似。
3、收集容器的元数据,例如 Pod 的 Label,可用于 Cilium 安全策略里的 Endpoint 识别,这个跟 Kubernetes 中的 Service 里的 Endpoint 类似。
4、将其有关容器标识和地址的知识与已配置的安全性和可见性策略相结合,生成高效的 BPF 程序,用于控制容器的网络转发和安全行为。
5、其他相关自定义配置。
下面我们再来看下 Cilium 是如何基于 eBPF 实现容器网络方案的,首先,我们了解下其流程图,具体如下所示:
基于上述 Cilium eBPF 流程图,简要解析如下:
1、Cilium Agent 生成对应的 eBPF 程序。
2、用 LLVM 编译 eBPF 程序,生成 eBPF 对象文件(object file,*.o)。
3、用 eBPF loader 将对象文件加载到 Linux 内核。
4、校验器(verifier)对 eBPF 指令会进行合法性验证,以确保程序是安全的,例如 ,无非法内存访问、不会 Crash 内核、不会有无限循环等。
5、对象文件被即时编译(JIT)为能直接在底层平台(例如 x86)运行的Native Code。
6、如果要在内核和用户态之间共享状态,BPF 程序可以使用 BPF map,这种一种共享存储 ,BPF 侧和用户侧都可以访问。
7、BPF 程序就绪,等待事件触发其执行。对于此示例,在某一时刻,若存在有数据包到达网络设备时,便触发 BPF 程序的执行。
8、BPF 程序对收到的包进行处理,随后将返回一个裁决(verdict)结果。
9、根据裁决结果,如果是 DROP,这个包将被丢弃;如果是 PASS,包会被送到更网络栈的 更上层继续处理;如果是重定向,就发送给其他设备。
基于上述的流程,我们可以看出,基于 eBPF ,对数据平面(Datapth)进行了重新定义,具体如下,
首先,从长期看,eBPF 这项新功能会减少未来的 Feature Creeping Normality。因为用户或开发者希望内核实现的功能,以后不需要再通过改内核的方式来实现了。只需要一段 eBPF 代码,实时动态加载到内核就行了。
其次,基于 eBPF,内核也不会再引入那些影响Fast Path 的蹩脚甚至 Hardcode 代码 ,从而也避免了性能的下降。
再者,eBPF 还使得内核完全可编程,安全地可编程(Fully and Safely Programmable ),用户编写的 eBPF 程序不会导致内核 Crash。另外,eBPF 设计用来解决真实世界中的线上问题,而且我们现在仍然在坚守这个初衷。
站在网络角度,我们来看一下基于传统的 Kube-Proxy 处理 Kubernetes Service 时,包在内核中的转发路径是怎样的?简要流程如下图所示:
具体步骤如下:
1、网卡收到一个包(通过 DMA 放到 Ring-Buffer)。
2、包经过 XDP Hook 点。
3、内核给包分配内存,此时才有了大家熟悉的 Skb(包的内核结构体表示),然后 送到内核协议栈。
4、包经过 GRO 处理,对分片包进行重组。
5、包进入 TC(Traffic Control)的 Ingress Hook。接下来,所有标识为浅蓝色的框都是 Netfilter 处理点。
6、Netfilter:在 PREROUTING Hook 点处理 Raw Table 里的 Iptables 规则。
7、包经过内核的连接跟踪(Conntrack)模块。
8、Netfilter:在 PREROUTING Hook 点处理 Mangle Table 的 Iptables 规则。
9、Netfilter:在 PREROUTING Hook 点处理 Nat Table 的 Iptables 规则。
10、进行路由判断(FIB:Forwarding Information Base,路由条目的内核表示,译者注) 。接下来又是四个 Netfilter 处理点。
11、Netfilter:在 FORWARD Hook 点处理 Mangle Table 里的 Iptables 规则。
12、Netfilter:在 FORWARD Hook 点处理 Filter Ttable 里的 Iptables 规则。
13、Netfilter:在 POSTROUTING Hook 点处理 Mangle Table 里的 Iptables 规则。
14、Netfilter:在 POSTROUTING hook 点处理 Nat Table 里的 Iptables 规则。
15、包到达 TC Egress Hook 点,会进行出方向(Egress)的判断,例如判断这个包是到本 地设备,还是到主机外。
16、对大包进行分片。根据 Step 15 判断的结果,这个包接下来可能会:发送到一个本机 veth 设备,或者一个本机 Service Endpoint,或者,如果目的 IP 是主机外,就通过网卡发出去。
上述我们已经了解到传统的包转发路径,再来看下基于Cilium eBPF 中的包转发路径,简要流程如下图所示:
基于对比可以看出,Cilium eBPF Datapath 做了短路处理:从 TC ingress 直接 ShortCut 到 TC egress,节省了 9 个中间步骤(总共 17 个)。更重要的是:这个 Datapath 绕过了 整个 Netfilter 框架(浅蓝色的组件),Netfilter 在大流量情况下性能较差。
移除那些不必要的组件后,Cilium eBPF Datapath 流程示意图可精简为以下:
其实,基于其设计理念,Cilium eBPF 还能走的更远。例如,如果包的目的端是另一台主机上的 Service Endpoint,那你可以直接在 XDP 框中完成包的重定向(收包 1->2,在步骤 2 中对包 进行修改,再通过 2->1 发送出去),将其发送出去,如下图所示:
基于XDP技术, XDP 为 eXpress DataPath 的缩写,支持在网卡驱动中运行 eBPF 代码,而无需将包送 到复杂的协议栈进行处理,因此处理代价很小,速度极快。
最后,我们来了解下Cilium 的 Service Load Balancing 设计,简要架构如下图所示:
因时间原因,故针对其流量解析暂不在本章描述,后续再做补充,基于此,本篇文章关于Cilium eBPF 解析到此为止,大家有问题随时留意沟通。