Poireau:一个抽样分配调试器

Skip to content

backtrace-labs / **poireau ** Public

Poireau:一个抽样分配调试器

License

MIT license 91 stars 5 forks

Poireau:一个抽样分配调试器

libpoireau 库拦截对 malloc/calloc 等函数的一小部分调用,从而生成应用程序堆占用空间的统计代表性概览。虽然拦截器目前只跟踪长期存在的分配(例如,内存泄漏),但我们计划实现防护页,就像 Electric Fence 一样。

抽样方法使得在生产环境中使用此库成为可能,对性能的影响极小(请参阅“性能开销”部分),并且无需更改代码生成,这与例如 LeakSanitizerValgrind 不同。

该库的实现策略将大部分复杂性卸载到内核或外部分析脚本,并且仅覆盖系统内存分配器(或任何已经覆盖系统 malloc 的其他分配器)用于少数采样的分配,这意味着该工具不太可能从根本上改变程序的行为。 预加载 libpoireau.so 的侵入性远小于插入例如 tcmalloc,仅仅是因为人们想要调试分配。 代码库也小得多,并且在生产环境中删除新库之前更容易审核。

最后,poireau.py 分析脚本没有扫描堆中的引用,而只是报告旧的分配。 对于应用程序服务器以及希望在启动后快速进入稳定状态的其他工作负载,这比仅报告无法访问的对象更有用:堆占用空间的缓慢增长是一个问题,即使罪魁祸首是可访问的,例如,在没有应该被清除的列表里。

如何构建 libpoireau

libpoireau 目前针对 Linux 4.8+(用于静态定义的跟踪点支持)在具有 4 KB 页面的 64 位平台上。 执行 make.sh 以在当前目录中创建 libpoireau.so;该代码需要与 GCC 兼容的 C11 实现。

如何使用 libpoireau

在执行要调试的程序之前,将 LD_PRELOAD="$LD_PRELOAD:$path_to_libpoireau.so" 添加到环境中。

在使用 libpoireau 之前,我们必须使用 Linux perf 注册其静态探测点;这可以在使用 LD_PRELOAD 启动程序之前完成,也可以在启动程序之后完成,这无关紧要。

sudo perf buildid-cache --add ./libpoireau.so
sudo perf list | grep poireau # should show tracepoints

我们现在可以启用跟踪点,以便在 libpoireau 覆盖 stdlib 调用时生成 perf 事件。

sudo perf probe sdt_libpoireau:*

这足以让 Linux perf 报告这些事件,例如,在 perf top 中。 但是,这会产生大量信息,不一定有用。

执行 scripts/poireau.sh $PID 以在该 PID 上启动 perf trace,并将输出馈送到分配跟踪脚本。 该脚本每 10 分钟会转储一个当前存在的旧(> 5 分钟)采样分配的列表。 向 poireau.py 发送 HUP 信号以获取所有活动采样分配的列表。 旧的分配最终会充满已知的泄漏或启动分配;通过向 poireau.py 发送 USR1 信号,从未来的报告中删除所有当前旧的分配。

进程外分析的一个关键优势是,我们仍然可以在崩溃后提供信息。 向 poireau.py 发送 USR2 信号以列出最近对 freerealloc 的一些调用,如果这有助于调试 use-after-free,那就太好了。

Perf 通常需要 sudo 访问权限,但是以 root 身份运行所有 poireau.py 毫无意义。 poireau.sh 仅使用 sudo 执行 perf。 为了覆盖 sudo 下的 perf 二进制文件,请使用 PERF=which perf scripts/poireau.sh ...

您还可以通过不带任何参数地调用 poireau.sh 来启用系统范围的跟踪。 如果一次只有一个进程会 LD_PRELOAD libpoireau.so,这非常有用:poireau.py 中的分析代码目前在匹配分配和释放时不会区分进程(编辑 poireau.py 中的全局 COMM 模式以仅从匹配某个正则表达式的程序中获取事件)。 系统范围的跟踪使跟踪程序启动后立即发生的事件变得更加容易。

