Linux内核性能分析工具BCC

708 阅读14分钟

介绍

这是由 3 部分组成的系列中的第 1 部分,其目标是简要介绍 BCC 以及如何开始使用它来tracing内核和用户空间应用程序。本页是假设读者不熟悉 Linux tracing而编写的。因此,作为一般性介绍,我们将首先简要介绍一般trace术语,然后深入探讨 BCC。如果您已经熟悉tracing术语并且只想跳到 BCC,请随时跳到该部分。

在第一部分中,我将简要介绍tracing、BPF 和 BCC。本系列的第 2 部分将深入研究两个 BCC 脚本示例。最后,第 3 部分 将深入探讨 QEMU 和内核中更高级的 BCC 脚本示例。

Linux tracing

在开始使用 BCC 之前,至少熟悉当今使用的常见tracing术语非常重要。观察和追踪事物的方法有很多种;了解您想要观察的范围以及trace前端的功能(包括 BCC!)将为您节省大量时间并减少挫败感。

以下是用于对分析技术和工具进行分类的术语:

  • tracing——基于事件的记录
    • BCC 使用这种类型的仪器
    • _与窥探_或_事件转储_同义
    • 可以对tracing事件运行小程序以进行即时处理
  • 采样- 采用测量的子集来创建目标的粗略轮廓
    • Perf 使用这种类型的仪器
    • 与_分析同义_
    • 与tracing相比,性能开销更低
  • 可观察性- 通过观察了解系统,并对实现此目的的工具进行分类
    • 取决于了解目标系统或应用程序所需的内容
    • 可以使用tracing和/或采样来完成此任务
  • 可见性——观察或追踪某事物的能力
    • 即 GDB 提供代码执行的可见性
    • 动态tracing对内联函数不可见
    • 静态tracing对内联函数具有可见性
  • 事件- 软件中导致探测器或tracing点“触发”(发送信息)的操作
    • 松散定义;事件可以是简单地命中函数、函数中的特殊情况、函数的结束、例程的开始等。
    • 用户本质上是根据他们启用的探针/tracing点以及事件触发的时间/地点来定义事件

### Tracing术语:

image.png

该图本质上是 Linux tracing的大图。值得注意的是,这并不是所有内容的完整列表,并且并非每个前端都使用每个数据提取器或数据源。

本页并未详细介绍上图中的所有内容。我们将重点关注数据源,更具体地说是 kprobes/kretprobes、uprobes/uretprobes、内核tracing点和 USDT 探针。了解不同的数据源及其功能是了解trace前端功能的关键。例如,sysdig 不使用列出的任何数据源,并且仅对系统调用具有可见性。

值得一提的是 BCC 与上图的关系。如前所述,BCC 是一个tracing前端。trace前端是一个提供trace技术和功能(有时还提供采样,如 perf)的应用程序。

trace前端分为两类:低级前端高级前端。高级前端通常是一个更加用户友好的tracing工具套件,您可以使用它来快速观察目标。一些高级trace前端构建在低级前端之上,例如 bpftrace 构建在 BCC 之上。低级trace前端通常比较简单,用户友好性较差,需要开发人员做更多的工作才能利用它们。低级前端的好处是,您可以获得更多的灵活性和对事件、数据处理和显示的控制(尽管 DTrace 和 bpftrace 也可以提供一些灵活性和控制)。

BCC 被认为是低级前端,因为我们必须编写自己的脚本才能使用它!换句话说,没有像 DTrace 和 bpftrace 那样的命令可以调用,我们将调用我们自己的(通常是 Python)脚本。

最后一点:如果您不熟悉,BCC 代表_BPF Compiler Collection_。上图中的BPF数据提取器就是这个BPF。BPF(或 eBPF)提供内核(kprobes/kretprobes、内核tracing点)和用户空间(uprobes/uretprobes、USDT 探针)的可见性,使 BCC 成为具有竞争力且灵活的前端。

动态和静态tracing:

