下文是本人最近在Dockone社区做的一次关于BPF技术的学习分享。

背景介绍

近两年BPF技术跃然成为了一项热门技术,在刚刚结束的KubeCon 2020 Europe会议上有7个关于BPF的技术分享, 而在KubeCon 2020 China会议上也已有了3个关于BPF技术的中文分享,分别来自腾讯和PingCAP,涉足网络优化和系统追踪等领域。在中文社区里,包括阿里巴巴、网易、字节跳动等国内第一梯队IT公司也越来越关注BPF这项新技术。本文主要介绍BPF技术发展和应用,以及我是如何学习BPF技术的。

内容大纲

BPF是什么

首先,BPF是什么?需要回答BPF是什么,就得先回答为什么需要BPF。

多年前很多程序,例如网络监控器,都是作为用户级进程运行的。为了分析只在内核空间运行的数据,它们必须将这些数据从内核空间复制到用户空间的内存中去,并进行上下文切换。这与直接在内核空间分析这些数据相比,导致了巨大的性能开销。

而随着近年来网络速度和流量井喷式增长,一些应用程序必须处理大量的数据(如音频、视频流媒体数据)。要在用户空间监控分析那么多的流量数据已经不可行了,因而BPF应运而生——一种在内核空间执行高效安全的程序的机制。

BPF全称是「Berkeley Packet Filter」,翻译过来是「伯克利包过滤器」,顾名思义,它是在伯克利大学诞生的,1992年Steven McCanne 和 Van Jacobson 写了一篇《The BSD Packet Filter: A New Architecture for User-level Packet Capture》论文 ,第一次提出了BPF技术,在文中,作者描述了他们如何在 Unix 内核实现网络数据包过滤,这种新的技术比当时最先进的数据包过滤技术快 20 倍。

下图为BPF概览,来自上面的论文:

BPF 在数据包过滤上引入了两大革新:

  • 一个新的虚拟机 (VM) 设计,可以有效地工作在基于寄存器结构的 CPU 之上;
  • 应用程序使用缓存只复制与过滤数据包相关的数据,不会复制数据包的所有信息,最大程度地减少BPF 处理的数据,提高处理效率;

我们熟悉的tcpdump就是基于BPF技术,成为了站在神器肩膀上的「神器」。

发展到现在名称升级为eBPF:「extended Berkeley Packet Filter」。它演进成为了一套通用执行引擎,提供可基于系统或程序事件高效安全执行特定代码的通用能力,通用能力的使用者不再局限于内核开发者。其使用场景不再仅仅是网络分析,可以基于eBPF开发性能分析、系统追踪、网络优化等多种类型的工具和平台。

eBPF 由执行字节码指令、存储对象和辅助函数组成。下图是eBPF工作原理演示:

eBPF字节码指令在内核执行前必须通过 BPF 验证器的验证,同时在启用 BPF JIT 模式的内核中,会直接将字节码指令转成内核可执行的本地指令运行,具有很高的执行效率。

原来的 BPF 就被称为cBPF(classic BPF),目前已基本废弃。当前Linux 内核只运行 eBPF,内核会将cBPF 透明转换成 eBPF 再执行。下文提到的BPF字样没有特别说明的话,是泛指cBPF和eBPF。

BPF技术发展史

从1992年发布以来,BPF技术快速发展,除了技术本身得到了升级,基于它的工具和平台也如雨后春笋一般层出不穷,下面是我整理的BPF技术发展史,里面罗列几个重要的eBPF发展里程碑和基于eBPF技术的工具和平台的诞生事件,其中包括BCC、Cilium、Falco、bpftrace、kubectl-trace,还有最近非常热门的腾讯云独创的IPVS-BPF模式:

BPF的超能力是什么

那么对于Linux内核开发来说,BPF究竟有哪些超能力,吸引了这么多的开发者纷纷「入局」?

在了解BPF的超能之前,我们先来看下当前Linux内核由哪些困难。