TL;DR:

  1. 使用 perf buildid-cache --addperf probe 注册 libpoireau 的跟踪点。
  2. 准备您的程序以使用 libpoireau.so instrumentation 运行,例如,使用 LD_PRELOAD=/path/to/libpoireau.so
  3. 通过执行以下操作之一来获取 libpoireau 跟踪点事件: a. 启动instrumentation程序并运行 scripts/poireau.sh $PROGRAM_PID。 b. 在运行 scripts/poireau.sh 之前编辑 scripts/poireau.py 中的 COMM 模式,然后启动 instrumentation 程序。
  4. 等待 poireau.sh 每十分钟报告一次长时间存在的(> 五分钟)采样分配的堆栈。
  5. perf 的包可能不稳定。 尝试从源代码构建,并通过在运行 poireau.sh 之前设置 PERF=which perf poireau.sh 指向自定义可执行文件。

使用信号与 poireau.py 交互:

如何在启用 libpoireau 之后进行清理

使用以下命令禁用跟踪点

sudo perf probe --del sdt_libpoireau:*

并使用以下命令从 perf 的缓存中删除 libpoireau

sudo perf buildid-cache --remove ./libpoireau.so

以从 perf 子系统中擦除 libpoireau 的所有痕迹。

如果您不得不编辑一个 init 脚本,以在执行程序之前插入 LD_PRELOAD 变量,那么在编辑之后尽快撤消编辑并重新启动 instrumented 程序是有意义的。

高级用法

您可以通过将 POIREAU_SAMPLE_PERIOD_BYTES 设置为正的采样率(以字节为单位)来覆盖默认采样率(平均每 32 MB 采样一次)。

Poireau 也可以用来拍摄活动采样分配的快照(并打印出来),只要估计的堆占用空间达到新的高水位线即可。 只需将 --track-high-water-mark 参数传递给 poireau.py 即可; poireau.sh 会消耗第一个参数(如果有),并将其余参数传递给 poireau.py。 对于系统范围的跟踪,请将 * 作为第一个参数传递给 poireau.sh

poireau.py 分析脚本在 --track-high-water-mark 之后接受第二个位置参数:这是它将报告活动采样分配的最小大小(以字节为单位)。

Poireau 也可以与 perf record 一起用于短时间的任务; 这对于高水位堆分析特别有用。 首先使用 perf record -T -e std_libpoireau:* --call-graph=dwarf -- ./profilee ... 记录 perf.data,然后使用 perf script | ./poireau.py ... 将数据传递给分析。

它是如何工作的?

LD_PRELOADed 时,libpoireau 拦截对 malloc/calloc/realloc/free 的每个调用,并将绝大多数调用快速转发到如果在没有 libpoireau 的情况下将使用的真实实现。

只有标记为采样的那些分配才会被转移,在 malloccalloc 的情况下,并且仅当在已转移的分配上调用 free 时,才会被覆盖。 最后,出于采样目的,realloc 被视为一对 mallocfree

采样逻辑模拟一个以相等概率采样每个已分配字节的过程。 (硬编码的)采样率旨在平均每 32 MB 采样一个分配; 例如,我们对 100 字节的分配请求成为样本的一部分的概率与我们翻转 100 次有偏差的硬币(正面的概率为 1 / (32 * 1024 * 1024))并决定如果其中任何一个硬币翻转为“正面”,则将该请求作为样本的一部分的情况相同。

这种无记忆的采样策略使得即使在对抗性工作负载下也能得出关于堆分配调用形状的统计界限。 但是,一个简单的实现很慢。 我们没有为每个分配的字节翻转有偏差的硬币,而是通过从 Exponential 分布生成值来生成连续“反面”结果的数量。

每当选择对 malloccallocrealloc 的调用进行采样时,libpoireau 都会执行使用 USDT(用户静态定义的跟踪)探测进行检测的代码。 Linux perf 可以注释该代码以生成事件(这是一个系统范围的开关,适用于链接共享库的每个进程); 我们使用这些事件来让内核捕获每个采样调用的调用堆栈。

