关于运维:性能优化-Memory-Leak

9次阅读

共计 6341 个字符,预计需要花费 16 分钟才能阅读完成。

如果应用程序内存使用量正在稳步增长,这可能是因为配置谬误导致的内存增长,或者因为软件谬误导致内存透露。对于某些应用程序,因为垃圾内存回收工作艰难,耗费了 CPU,性能可能会开始升高。如果应用程序应用的内存变得太大,性能可能会因为分页(swapping)而降落,或者应用程序可能会被零碎杀死(Linux’s OOM killer)。

针对下面的状况,就须要从应用程序或零碎工具查看应用程序配置和内存应用状况。内存透露起因的考察尽管很艰难,但有许多工具能够提供帮忙。有些人在依据应用程序中应用 malloc()来考察内存应用状况(如 Valgrind 和 memcheck),它还能够模仿 CPU,以便能够查看所有内存拜访。这可能会导致利用程序运行速度慢 20-30 倍或更多。另一个更快的工具是应用 libtcmalloc 的堆剖析性能,但它依然会使应用程序慢 5 倍以上。其余工具采取外围转储,而后后处理,以钻研内存应用状况,如 gdb。这些通常在应用外围转储时暂停应用程序,或要求终止应用程序,以便调用 free()函数。尽管外围转储技术为诊断内存透露提供了贵重的细节,但两者都不能轻松的发现内存透露的起因。

在此文中,我将总结我用于剖析已运行的应用程序上的内存增长和透露的四种跟踪办法。这些办法能够利用堆栈跟踪查看内存应用的代码门路,并可视化为火焰图。我将演示无关 Linux 的剖析,而后总结其余操作系统。

以下图中显示了以下四种办法,如绿色文本中的事件:

所有办法都会有些毛病,我会在下文中具体解释。

目录:

  Prerequisites
  Linux
  1. Allocator
  2. brk()
  3. mmap()
  4. Page Faults
  Other OSes
  Summary
  

Prerequisites

以下所有办法都须要堆栈跟踪能力对跟踪者可用,所以须要先修复这些跟踪器。许多应用程序都是应用 -fomit-frame-pointer 的 gcc 选项编译的,这突破了基于帧指针的堆栈走行。VM 运行时(如 Java)可自在编译办法,在没有额定帮忙的状况下可能无奈找到其符号信息,从而导致堆栈跟踪仅十六进制。还有其余的陷阱(gotchas)。请参阅我以前对于修复 Stack Traces 和 JIT Symbols For perf。

Linux: perf, eBPF

以下是通用办法。我将应用 Linux 作为指标示例,而后总结其余操作系统。

Linux 上有许多用于内存剖析的跟踪器。我将在这里应用 perf 和 bcc/eBPF,这是规范的 Linux 跟踪器。perf 和 eBPF 都是 Linux 内核源的一部分。perf 实用于较旧的 Linux 零碎,而 eBPF 至多须要 Linux 4.8 来执行堆栈跟踪。eBPF 能够更轻松地执行内核摘要,使其更高效并升高开销。

  1. Allocator Tracing: malloc(), free(), …

这是跟踪内存分配器函数、malloc()、free()等的办法。设想一下,你能够针对一个过程运行 Valgrind memcheck “-p PID”,并收集内存透露统计信息 60 秒左右。这尽管不能获得一个残缺的图片,但也有心愿捕捉到足以令人震惊的透露。如果没能抓到无效的信息,就须要继续足够长的工夫。

这些分配器函数对虚拟内存(而不是物理(驻留)内存)进行操作,而物理(驻留)内存通常是透露检测的指标。侥幸的是,它们通常具备很强的相关性,用于辨认问题代码。