传统的Linux内核开发需要实现以下3个目标:

  • 强安全,即不能允许不可信的代码运行在内核中,这是头等重要的事情
  • 高性能,作为承载千百万服务的操作系统内核,如果没有高性能的保障,互联网蓬勃发展将收到严重影响
  • 持续交付,在越来越多应用进入到云原生时代的今天,持续交付这个命题,一点都不陌生,而在内核开发领域,这点也至关重要,每次功能的升级,都需要你重新安装新的系统,大多数人都不会买账。我们希望做到跟Chrome浏览器升级一样,用户都不会注意到升级完成了(除非有一些视觉上的变化),实现真正的无缝升级。

然而想要实现上面的目标,没有想象中那么简单。

我们来看进行Linux内核开发的一般解决方案以及它们的缺陷。

  • 直接修改内核代码进行开发,通过API暴露能力,可能要等上n年用户才能更新到这个版本来使用,而且每次的功能更新都可能需要重新编译打包内核代码
  • 开发新的可即时加载的内核模块,用户可以在运行时加载到Linux内核中,从而实现扩展内核功能的目的。然而每次内核版本的官方更新,可能会引起内核API的变化,因此你编写的内核模块可能会随着每一个内核版本的发布而不可用,这样就必须得为每次的内核版本更新调整你的模块代码,你得非常小心,不然就会让内核直接崩溃。

来看BPF带来的解决方案,它是如何实现上面3个目标的:

  • 强安全:BPF验证器(verifier)会保证每个程序能够安全运行,它会去检查将要运行到内核空间的程序的每一行是否安全可靠,如果检查不通过,它将拒绝这个程序被加载到内核中去,从而保证内核本身不会崩溃,这是不同于开发内核模块的。比如以下几种情况是无法通过的BPF验证器的:
    • 没有实际加载BPF程序所需的权限
    • 访问任意内核空间的内存数据
    • 将任意内核空间的内存数据暴露给用户空间
    BPF验证机制很像Chrome浏览器对于Javascript脚本的沙盒机制。
  • 高性能:一旦通过了BPF验证器,那么它就会进入JIT编译阶段,利用Just-In-Time编译器,编译生成的是通用的字节码,它是完全可移植的,可以在x86和ARM等任意球CPU架构上加载这个字节码,这样我们能获得本地编译后的程序运行速度,而且是安全可靠的。
  • 持续交付:通过JIT编译后,就会把编译后的程序附加到内核中各种系统调用的钩子(hook)上,而且可以在不影响系统运行的情况下,实时在线地替换这些运行在Linux内核中的BPF程序。举个例子,拿一个处理网络数据包的应用程序来说,在每秒都要处理几十万个数据包的情况下,在一个数据包和下一个数据包之间,加载到网络系统调用hook上的BPF程序是可以自动替换的,可以预见到的结果是,上一个数据包是旧版本的程序在处理,而下一个数据包就会看到新版本的程序了,没有任何的中断。这就是无缝升级,从而实现持续交付的能力。

因此,大名鼎鼎的系统性能优化专家Brendan Gregg对于BPF的到来,就给出了以下的名言:

“Super powers have finally come to Linux” — Brendan Gregg

接下来我们来简单描述下BPF超能力的核心技能点。

BPF超能的核心技能点——BPF Hooks

第一个核心技能点是「BPF Hooks」,即BPF钩子,也就是在内核中,哪些地方可以加载BPF程序,在目前的Linux内核中已经有了近10种的钩子,如下所示:

  • kernel functions (kprobes)
  • userspace functions (uprobes)
  • system calls
  • fentry/fexit
  • Tracepoints
  • network devices (tc/xdp)
  • network routes
  • TCP congestion algorithms
  • sockets (data level)

从文件打开、创建TCP链接、Socket链接到发送系统消息等几乎所有的系统调用,加上用户空间的各种动态信息,都能加载BPF程序,可以说是无所不能。它们在内核中形成不同的BPF程序类型,在加载时会有类型判断。

下图的内核代码片段是用来判断BPF程序类型:

BPF超能的核心技能点——BPF Map

第二个核心技能点——「BPF Map」