我们要查看的数据源是 kprobes/kretprobes、内核tracing点、uprobes/uretprobes 和 USDT 探针。它们非常强大,通常按动态和静态trace功能进行分类。kprobes/kretprobes 和 uprobes/uretprobes 是动态检测的一种形式,而内核trace点和 USDT 探针是静态检测的一种形式。

动态tracing- 将检测点插入到实时软件中(例如在生产中)

  • 也称为_动态仪器_
  • kprobes/kretprobes:内核级函数的动态检测
    • 当执行到达被trace的目标内核函数的_开头时,_ kprobes会触发
    • 当执行从被trace的目标内核函数_返回时,_ kretprobes会触发
  • uprobes/uretprobes:用户空间级 C 函数的动态检测
    • 当执行到达目标用户空间 C 函数的_开头时,_ uprobes会触发
    • 当执行从目标用户空间 C 函数_返回时,_ uretprobes触发
  • 优点:不使用时零开销,无需重新编译或重建
  • 缺点:函数可以从一个软件版本重命名或删除到下一个软件版本(_界面稳定性问题),___联函数不可见。

静态tracing- 插入在软件中静态定义的稳定trace点

  • 也称为_静态仪器_
  • 由开发者定义和实现
  • 内核tracing点:内核级代码的静态检测
    • 当前为 Linux 机器定义的内核trace点可以在_/sys/kernel/debug/tracing/events/*中找到_
    • _在include/trace/events_中定义新的内核trace点
  • USDT Tracepoints:用户空间级 C 代码的静态检测
    • 也称为_USDT 探针、用户标记、用户空间trace点_等。
    • _可以使用systemtap-sdt-dev_包中的标头和工具或自定义标头添加到应用程序
  • 优点:可以放置在代码中的任何位置(包括_内联_函数)、变量(包括结构)的高可见性、更稳定的 API
  • 缺点:许多定义的trace点可能成为维护负担,需要重新编译/重建以添加新的trace点,禁用trace点的非零开销(trace点处的_nop_指令)

Linux trace:总结

trace有许多前端能够与之交互的多种技术,其中最强大的技术是_kprobes、uprobes、内核trace点_和_USDT 探针/trace点_。在大多数情况下,拥有一个同时使用静态和动态trace的前端将是获得跨系统可观察性的最佳选择。

请记住,kprobes 和 uprobes 是动态插入的,这意味着它们已经存在于实时生产中,trace器只需要挂接并启用它们即可激活它们。动态trace的好处是禁用探针的开销为零,而且它们已经在内核和用户空间中。动态trace的缺点是任何使用它们的trace工具都可能会破坏未来的软件版本。这是因为 kprobes 和 uprobes 附加的函数(例如函数的名称及其参数和返回值)可能会随着时间的推移而改变,从而改变探针的范围。

另一方面,内核trace点和 USDT trace点是静态插入的,这意味着它们需要由开发人员定义和实现。静态trace的好处之一是它具有更稳定的 API,因为开发人员可以将名称和参数定义为他们想要的任何内容。除此之外,另一个很大的好处是您可以trace目标函数中的任何位置。也就是说,虽然您只能在动态trace中trace参数和返回值,但静态trace允许您tracetrace点所在函数中可见的任何变量(包括结构)。因此,其缺点之一是维护。随着定义的trace点越多,随着软件版本的更改,这些trace点之一不再兼容的风险就越大。

一般建议,最好先使用静态trace,然后在静态trace不可用时切换到动态trace。

BCC简介

image.png

BCC (BPF Compiler Collection)是第一个为****BPF(Berkeley Packet Filter)开发的高级trace框架。BPF 是 1992 年开发的一项默默无闻的技术,旨在提高网络数据包过滤的性能。2013年,BPF被重写并进一步发展为eBPF(扩展BPF),并于2014年纳入Linux内核。这次BPF的重写将其变成了通用执行引擎,极大地增加了BPF技术的潜力。

:1992 年开发的 BPF 技术如今被称为Classic BPF(不再进一步开发)。eBPF现在是主流的BPF技术,今天简称为BPF

这项新技术提供了在各种内核和用户空间应用程序事件(例如磁盘 I/O 或 QEMU)上运行小程序的方法。从本质上讲,它使内核完全可编程,使内核和非内核开发人员能够以他们认为合适的任何方式定制和控制他们的系统(BCC 使这变得非常简单)。

BPF 由于其虚拟指令集规范而被视为虚拟机(_不应_解释为处理器之上的另一个机器层)。这些指令由 Linux 内核 BPF 运行时执行,其中包括解释器和 JIT 编译器,用于将 BPF 指令转换为本机指令来执行。

注意:在 Spectre 漏洞之后,一些 Linux 发行版无条件启用 x86 的 JIT,这会完全删除解释器(被编译出来)。

BPF 还使用验证器来检查 BPF 指令的安全性,确保 BPF 程序不会崩溃或损坏内核。然而,它并不完美,因为它并不能阻止用户编写不合逻辑(但可执行)的程序。例如,隐式除以零错误不会被验证者捕获。然而,未绑定的 for 循环将被验证者捕获。

image.png

有了BPF为什么还要BCC?

如果BCC背后真正的推手真的是BPF,那么BCC到底能为我们做什么呢?好吧,在 BCC 之前,BPF 的唯一前端(回想一下 Linux 大图中的 BPF 数据提取器)是原始 BPF 字节代码、C 和 perf。虽然这些工具/前端的原始潜力非常高,但要充分利用 BPF,即使不是不切实际,也是非常困难的。换句话说,对于大多数人来说,学习曲线太陡峭,因此将选择其他trace技术(为了简单起见,交易能力)。

BCC 保留了 BPF 的原始潜力,也使普通开发人员更容易使用它。它提供了一个 C 编程环境,用于编写内核 BPF C 代码(也称为嵌入式 C)和用于用户空间接口的 Python、Lua 或 C++。换句话说,您可以使用嵌入式 C 从trace事件中提取数据,并使用 Python 脚本、Lua 或 C++ 程序以您想要的方式进一步处理和显示数据。开始使用 BCC 制作自己的 BPF 程序的学习曲线大大缩短(而且相当简单),尤其是现在,因为它已经成熟了很多年,有更多的文档等。

除了使 BPF 的潜力更加明显之外,BCC 存储库还包含 70 多个现成的 BPF 程序,用于性能分析和调试。正如您在下图中看到的,整个堆栈中的几乎所有内容都有一个脚本!这些都是开箱即用的,并且每个都有完整的文档和示例用法。还有大量资源甚至指南可以帮助您开始开发自己的 BPF 程序。

谁会对 BCC 感兴趣?

image.png 公平地说,编写一个 C 程序来提取数据并编写一个 Python 或 C++ 程序来处理和显示数据并不是trace中最快的事情,尤其是当您可以在不同的前端上使用单行代码来执行数据trace时。得到相同的结果(尽管 BCC 确实有 BPF 工具_trace.py_提供了“one-liner”能力)。

在我看来,以下可能是有人认为 BCC 比其他trace前端更有用的一些原因:

  • 您想要类似 perf 的东西,但需要更多地控制收集和显示的数据
  • 您想要收集自定义统计数据、监视特定事件等。
  • 您想为系统/子系统创建自定义分析工具
  • 您希望完全控制tracing事件、数据处理和输出显示
  • 你需要使用 BPF(无论出于何种原因)
  • 一个预制的 BPF 工具可以做一些接近你想要的事情(并且你想要编辑它)

当然,这并不是人们使用密件抄送的全部原因,但它涵盖了大多数情况。从本质上讲,当您需要更深入地了解系统中发生的情况并且希望完全控制正在收集的数据时,BCC 非常有用。

BCC特点:

下面的列表是 BCC 为我们提供的功能的完整列表,包括内核级功能、用户级功能和一些突出显示的预制 BPF 工具。请注意,本页未涵盖下面列出的大多数功能。由于这是介绍,因此我们将重点介绍您在开始使用时首先需要了解的基础知识。一旦您熟悉了基础知识,您就可以开始探索 BCC 提供的其他功能。

内核级特性

BCC 可以使用许多内核级功能,例如 BPF、kprobes、uprobes 等。以下列表包括一些实现细节(括号内):

  • 动态trace,内核级(BPF 支持 kprobes)
  • 动态trace,用户级(BPF 支持 uprobes)
  • 静态trace,内核级(BPF 支持内核trace点)
  • 定时采样事件(带_perf_event_open()_的 BPF )
  • PMC 事件(带_perf_event_open()_的 BPF )
  • 过滤(通过 BPF 程序)
  • 调试输出(bpf_trace_printk()
  • 每个事件输出 ( bpf_perf_event_output() )
  • 基本变量(全局变量和每线程变量,通过 BPF 映射)
  • 关联数组(通过 BPF 映射)
  • 频率计数(通过 BPF 图)
  • 直方图(二的幂、线性和自定义;通过 BPF 映射)
  • 时间戳和时间增量(_bpf_ktime_get_ns()_和 BPF 程序)
  • 堆栈trace - 内核(BPF 堆栈图)
  • 堆栈trace - 用户空间(BPF 堆栈图)
  • 覆盖环形缓冲区(perf_event_attr.write_backward
  • 低开销仪表(BPF JIT、BPF 映射摘要)
  • 生产安全(BPF验证器)

有关这些内核级功能的更多信息和背景,请参阅Brendan Gregg 所著的 BPF 书中的第 2 章:技术背景。

用户级功能

BCC 还提供了来自 BCC 用户级前端的许多用户级功能。实施细节(括号内):

  • 静态trace,用户级(通过.uprobes 进行 SystemTap 风格的 USDT 探测(例如_DTRACE_PROBEn()_))
  • 调试输出(Python 与_BPF.trace_pipe()BPF.trace_fields()_)
  • 每个事件输出(BPF_PERF_OUTPUT宏和BPF.open_perf_buffer()
  • 间隔输出(BPF.get_table()table.clear()
  • C 结构体导航,内核级(BCC 重写器映射到_bpf_probe_read()_)
  • 符号解析,内核级(kysm()kysmaddr()
  • 符号解析,用户级 ( usymaddr() )
  • Debuginfo 符号解析支持
  • BPF trace点支持(通过_TRACEPOINT_PROBE_)
  • BPF 堆栈trace支持 ( BPF_STACK_TRACE )

有关这些用户级功能的更多信息和背景,请再次参阅Brendan Gregg 所著的 BPF 书中的第 2 章:技术背景。

其他功能:教程、预制工具、参考指南

除了 BCC 必须提供的所有技术功能之外,它还附带大量示例、教程和预制工具。这些都非常有用,如果您还没有的话,您肯定会想检查一下。

BCC 存储库中包含两个教程:一个使用 BCC 工具解决性能、故障排除和网络问题的教程,以及一个 BCC BPF Python 开发人员教程,帮助您开始使用 C 和 Python 制作自己的 BPF 程序。这就是我们在下一节中创建一些示例时将要做的事情,只不过这些示例是我自己的,旨在_补充_BCC 的 Python 开发人员指南。也就是说,一旦您浏览完我的示例,您还需要浏览 BCC 开发人员指南的示例。

  • docs/tutorial.md:使用 BCC 工具解决性能、故障排除和网络问题
  • docs/tutorial_bcc_python_developer.md:使用 Python 接口开发新的 BCC BPF 程序

注: _examples/*_中的BPF程序对应于BCC Python开发者教程;这些仅供参考

BCC 附带的预制 BPF 工具可以在_tools/*中找到,而且有很多!每个工具都带有自己的***.txt**_文件,其作用类似于该工具的联机帮助页。其中一些工具包括:

  • tools/biolatency.py/txt:将块设备 I/O 延迟总结为直方图
  • tools/cpuunclaimed.py/txt:采样 CPU 运行队列并计算无人认领的空闲 CPU
  • tools/hardirqs.py/txt:测量硬IRQ(硬中断)事件时间
  • tools/klockstat.py/txt:trace内核互斥锁定事件并显示锁定统计信息
  • ETC。 ...