eBPF 的发展演进 --- 从石器时代到成为神(二)
3. 发展溯源
回顾技术的发展过程,就像观看非洲大草原日出日落一样,宏大的过程让人感动,细节部分引人深思。每天循环不辍,却又每天不同。
BPF 的应用早已超越了它最初的设计,但如果要追溯 BPF 最初的来源,则必须回归到它最初的应用领域,再进行理解分析。
BPF 最初的用途在于观测,最初用于网络报文的抓取和分析。
因此 BPF 的最初、最根本的来源,是作为一种观测手段出现的。
而在这个领域中,技术的演进迭代,是一个很长的过程,体现了内核技术发展的艰辛、也同时充满了趣味。
如果把内核看作一个世界,在这个广袤的土地上,观测技术的发展,也同样经历了从蒙昧到现代的发展过程。
每个时代都有其独具特色的观测技术,它决定了当时的开发人员需要具备什么样的功底,什么样的开发方式,这构成了一个时代特色,也谱写了时代的故事。
而每次时代的更迭,总是在某些方面颠覆了或者突破了传统的思维,从而引发了观测方式的巨大进步,促进了效率和可观测性的提升。对现有技术的深入研究与颠覆性的思想所构成的创新,是技术领域演进的基本形式。而其创新的动力又是什么呢?我们在后文逐步揭示。
3.1. 石器时代
曾几何时,内核的开发还在初始阶段,由于内核的原理复杂、所处的位置特殊,开发方式和用户态有很大不同。内核开发难度远远大于用户态的应用开发,尤其调试比较困难。犹记得那时对于内核是否引入 GDB 调试机制,有过一些争论。其分歧点就在于,引入过于复杂的机制会改变内核的行为特性,影响问题的稳定性,反而不利于问题的分析定位。
那时最值得信赖的工具就是 printk 了。这是一种低介入的观测工具,使用简单,几乎可以用于任何地方,帮助开发人员观测内核的运行状态。但显著的缺点是不够灵活,如果问题涉及的逻辑路径比较长、分支比较复杂的话,需要反复多次才能定位问题的根源。因此,那时候对内核开发人员的一个必不可少的要求,就是对所负责子系统的实现原理和代码逻辑的熟悉程度需要非常高,能够根据比较少的观测信息,准确定位问题的根源。
事物总是存在两面性,就像当初产生的那场争论一样,printk 除了基本的信息输出机制外,几乎没有提供任何强有力的特性,这固然体现了当时的技术水平还在比较原始的阶段(没错,就像是石器时代),但同时也倒逼当时的内核开发人员超强的代码理解和分析能力。以便弥补简陋的工具对效率的掣肘,更快地解决程序中的 BUG。
另一方面,客观地讲,printk 固然简单,卓尔无往不利。它可以使用在任何地方,具有完全的上下文访问能力,不受约束的表达能力。
它的观测能力和程序本身完全相等,程序本身能看到什么它就能看到什么,可以说是强大到巅峰。这种强大也是其无法被取代的根本原因,尽管内核的调测技术不断在发展,这一点始终未被超越。
它可以用任何线性的文本形式,输出开发人员关注的上下文信息。在后来,这种表达能力得到了进一步发展,支持了部分正则文法。
它的缺点在于缺乏交互性,任何一点改变都需要修改程序。另一方面,不管上层流程是否被关注,它的信息都会被输出,大大影响了性能。
printk 可以说是最强大的工具,至今我也是这样认为。但它同时也是最粗糙的工具。就像石头一样,prink 随处可见,随处可用,用了就一定有所得。简单、强大、直接。但是同样像石头一样,如果用得多了,就会成为垃圾。
printk 相比于 BPF,拥有完全不受限制的上下文访问能力,使用的地方几乎没有限制,仅从观测的角度,强大之处有过之而无不及。但是使用方式过于原始,缺乏工业化的扩展能力,因此如果在更长的时间尺度、更广的应用领域来看的话,printk 无法和 BPF 相提并论。
3.2. 铁器时代
在石器时代,人们使用石头磨制的工具进行生产,这些工具粗糙、非标准化、材质原始容易损坏,笨重、使用寿命短。
Printk 也是一样,每次执行时都会输出信息,但大多数时候是不需要的;寿命短,每次改变需要修改代码。
随着内核越来越成熟,架构设计、模块划分、内部功能等等都越来越规范合理。内核的特性,由各个子系统分别负责,内核的整体表现是各个子系统行为表现的综合。而子系统内部的关键路径,决定了子系统主要的行为表现,比如:调度系统中的 CPU 时间统计、上下文切换,迁移等等;内存管理系统中的内存分配、NUMA 平衡;虚拟内存中的页面错误、交换次数等等。
随着内核设计的规范化,其内部的关键节点和呈现在外部的语义都越来越清晰和标准化。要掌握内核的运行状态,其实并不需要随处观察,只需要掌握几个关键节点、关键信息就可以了。
以关键变量为基础,工具得以升级;以语义规范化为基础,为交互式的观测机制提供了基础。至此,观测手段不再是单纯的信息输出,它也可以反过来影响系统行为实现多维度的观测。
虚拟文件系统 Proc 首先打通了用户态和内核态的交互通道,从原来只能控制日志级别,到可以控制数据本身,可以控制的范围更广、更深了;从文本交互,转换为二进制交互,内核性能受到的影响进一步降低。
提供了标准化的 API,类型的支持,降低了开发难度,便于推广使用。
提炼出关键参数,通过虚拟文件系统进行交互式的系统观测,反过来有利于内核的规范化。
3.3. 蒸汽时代
Proc 的定义很大一部分,还是与具体的上下文相关,并不适合大批量的使用。
Trace 定义了协议规范,抽象层次更高,可以批量使用。
Trace 是一个更加纯粹的观测机制,给用户提供了通用简单的接口,底层实现了很丰富的机制。可以支持大量使用,对于可观测性的提升起到了根本性的推动。可以批量重复使用,这是它和其他观测方式的区别。
如果说 Proc 采用了代码数据化的思想,那么 Trace 采用很多元编程的思想,极大简化了外部接口,减少了重复代码。
3.4. 电气时代
Trace 机制固然好用,只要预先铺设了基础设施,运行时就可以随时开启观测。但缺点是,对于没有铺设铁轨的地方,火车的承载能力再强也是无法到达的。
Trace 的机制很通用,但另一方面,它无法深入业务层面进行更进一步的调测。要实现这一点,需要完整的上下文能力和可编程能力,因此 kprobe 出现了。
只要由函数的地方,就像通了电一样,随时可以点亮,这是 Kprobe 强于 Trace 的覆盖能力。能够完整访问函数上下文,这是 Kprobe 强于 Trace 的业务理解能力。
3.5. 智能时代
Kprobe 是动态性的萌芽,但是很多方面不足。它在内核态运行,需要对内核编程有一定了解,编程门槛较高。它有安全性问题,它还有可扩展性问题,等等。
从计算能力来说,所有图灵机的计算能力是相等的,要解决能力问题,最终是要实现一个虚拟机的。而在内核态实现一个虚拟机,所涉及到的安全问题是必须考虑的,通过 Verifier 和运行时 Helper 函数,做到了逻辑约束和上下文隔离。虚拟机、Verifier 和 Helper 函数,是 BPF 和 Kprobe 的根本区别。
评论