有时应用分配器跟踪,开销很高,因而这更像是一种调试办法,而不是生产探查器。这是因为分配器性能(如 malloc()和 free()可能十分频繁(每天数百万次),并且增加大量的开销可能会减少。然而,解决问题是值得的。它能够比 Valgrind 的 memcheck 或 tcmalloc 的堆剖面器的开销小。如果你想本人试试这个,我会先看看应用 eBPF 的内核摘要能够解决多少,这在 Linux4.9 及更高版本上成果最好。

1.1. Perl Example

上面是应用 eBPF 进行内核内摘要的分配器跟踪示例。利用 bcc 的 stackcount 工具,应用 Perl 程序简略地对 libc malloc() 进行统计,给定过程的调用,。它应用 uprobes 探测 malloc() 函数以实现用户级动静跟踪。

# /usr/share/bcc/tools/stackcount -p 19183 -U c:malloc > out.stacks
^C
# more out.stacks
[...]

  __GI___libc_malloc
  Perl_sv_grow
  Perl_sv_setpvn
  Perl_newSVpvn_flags
  Perl_pp_split
  Perl_runops_standard
  S_run_body
  perl_run
  main
  __libc_start_main
  [unknown]
    23380

  __GI___libc_malloc
  Perl_sv_grow
  Perl_sv_setpvn
  Perl_newSVpvn_flags
  Perl_pp_split
  Perl_runops_standard
  S_run_body
  perl_run
  main
  __libc_start_main
  [unknown]
    65922
    

下面的输入是堆栈跟踪及其产生次数。例如,最初一个堆栈跟踪导致调用 malloc() 65922 次。这是内核上下文中计算的频率,并且仅在程序完结时输入后果。这样,咱们防止了将每个 malloc() 的数据传递到的用户空间的开销。我也应用 -U 选项仅跟踪用户级堆栈,因为我正在检测用户级函数:libc’s malloc()。

而后,应用我的 FlameGraph 软件将该输入转换为火焰图:

$ ./stackcollapse.pl < out.stacks | ./flamegraph.pl --color=mem \
    --title="malloc() Flame Graph" --countname="calls" > out.svg
    

当初把 out.svg 在浏览器中关上。显示了上面的火焰图。将鼠标悬停在元素上查看详细信息并单击以缩放(如果 SVG 在浏览器中不起作用,请尝试 PNG):

这表明大部分内存调配通过了 Perl_pp_split() 门路。

如果想本人尝试此办法,请记住跟踪所有内存调配函数:malloc(), realloc(), calloc() 等。还能够检测调配的内存大小,而不是样本计数(函数调用回数),以便火焰图显示调配的字节而不是调用计数。Sasha Goldshtein 曾经编写出一种基于 eBPF 的高级工具,用于检测这些性能,该工具可跟踪尚未在距离内开释的长期幸存调配,以及用于辨认内存透露。它是 bcc 中的 memleak:请参阅示例文件 example file。

1.2. MySQL Example

这个例子是解决基准负载的 MySQL 数据库服务器。我将开始应用如后面所述的堆栈计数火焰图(应用 stackcount -D 30 指定持续时间为 30 秒)。生成的火焰图为(SVG, PNG):

通过下面的火焰图,咱们能够看到大多数的 malloc() 呼叫是在 st_select_lex::optimize() -> JOIN::optimize() 被调用的。但这不是调配大部分字节的中央。

上面是 malloc() 字节火焰图,其中宽度显示调配的总字节数(SVG, PNG):

通过下面的火焰图,咱们能够看到大多数字节在 JOIN::exec() 中调配,而不是 JOIN::optimize()。这些火焰图的跟踪数据大抵同时捕捉,因而这里的区别是,某些调用大于其余调用,而不仅仅是在跟踪之间更改工作负载。

我开发了一个独自的工具,mallocstacks,这相似于堆栈计数,但将 size_t 参数作为指标,而不是只计算堆栈。火焰图生成步骤是 system-wide 跟踪 malloc():

# ./mallocstacks.py -f 30 > out.stacks
[...copy out.stacks to your local system if desired...]
# git clone https://github.com/brendangregg/FlameGraph
# cd FlameGraph
# ./flamegraph.pl --color=mem --title="malloc() bytes Flame Graph" --countname=bytes < out.stacks > out.svg

对于这个和晚期的 malloc() 计数火焰图,我增加了一个额定的步骤,只包含 mysqld 和 sysbench 堆栈(sysbench 是 MySQL 负载生成工具). 我在这里应用的理论命令是:

[...]
# egrep 'mysqld|sysbench' out.stacks | ./flamegraph.pl ... > out.svg

因为 mallocstacks.py 的输入(晚期应用 stackcollapse.pl)是 per-stack 跟踪的单行,因而很容易应用 grep/sed/awk 等工具来操作火焰图生成之前的数据。

我的 mallocstacks 工具只是一个概念验证,它只跟踪 malloc()。我会持续开发这些工具,但开销是一个问题。

1.3. Warning

正告:从 Linux 4.15 开始,通过 Linux uprobes 进行分配器跟踪的开销很高(在当前的内核中可能会有所改进)。此外,只管应用堆栈跟踪的内核内频率计数,Perl 程序运行速度慢 4 倍(从 0.53 秒到 2.14 秒)。但至多它比 libtcmalloc 的堆剖析要快,因为对于同一程序,它运行速度要慢 6 倍。这是最蹩脚的状况,因为它包含程序初始化,这使得 malloc() 变得很重。导致 MySQL 服务器在跟踪 malloc() 时吞吐量损失 33%(CPU 处于饱和状态,因而跟踪器没有 headroom)。这在生产中不可承受。

因为这个开销问题,我尝试应用以下各节中形容的其余内存剖析技术(brk(), mmap(), page faults)。

1.4. Other Examples

另一个例子是,Yichun Zhang(agentzh)应用 Linux 的 SystemTap 开发的 leaks.stp,它应用内核内总结来提高效率。他从这里创立了火焰图,example here,这看起来很棒。尔后,我向火焰图增加了一个新的调色板 (–color=mem),以便咱们能够辨别 CPU 火焰图(热色彩)和内存图(绿色)。

然而 malloc 跟踪的开销如此之高,因而我更喜爱间接办法,如以下无关 brk(), mmap(), and page faults 等章节所述。这是一个衡量:对于透露检测,它们不像间接跟踪分配器函数那样无效,但它们的确会产生更少的开销。

  1. brk() syscall

许多应用程序应用 brk() 来考察内存持续增长。此零碎调用(brk())能够设置程序断点在堆段(也称为过程数据段)的开端。brk() 不是由应用程序间接调用,而是提供 malloc()/free() 接口的用户级分配器。此类分配器通常不会将内存返回操作系统,而是将将开释的内存保留为未来调配的缓存。因而,brk() 通常只用于增长(而不是膨胀)。咱们也是假如这样来简化跟踪。

brk() 通常不频繁(例如 <1000/ 秒),这意味着应用 perf 进行每个事件跟踪可能就足够了。上面此办法测量应用 perf 的 brk() 的速率(在这种状况下应用内核计数):

# perf stat -e syscalls:sys_enter_brk -I 1000 -a
#           time             counts unit events
     1.000283341                  0      syscalls:sys_enter_brk
     2.000616435                  0      syscalls:sys_enter_brk
     3.000923926                  0      syscalls:sys_enter_brk
     4.001251251                  0      syscalls:sys_enter_brk
     5.001593364                  3      syscalls:sys_enter_brk
     6.001923318                  0      syscalls:sys_enter_brk
     7.002222241                  0      syscalls:sys_enter_brk
     8.002540272                  0      syscalls:sys_enter_brk
[...]

这是一个生产服务器,通常只有零 brk()s/ 秒。这就须要测量较长的工夫 (minutes),以捕捉足够的样本绘制火焰图。

如果 brk()s 的速率也很低,则只能在采样模式下应用 perf,在采样模式下,能够 per-event dumps。以下是应用 perf 和 FlameGraph 生成 brk 仪器和火焰图的步骤:

# perf record -e syscalls:sys_enter_brk -a -g -- sleep 120
# perf script > out.stacks
[...copy out.stacks to a local system if desired...]
# ./stackcollapse-perf.pl < out.stacks | ./flamegraph.pl --color=mem \
    --title="Heap Expansion Flame Graph" --countname="calls" > out.svg
    

下面包含一个 ”sleep 120″ 虚构命令。因为 brk 的不常见,您可能须要 120 秒甚至更长时间来捕捉足够的配置文件。

在较新的 Linux 零碎(4.8+)上,您能够应用 Linux eBPF。brk() 能够通过其内核函数 SyS_brk() or sys_brk() 调用。或 4.14+ 内核通过 syscalls:sys_enter_brk 跟踪点进行跟踪。我将在这里应用函数,并再次应用我的堆栈计数 bcc 程序显示 eBPF 步骤:

# /usr/share/bcc/tools/stackcount SyS_brk > out.stacks
[...copy out.stacks to a local system if desired...]
# ./stackcollapse.pl < out.stacks | ./flamegraph.pl --color=mem \
    --title="Heap Expansion Flame Graph" --countname="calls" > out.svg
    

上面是堆栈计数的一些示例输入:

$ cat out.stacks
[...]

  sys_brk
  entry_SYSCALL_64_fastpath
  brk
  Perl_do_readline
  Perl_pp_readline
  Perl_runops_standard
  S_run_body
  perl_run
  main
  __libc_start_main
  [unknown]
    3

  sys_brk
  entry_SYSCALL_64_fastpath
  brk
    19
    

下面的输入包含多个堆栈跟踪及其导致 brk() 的产生计数。我截断输入以仅显示最初两个堆栈和计数,只管残缺输入不会太长,因为 brk() 通常不频繁,并且没有这么多不同的堆栈:因为只有当分配器具备溢出其以后堆栈大小的申请时,它才产生。这也意味着开销非常低,应该能够忽略不计。将其与 malloc()/free() 插位器进行比拟,其中加速速度能够为 4 倍及更高。

当初一个示例 brk 火焰图 (SVG, PNG):

brk() 跟踪能够通知咱们导致堆扩大的代码门路。这能够是:

*  内存增长代码门路
*  内存透露代码门路
*  一个无辜的利用程序代码门路,碰巧溢出了以后堆的大小
*  异步分配器代码门路,该门路减少了应用程序,以应答空间减小

它须要一些侦探来辨别他们。如果您特地在搜寻透露,有时你会很侥幸,这将是一个不寻常的代码门路,很容易在 bug 数据库中找到作为已知的透露。

尽管 brk() 跟踪显示导致扩大的是什么,但稍后介绍的页面谬误跟踪显示随后耗费该内存的内容。

正文完
 0