此外,这些分配请求被转移到内部跟踪分配器。 这使我们能够识别在跟踪分配上对 freerealloc 的调用,这对于生成配对的 USDT 事件(“此分配已释放或重新分配”)至关重要; 它还确保我们将这些分配传递回备份跟踪分配器,而不是系统 malloc。

一些合成微基准测试

对性能敏感的程序倾向于避免在热点中使用动态内存分配。 也就是说,这里有一些微基准测试,试图限制 LD_PRELOADing 在 libpoireau.so 中的开销,方法是在单个线程中重复进行成对的 mallocfree 调用(对于大多数内存分配器来说是最佳情况)。 以下结果是在运行 Linux 5.3.11 和 glibc 2.27 的未加载 AMD EPYC 7601 上计时的。

大型分配 (1 MB),采样周期为 32 MB (p = 3.2%):

baseline (glibc malloc): 0.092 us/malloc-free (0.047 user, 0.046 system)
  preloaded, no probe: 0.153 us/malloc-free (0.058 user, 0.094 system)
 preloaded, with probes: 0.236 us/malloc-free (0.067 user, 0.169 system)
preloaded, with tracing: 0.271 us/malloc-free (0.069 user, 0.203 system)

这几乎是我们最糟糕的情况:我们预计会非常频繁地触发分配跟踪,每 32 次分配一次,并且我们的跟踪分配器比纯 mmap/munmap 略微复杂(我们仍然应该改进)。

中型分配 (16 KB),采样周期为 32 MB (p = 0.049%):

baseline (glibc malloc): 0.042 us/malloc-free (0.041 user, 0.001 system)
  preloaded, no probe: 0.044 us/malloc-free (0.043 user, 0.001 system)
 preloaded, with probes: 0.046 us/malloc-free (0.042 user, 0.004 system)
preloaded, with tracing: 0.054 us/malloc-free (0.042 user, 0.012 system)

在这种不太不合理的大小下,将采样分配转移到跟踪分配器的开销小于 5%。 我们还可以观察到,虽然每当我们执行跟踪点时触发中断并不是免费的,但与生成回溯所需的时间相比,花费在服务中断上的时间相对较小(< 20%)。 这并不奇怪,因为我们使用内核的同一部分,该部分在使用 perf 分析性能问题时会练习。

小型分配 (128 B),采样周期为 32 MB (p = 0.00038%):

baseline (glibc malloc): 0.017 us/malloc-free (0.017 user, 0.000 system)
  preloaded, no probe: 0.020 us/malloc-free (0.020 user, 0.000 system)
 preloaded, with probes: 0.020 us/malloc-free (0.020 user, 0.000 system)
preloaded, with tracing: 0.020 us/malloc-free (0.020 user, 0.000 system)

在这里,所有的减速都是由从我们的拦截器 malloc 到基本系统 malloc 的弹跳引起的。

TL;DR:在分配微基准测试中,对于小型或中型分配,libpoireau instrumentation 的开销约为 5-20%,而对于非常大的分配,则高达约 70%。 启用分配跟踪会为小型或中型分配增加 0-20%,为非常大的分配增加约 130%。

这些是最坏情况的数据,适用于除了重复 mallocfree 之外什么都不做的程序。 实际上,对性能敏感的程序希望在内存管理上花费的时间少于 10%(在大型分配上花费的时间要少得多),这意味着 libpoireau 和捕获堆栈跟踪引入的总开销可能更接近 1-5%。

供应商依赖项

libpoireau 包含来自 xoshiro 256+ 1.0 的代码,该代码由 David Blackman 和 Sebastiano Vigna (vigna@acm.org) 于 2018 年编写,并且 致力于公共领域

libpoireau 包含 Systemtap 的 sys/sdt.h 文件,该文件 致力于公共领域

关于

Poireau:一个抽样分配调试器