在过去的几十年中,计算系统变得日益复杂。有关软件行为的推理已经发展出了多个业务类别,所有这些类别都试图解决对复杂系统的洞察力所带来的挑战。获得这种可见性的一种方法是分析由计算系统中运行的所有应用程序生成的日志数据。日志是一种非常重要信息来源。他们可以为您提供有关应用程序行为方式的精确数据。但是,它们使您感到困扰,因为您仅能够获得一些开发应用程序的工程师在这些日志中公开的信息。从任何系统中以日志格式收集任何额外信息都可能与反编译程序并查看执行流程一样具有挑战性。另一种流行的方法是使用度量标准来推理程序为何以某种方式运行。指标与日志的数据格式有所不同;日志为您提供显式数据,而指标则聚合数据以衡量程序在特定时间点的行为。
可观察性
是一种从不同角度解决此问题的新兴实践。人们将可观察性定义为我们提出任意问题并从任何给定系统接收复杂答案的能力。可观察性,日志和指标聚合之间的主要区别是您收集的数据。鉴于通过练习可观察性,您需要在任何时间回答任何问题,唯一的推理数据的方法是收集系统可以生成的所有数据,并仅在有必要回答问题时汇总这些数据。
纳西姆·尼古拉斯·塔勒布(Nassim Nicholas Taleb)
是《防碎裂:从混乱中获得好处的东西》(企鹅兰登书屋)等畅销书的作者,他将“黑天鹅”一词推广到意想不到的事件中,产生了重大影响,如果在事件发生之前已经观察到它们,这是我们想要的结果。在他的《黑天鹅》一书中,他合理化了通过相关数据如何有助于缓解这些罕见事件的风险。在软件工程中,黑天鹅事件比我们想象的要普遍得多,这是不可避免的。因为我们可以假设我们无法防止此类事件,所以我们唯一的选择是在不影响业务系统的前提下,获得关于它们的尽可能多的信息以应对这些事件。可观察性可帮助我们构建健壮的系统,并减少将来发生的“黑天鹅”事件,因为它基于您收集可回答任何未来问题的任何数据的前提。对黑天鹅事件和可观察性实践的研究集中于一个中心点,即从系统中收集的数据。
Linux容器是Linux内核上用于隔离和管理计算机进程的一组功能之上的抽象。传统上负责资源管理的内核还提供任务隔离和安全性。在Linux中,容器所基于的主要功能是namespace和cgroups。Namespace是将任务彼此隔离的组件。从某种意义上说,当您在某个namespace中时,您获得的是操作系统的体验,就像计算机上没有其他正在运行的任务一样。 Cgroup是提供资源管理的组件。从操作的角度来看,它们使您可以对任何资源使用情况(例如CPU,磁盘I / O,网络等)进行精细控制。在过去的十年中,随着Linux容器的普及,软件工程师设计大型分布式系统和计算平台的方式发生了变化。多租户计算已完全依赖于内核中的这些功能。
通过诸多Linux内核的底层功能,我们获得了设计可观察系统时需要考虑的新的复杂性和信息来源。内核是事件驱动的系统,这意味着所有工作都基于事件进行描述和执行。打开文件是一种事件,通过CPU执行任意指令是一个事件,接收网络数据包是一个事件,依此类推。伯克利包过滤器(BPF)是内核中的子系统,可以检查那些新的信息源。 BPF允许您编写内核触发任何事件时可以安全执行的程序。 BPF为您提供了强有力的安全保证,以防止您在这些程序中注入系统崩溃和恶意行为。 BPF正在启用一系列新工具,以帮助系统开发人员观察和使用这些新平台。
在这本书中,我们向您展示了BPF为您提供的强大功能,使任何计算系统都更加可观察。我们还将向您展示如何在多种编程语言的帮助下编写BPF程序。我们已将程序代码放在GitHub上,因此您无需复制和粘贴它。您可以在本书的Git存储库中找到它。
但是在我们开始专注于BPF的技术方面之前,让我们看一看一切是如何开始的。
1992年,Steven McCanne和Van Jacobson撰写了论文“ BSD数据包过滤器:一种用于面向用户的数据包捕获的新架构”。在该论文中,作者描述了他们如何为Unix内核实现了一种比同时期已有包过滤器快20倍的网络数据包过滤器,。数据包过滤器有一个特定的用途:提供使用来自内核的直接信息来监视系统的应用程序。利用这些信息,应用程序可以决定如何处理这些数据包。 BPF在数据包过滤方面引入了两项重大创新:
- 一种新的虚拟机(VM),旨在与基于寄存器的CPU有效配合使用。
- 每个应用程序缓冲区的使用可以过滤数据包而不复制所有数据包信息。这样可以最大程度地减少决策所需的数据。
这些巨大的改进使所有Unix系统都将BPF用作网络数据包过滤的首选技术,从而放弃了占用更多内存且性能较低的旧实现。该实现仍存在于该Unix内核的许多派生版本中,包括Linux内核。
2014年初,Alexei Starovoitov引入了扩展的BPF实现。此新设计针对现代硬件进行了优化,使其生成的指令集比旧的BPF解释器生成的机器代码更快。此扩展版本还将BPF VM中的寄存器数量从2个32位寄存器增加到10个64位寄存器。寄存器数量及其长度的增加为编写更复杂的程序提供了可能,因为开发人员可以使用函数参数自由地交换更多的信息。这些更改以及其他改进使扩展的BPF版本的速度比原始BPF实现快了四倍。
此新实现的最初目标是优化处理网络过滤器的内部BPF指令集。此时,BPF仍然限于内核态,并且用户态中只有少数程序可以编写BPF过滤器供内核处理,例如Tcpdump和Seccomp,我们将在后面的章节中进行讨论。时至今日,这些程序仍为旧的BPF解释器生成字节码,但内核将这些指令转换为极大改进的内部表示形式。
2014年6月,BPF的扩展版本扩展到用户态。这是BPF未来的拐点。正如Alexei在介绍这些更改的补丁中所写的那样,“此补丁集展示了eBPF的潜力。”
BPF成为顶级内核子系统,并且不再局限于网络栈。 BPF程序开始看起来更像内核模块,重点是安全性和稳定性。与内核模块不同,BPF程序不需要您重新编译内核,并且可以确保它们完成而不会崩溃。
我们将在下一章中讨论的BPF验证器添加了这些必需的安全保证。这样可以确保所有BPF程序都能正常运行而不会崩溃,并且可以确保程序不会尝试内存的越界访问。但是,这些优点带有一定的限制:程序在大小上具有最大允许的限制,并且需要对循环进行限制,以确保不良BPF程序不会耗尽系统的内存。
通过这些更改使BPF可从用户态访问,内核开发人员还添加了一个新的系统调用(syscall)-- bpf。这个新的系统调用将成为用户态和内核态之间的中间件。在本书的第2章和第3章中,我们讨论了如何使用此syscall与BPF程序和映射一起使用。
BPF映射将成为在内核和用户态之间交换数据的主要机制。第2章演示了如何使用这些专用结构从内核收集信息,以及如何将信息发送到内核中已在运行的BPF程序。
扩展的BPF版本是本书的起点。在过去的五年中,自引入此扩展版本以来,BPF有了显着的发展,我们详细介绍了受此发展影响的BPF程序,BPF映射和内核子系统的发展。
BPF内核中的架构令人着迷。在整本书中,我们将深入探讨其具体细节,但在本章中,我们希望为您提供有关其工作原理的快速概述。
如前所述,BPF是一种高级虚拟机,在隔离的环境中运行代码指令。从某种意义上讲,您可以像对Java虚拟机(JVM)那样思考BPF,Java虚拟机是运行从高级编程语言编译的机器代码的专用程序。诸如LLVM和GNU编译器集合(GCC)之类的编译器将在不久的将来为BPF提供支持,从而使您可以将C代码编译为BPF指令。编译代码后,BPF使用验证程序来确保程序可以安全地由内核运行。它可以防止您运行可能会使内核崩溃而危害系统的代码。如果您的代码安全,则BPF程序将被加载到内核中。 Linux内核还为BPF指令集成了即时(JIT)编译器。程序经过验证后,JIT将直接将BPF字节码转换为机器码,从而避免了执行时间的开销。该体系结构的一个有趣方面是,您无需重新启动系统即可加载BPF程序;您可以按需加载它们,也可以编写自己的初始化脚本,这些脚本在系统启动时加载BPF程序。
在内核运行任何BPF程序之前,它需要知道该程序附加到哪个执行点。内核中有多个入口点,并且列表在不断增加。执行点由BPF程序类型定义;我们将在下一章中讨论它们。当选择执行点时,内核还会提供特定的函数帮助器,可用于处理程序接收的数据,从而使执行点和BPF程序紧密耦合。
BPF体系结构的最后一个组件负责在内核和用户态之间共享数据。这个组件称为BPF映射,我们将在第3章中讨论映射。BPF映射是共享数据的双向结构。这意味着您可以从内核和用户态这两个方面进行写入和读取。有几种类型的结构,从简单的数组和哈希映射到专用的映射,可以将整个BPF程序保存在其中。
随着本书的进展,我们将更详细地介绍BPF体系结构中的每个组件。您还将学习如何利用BPF的可扩展性和数据共享,并通过具体示例覆盖从堆栈跟踪分析到网络过滤和运行时隔离的主题。
我们写这本书是为了帮助您熟悉日常工作中需要的基本BPF内容。 BPF仍然是一种发展中的技术,随着我们撰写本书,新的概念和范例也在不断发展。理想情况下,这本书将为您提供BPF基础组件的坚实基础,从而帮助您轻松地扩展知识。
下一章将直接介绍BPF程序的结构以及内核如何运行它们。它还涵盖了内核中可以附加这些程序的点。这将帮助您熟悉程序可以使用的所有数据以及如何使用它们。