一个程序通常复杂的逻辑都有一个必不可少的部分,那就是记录数据的状态。对于BPF程序来说,可以在哪里存储数据状态、统计信息和指标信息呢?这就是BPF Map的作用,BPF程序本身只有指令,不会包含实际数据及其状态。我们可以在BPF程序创建BPF Map,这个Map像其他编程语言具有的Map数据结构类似,也有很多类型,常用的就是Hash和Array类型,如下所示:

  • Hash tables, Arrays
  • LRU (Least Recently Used)
  • Ring Buffer
  • Stack Trace
  • LPM (Longest Prefix match)

下图所示是一个典型的BPF Map创建代码:

值得一提的是:

  • BPF Map是可以被用户空间访问并操作的
  • BPF Map是可以与BPF程序分离的,即当创建一个BPF Map的BPF程序运行结束后,该BPF Map还能存在,而不是随着程序一起消亡

基于上面两个特点,意味着我们可以利用BPF Map持久化数据,在不丢失重要数据的同时,更新BPF程序逻辑,实现在不同程序之间共享信息,在收集统计信息或指标等场景下,尤其有用。

BPF超功能的核心技能点——BPF Helper Function

第三个核心技能点——「BPF Helper Function」,即BPF辅助函数。

它们是面向开发者的,提供操作BPF程序和BPF Map的工具类函数。由于内核本身会有不定期更新迭代,如果直接调用内核模块,那天可能就不能用了,因此通过定义和维护BPF辅助函数,由BPF辅助函数来去面对后端的内核函数的变化,对开发者透明,形成稳定API接口。

例如,BPF程序不知道如何生成一个随机数,有一个BPF辅助函数会可以帮你检索并询问内核,完成”给我一个随机数”的任务,或者”从BPF Map中读取某个值”等等。任何一种与操作系统内核的交互都是通过BPF辅助函数来完成的,由于这些都是稳定的API,所以BPF程序可以跨内核版本进行移植。

下图是部分BPF辅助函数的列表:

BPF超能力的限制

介绍完这些BPF超能力的技能点,接下来要讲讲超能力也有限制的地方。

BPF技术虽然强大,但是为了保证内核的处理安全和及时响应,内核对于BPF 技术也给予了诸多限制,如下是几个重点限制:

  • eBPF 程序不能调用任意的内核参数,只限于内核模块中列出的 BPF Helper 函数,函数支持列表也随着内核的演进在不断增加
  • eBPF程序不允许包含无法到达的指令,防止加载无效代码,延迟程序的终止
  • eBPF 程序中循环次数限制且必须在有限时间内结束
  • eBPF 堆栈大小被限制在 MAXBPFSTACK,截止到内核 Linux 5.8 版本,被设置为 512。目前没有计划增加这个限制,解决方法是改用 BPF Map,它的大小是无限的。
  • eBPF 字节码大小最初被限制为 4096 条指令,截止到内核 Linux 5.8 版本, 当前已将放宽至 100 万指令( BPF_COMPLEXITY_LIMIT_INSNS),对于无权限的BPF程序,仍然保留4096条限制 ( BPF_MAXINSNS )

更多相关信息可以查看这里。随着技术的发展和演进,限制也在逐步放宽或者提供了对应的解决方案。

BPF应用场景

接下来我们通过3个案例分析,来看下具有强大超能力的BPF在实际环境中的应用场景:

Cilium

Cilium,是首款完全基于eBPF程序实现了kube-proxy的所有功能的K8S CNI网络插件,无需依赖iptables和IPVS。

我们知道kube-proxy基于iptables的服务负载均衡功能在大规模容器场景下具有严重的性能瓶颈,同时由于容器的创建和销毁非常频繁,基于IP做身份关联的故障排除和安全审计等也很难实现。

Cilium作为一款Kubernetes CNI插件,从一开始就是为大规模和高度动态的容器环境而设计,并且带来了API级别感知的网络安全管理功能,通过使用基于Linux内核特性的新技术——BPF,提供了基于service/pod/container作为标识,而非传统的IP地址,来定义和加强容器和Pod之间网络层、应用层的安全策略。因此,Cilium不仅将安全控制与寻址解耦来简化在高度动态环境中应用安全性策略,而且提供传统网络第3层、4层隔离功能,以及基于http层上隔离控制,来提供更强的安全性隔离。

另外,由于BPF可以动态地插入控制Linux系统的程序,实现了强大的安全可视化功能,而且这些变化是不需要更新应用代码或重启应用服务本身就可以生效,因为BPF是运行在系统内核中的。

以上这些特性,使Cilium能够在大规模容器环境中也具有高度可伸缩性、可视化以及安全性。

Cilium如何通过eBPF程序实现请求转发的原理分析:

Cilium通过将eBPF程序加载到内核的几个网络Hook上,包括TC,XDP,实现了原来Kube-Proxy请求转发的能力。在内核网络栈中,这两个网络hook都要比kube-proxy所依赖的iptables处于更早的网络前端,还没有生成完全的网络报文的上下文(包含更复杂的元数据结构),因此具有更高的网络数据处理效率,如下图所示:

配合eBPF Map存储后端Pod地址和端口,实现高效查询和更新,如下图所示:

更多Cilium部署使用内容可以看我的这篇博文

Falco

Falco,来自老牌安全厂商Sysdig开源的关注云原生运行时安全的项目。

当前问题:目前云原生时代Kubernetes技术解决了基础架构平台Day 1 Operation问题,而Day 2 Operation包含了monitor,maintain,和 troubleshoot等一系列运行时工作,其中「云原生安全问题」已经引起越来越多的注意。

Falco解决方案:Falco是第一个加入CNCF的关注云原生运行时安全的开源项目,目前是威胁Kubernetes平台监测引擎的事实标准,还可以监测意外的应用行为和运行时发出的威胁警告。

Falco如何使用eBPF程序实现实时应用监控:

Falco主要使用了raw_tracepoint的系统调用hook,检测应用进程的启动和退出,然后通过Perf类型的BPF Map将检测到的数据发送回用户空间,实现监控的pipeline。官方Falco已自带了很多默认监控规则,具体可以查看Falco pod中的/etc/falco/falco_rules.yaml文件。

下面是几个Falco默认监控规则样例:

最近两年KubeCon大会上都有Falco的精彩talk,都是来自sysdig的大神,介绍了Falco的运作原理和使用场景,是了解掌握Falco非常好的材料,其中Kris Nova是个非常cool的技术女神,她的PPT也非常有个性,是基于命令行的。

更多Falco部署使用内容可以看我的这篇博文

Kubectl-trace

Kubectl-trace,一款基于bpftrace的kubectl插件,帮助用户追踪排查K8S应用的运行情况。

首先介绍下bpftrace,它是一种高阶的描述系统追踪的语言,其优势在于可以通过一行脚本(One-Liner)实现多种应用追踪能力,如下图所示:

通过和BCC集成,能实现更强追踪能力。如果你想要的脚本比较复杂,可以放在一个扩展名为bt的文件里,如下图所示的bashreadline.bt:

回到我们的kubectl-trace,它的能力就是可以把定义好的bpftrace脚本attach到指定的节点、pod上,来追踪脚本里定义的目标。具体来说,kubectl-trace插件在K8S集群内是通过名为trace-runner的job形式指定执行一个bpftrace程序,如下图所示:

而在这个trace-runner job的pod中,它的工作原理就是一个原生bpf程序原生的工作流程了,通过加载bpf程序(即bpftrace生成的)到kprobes,uprobes,tracepoints等系统调用hook上,并把程序的输出重定向到磁盘上,以便进行事后查询,整体工作流程如下图所示:

BPF社区和生态

BPF社区网站

各种社区网站,是学习BPF的好去处,下面是推荐的社区网站:

学习技术还是得从源代码开始,下面是与bpf相关的代码仓库:

学习技术也需要沟通交流,下面是推荐的沟通渠道:

BPF社区的大神们

  • Brendan Gregg,来自Netflix最强BPF布道师,他的博客都是关于Linux系统优化的,观点独到,每一篇都值得一读;
  • Alexei Starovoitov,eBPF创造者,目前在Facebook就职,经常能在内核代码commit中看到他的踪迹;
  • Daniel Borkmann,eBPF kernel co-maintainer,目前在Cilium所在的公司Isovalent就职,是给eBPF增加feature的能力者;
  • Thomas Graf,Cilium之父,Isovalent的CTO,他也是eBPF和Cilium的强力布道师,能说会道,各种大会上都有他的风采;
  • Quentin Monnet,BPFTool co-maintainer,Quentin是在stackoverflow上bpf问题的killer,twitter有关于eBPF的系列实战短文,值得细品;

BPF书籍

BPF技术分享

建议大家可以跟踪各种大会上的eBPF技术分享:

  1. 最开始提到的最近两年的Kubecon上的eBPF相关Session
    下面是最新的几个session详细链接:
  2. LPC 2020 Networking and BPF Summit,这个会刚结束不久,可以说是BPF技术的专题会,上面有非常多的eBPF实践案例以及未来可能增加的功能列表,比如BPF Map是否能resize,而不是一上来就定义好大小。

使用BPF技术的项目

罗列下使用BPF技术的项目:

  • Tcpdump
  • BCC, BPFTrace, kubectl-trace from IOVisor
  • Cilium from Isovalent
  • Falco from Sysdig
  • Katran from Facebook
  • Bottlerocket from Amazon

中文社区有:

  • 腾讯云IPVS-BPF K8S网络优化方案
  • Kernel Chaos With BPF by PingCAP
  • 网易轻舟做系统检测和网络优化
  • 字节跳动做高性能网络ACL管理

更多IT公司都在尝试使用BPF技术到各个领域中。

我是如何学习BPF技术

最后,讲一下我是如何学习BPF技术的。

先来说说我和BPF的缘起。我第一次接触BPF技术是在今年年初,看到了介绍Cilium这款k8s网络插件的视频,它能通过BPF程序实现了所有Kube-proxy功能,而且把请求转发效率提升了近50%,觉得非常不可思议,然后就决定深入了解下BPF这项超能力技术。

整个学习过程,总的来说我是先看了很多英文文章,因为当时中文社区关于BPF技术的文章非常少,然后设计编写自己的BPF程序,希望把它跑起来并获得预期的结果,期间会有很多问题发生,就会不停地去调试代码,最终通过写博客,把每次有目的的学习过程和成果,记录总结并分享出来。

其中要给大家重点说的就是学习之初要做计划。

做计划对大多数人来说,是获得成功结果的必要条件,而做计划的过程是我认为就是全盘思考的过程。当然计划不是一成不变的,以你自己的学习节奏和输出效率,可以在过程中按需进行适当调整。下图是我刚开始制定的BPF学习计划:

第一阶段主要是学习和翻译经典英文材料,跟“多读书,读好书”道理一样,翻译材料也是这样,首选要去优秀的社区,然后去选优质的材料,通过逐句逐句地翻译,就能理解这项技术的方方面面。这个过程有点像读文档翻译文档,但是比看文档有趣多了,因为这种优质的材料里一般都会有精彩的故事,让你知道技术的诞生和发展其背后真正的驱动力。

BPF的优秀社区是哪家?我选择了上面提到的LWN.net社区,最早全称为Linux Weekly News。现在内容已经不单单是Linux了,涉足广泛。很多资深IT圈人士都会给LWN供稿,内容详实有深度,值得反复阅读。

而在选取材料时,由于不同阶段的阅读目标不同,初级阶段建议选择没有太多技术细节的,能讲明白BPF技术是什么?解决什么问题?以及发展历程,就可以作为候选。

完成了第一阶段,就会对这项技术的背景和术语有了初步的认识,接下来就是选取一个兴趣点作为切入口,深入了解这项技术的使用场景和工作原理,也就是第二阶段——动手实践。

我自己对使用BPF进行网络优化这个领域很感兴趣,因此它变成了我动手学习的切入口。下面是我的第一个网络层面的XDP BPF程序:

目的非常简单,就是所有经过XDP钩子的网络数据包,全部drop掉,也就是形成了“丢包”的结果,这就是业界最出名的一个XDP BPF应用场景的雏形——Facebook基于XDP实现高效的防DDoS攻击,其本质上就是实现尽可能早地实现「丢包」,而不去消耗系统资源创建完整的网络栈链路,即「early drop」。

像上面这样,通过准备动手实验环境,写代码,调试代码,这一步步的过程中一定会出现预料之外的问题,不用怕,要有耐心去啃,虽然耗时会可能会非常长,但要有不达目的不罢休的毅力。

我的自己的一个经历,就是在调试TC BPF程序时,发现结果总是不符合预期,然后就不停地调试代码,同时也对用来调试的BPF辅助函数bpf_trace_printk()进行了深入地研究(链接是我当初整理的关于这个函数的思维导图),从而了解到了BPF底层实现,包括它是由11个64位寄存器、1个计数器和1个512字节BPF stack组成。寄存器命名规则是r0-r10,每个寄存器都有专属的作用:

  • r0保存了调用一次辅助函数后的返回值
  • r1 – r5 保存了从BPF程序到辅助函数的参数列表
  • r6 – r9 是用来保存中间值的寄存器,它可以被多个辅助函数调用
  • r10 是唯一的只读寄存器,包含访问BPF stack的指针

上面这些都是你不罢休的成果,要相信由付出必有回报。

第三阶段,就是我现在正在做的事情,总结和分享。成功的实践一定会让你有输出的欲望,我选择博客作为我的输出渠道,当然大家也可以去选择其他的渠道。对我而言,通过文字和图片把一件事情娓娓道道是个很舒服的方式。而把这些内容再分享出去,让更多的人了解这项技术并获得收获,会令你自己感到快乐,这就是分享的意义。

再分享一点关于如何阅读理解代码的TIPS:

最后分享下学习BPF给我带来的收获:

  1. 能够静下心来看Linux内核代码,这件事听起来简单,做起来不易,因为有了学习兴趣有学习目标,我开始习惯于阅读那些看起来冗长晦涩的代码
  2. 理解Linux系统调用、文件系统等功能模块的工作原理,正式由于能静下心来读代码,所以那些原本认为这辈子都看不懂的东西,竟然慢慢变得清晰起来
  3. 写文章可以锻炼很多其他软技能,比如画图,录视频,做视频等等,写技术博客就是这么一件痛并快乐着的事情

谢谢大家,我的分享到此结束,本次分享属于抛砖引玉,如果对BPF技术感兴趣想深入了解的,可以关注我博客的BPF系列文章:https://davidlovezoe.club/wordpress/archives/tag/bpf 以及github上配套的BPF源码repo:https://github.com/nevermosby/linux-bpf-learning/

PPT文档下载链接: https://pan.baidu.com/s/1p32lDqGiu4m8L6ShCkP_6A 提取码: kiga

9 对 “Linux超能力BPF技术介绍及学习分享(附PPT)”的想法;

  1. 您好!我最近也在研究xdp,并尝试使用xdp在两个docker容器之间直接做流量转发,xdp挂载在主机侧的两个veth网卡上,当收到来自容器的数据包就修改目的MAC地址为对端容器MAC地址,然后使用bpf_redirect重定向流量,之后两个容器相互之间可以ping通,但是想建立tcp连接的时候总会出现tcp重传错误。您知道这是什么原因吗?
    这个是我在xdp-tutorial上就这个问题提的issue, 上面的实验步骤比较详细,不知道您能否抽空看一下,不胜感激https://github.com/xdp-project/xdp-tutorial/issues/160

    1. hello,我那周末从头实验了xdp-project repo里的示例,但是还没来得及做到你遇到问题的那个,不好意思。我看到issues里面已经解决问题了,特别棒,要不要写篇blog分享出来。

        1. 我自己倒是没有开群(倒是可以计划下),加入了一个朋友开的群叫《KS2 & BPF & 云原生网络学习群🚢》,如果你想进群,可以发我邮件你的微信联系方式。

  2. 利用Just-In-Time编译器,编译生成的是通用的字节码,它是完全可移植的,可以在x86和ARM等任意球CPU架构上加载这个字节码。
    是不是多了”球”字。

  3. 大佬你好,我想在内核网络IO路径上对指定的数据结构(msghdr/iov_iter/iovec)做可靠性加固(假设是极端环境、软硬件不可靠的场景)。比如说在内核访问这些数据结构前需要验证下这些数据结构的正确性,是否可以用ebpf程序对这些数据结构做监测,一旦内核访问这些数据结构,就跳转到我的ebpf程序执行?望百忙中抽空解答,不胜感激~

发表评论

邮箱地址不会被公开。 必填项已用*标注