内存泄漏(增长)火焰图
The following article is from Linux阅码场 Author 戴君毅
本文由西邮陈莉君教授研一学生戴君毅、梁金荣、马明慧等翻译,宋宝华老师指导和审核。译者戴君毅、梁金荣、马明慧等同学热爱开源,践行开放、自由和分享。
原文地址:http://www.brendangregg.com/FlameGraphs/memoryflamegraphs.html
正文
当你的应用程序占用的内存不断地提升时,你不得不立即修复它。造成这种情况的原因可能是因为错误配置而导致的内存增长,也可能是因为软件bug引起的内存泄露。
无论哪一种,由于垃圾回收机制开始积极响应(消耗CPU),一些应用的性能便会开始下降。一旦某个应用增长得太过庞大,那么其性能会受调页机制(swapping)的影响出现断崖式下降,甚至可能直接被系统kill掉(Linux系统的OOM Killer)。
无论是内存泄漏还是内存增长,如果你的应用在扩展,你肯定想先看看其内部发生了什么,说不定其实是个很容易修复的小问题。但关键是你怎样才能做到呢?
在调试增长问题时,不管你是使用应用程序还是系统工具,通常都要检查一下应用配置以及内存使用情况。
内存泄漏问题往往更难处理,但好在有一些工具可以提供帮助。一些工具采用对应用程序的malloc调用进行程序插桩(instrumentation)的方式,比如Valgrind memcheck,它还能够模仿一颗CPU,以至于所有的内存访问都能被检测到。
但使用该工具通常会导致某个应用程序变慢20-30倍,有时甚至会更慢;另外一个工具是libtcmalloc的堆分析器,使用它能快一点,但应用程序还是会慢5倍以上;
还有一类工具(比如gdb工具)会引发core dump,并随后处理它来研究内存使用情况。通常在发生core dump时会要求程序暂停,或者是终止,那么free()例程就会被调用。
所以尽管插桩型工具或者core dump技术都能够提供宝贵的细节,但别忘了你此时针对的是一个时刻增长的应用,这种情况下无论哪种工具都很难使用
本文我会总结一下我在分析(运行时应用的)内存增长和内存泄漏问题时所用到的四种追踪方法。
运用这些方法能够得到有关内存使用情况的代码路径,随后我会使用栈追踪技术对代码路径进行检查,并且会以火焰图的形式把它们可视化输出。
我将在Linux上演示一下分析过程,随后概述一下其它系统的情况。
| 先决条件 Linux:perf,eBPF |
| 1. 追踪分配器函数:malloc(), free(), ... 2. brk() 3. mmap() 4. 缺页中断 |
| 其他操作系统 |
| 总结 |
先决条件
-fomit-frame-pointer
这个GCC选项,这会使得基于帧指针的栈追踪技术无法使用;Linux: perf, eBPF
1. 追踪分配器函数: malloc(), free(), ...
-p PID
"选项在某个进程上运行Valgrind memcheck工具,并收集60秒内其内存泄漏的信息。1.1. Perl的例子
# /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
$ ./stackcollapse.pl < out.stacks | ./flamegraph.pl --color=mem \
--title="malloc() Flame Graph"--countname="calls"> out.svg
Perl_pp_split()
路径进行的。(最宽的一个分支)1.2. MySQL的例子
stackcount-D30
,代表持续时间为30秒。得到的火焰图如下:st_select_lex::optimization()
-> JOIN::optimization()
这条路径,然而这并不是分配内存最多的地方。JOIN::exec()
中分配的,只有少数字节在 JOIN::optimization()
中分配。size_t
为单位进行统计。# ./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
[...]
# egrep 'mysqld|sysbench' out.stacks | ./flamegraph.pl ... > out.svg
1.3. 警告
1.4. 其他例子
2. brk()系统调用
# perf stat -e syscalls:sys_enter_brk -I 1000 -a
# time counts unit events
1.0002833410 syscalls:sys_enter_brk
2.0006164350 syscalls:sys_enter_brk
3.0009239260 syscalls:sys_enter_brk
4.0012512510 syscalls:sys_enter_brk
5.0015933643 syscalls:sys_enter_brk
6.0019233180 syscalls:sys_enter_brk
7.0022222410 syscalls:sys_enter_brk
8.0025402720 syscalls:sys_enter_brk
[...]
sampling
模式,该模式下将对每个事件都进行dump。# 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
sleep120
”哑命令。由于brk的频率较低,可以将采样时间维持120秒(你也可以增加)来捕获足够多的样本用于分析。syscall:sys_enter_brk
这个tracepoint实现追踪。这里我将通过内核函数Sys_brk,使用BCC工具stackcount来演示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
下面是部分stackcount输出的结果:
$ 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
一条导致内存增长的代码路径 一条导致内存泄漏的代码路径 一条无辜的代码路径,恰好引发了当前堆空间不足的问题 异步分配器函数的代码路径,在可用空间减少时调用
3. mmap() syscall
syscall:sys_enter_mmap
。# perf record -e syscalls:sys_enter_mmap -a -g -- sleep 60
# perf script > out.stacks
[...copy out.stacks to a local system if desired...]
# ./stackcollapse-perf.pl < out.stacks | ./flamegraph.pl --color=mem \
--title="mmap() Flame Graph"--countname="calls"> out.svg
syscall:sys_enter_mmap
这个tracepoint实现追踪。# /usr/share/bcc/tools/stackcount SyS_mmap > out.stacks
[...copy out.stacks to a local system if desired...]
# ./stackcollapse.pl < out.stacks | ./flamegraph.pl --color=mem \
--title="mmap() Flame Graph"--countname="calls"> out.svg
一条导致内存增长的代码路径 一条映射内存泄漏的代码路径 异步分配器函数的代码路径,在可用空间减少时调用
4. 缺页中断
# perf stat -e page-faults -I 1000 -a
# time counts unit events
1.000257907534 page-faults
2.000581953440 page-faults
3.000886622457 page-faults
4.001184123701 page-faults
5.001474912690 page-faults
6.001793133630 page-faults
7.002094796636 page-faults
8.002401844998 page-faults
[...]
# perf record -e page-fault -a -g -- sleep 30
# perf script > out.stacks
[...copy out.stacks to a local system if desired...]
# ./stackcollapse-perf.pl < out.stacks | ./flamegraph.pl --color=mem \
--title="Page Fault Flame Graph"--countname="pages"> out.svg
handle_mm_fault()
来动态追踪缺页中断,也可以在4.14以上的内核中通过tracepoint来追踪,比如 t:exceptions:page_fault_user
和 t:exceptions:page_fault_kerne
。# /usr/share/bcc/tools/stackcount 't:exceptions:page_fault_*' > out.stacks
[...copy out.stacks to a local system if desired...]
# ./stackcollapse.pl < out.stacks | ./flamegraph.pl --color=mem \
--title="Page Fault Flame Graph"--countname="pages"> out.svg
Universe::initialize_heap
到 os::pretouch_memory
这条,但其实我对右边表示编译过程的那条分支更感兴趣,因为它能显示出有多少内存增长是由于java的编译造成的,而不是数据本身造成的。一条导致内存增长的代码路径 一条导致内存泄漏的代码路径
其它操作系统
Solaris: 可以使用DTrace进行内存追踪,这是我的原创文章: Solaris 内存泄漏(增长)火焰图
FreeBSD: 可以像Solaris一样使用DTrace,我有机会可以分享一些例子。
总结
分配器函数追踪 brk()系统调用追踪 mmap()系统调用追踪 缺页中断追踪
链接
我在USENIX LISA 2013 的关于Blazing Performance with Flame Graphs 的报告演讲中介绍了这四种分析内存的方法,在 幻灯片102 和 视频 56:22处。 我的原创网页内容Solaris Memory Flame Graphs 提供了更多的例子(基于其他系统) 在我的 2016 ACMQ文章 The Flame Graph中我总结了这四种内存分析的方法, 文章以Communications of the ACM, Vol. 59 No. 6出版。
后台回复“加群”,带你进入高手如云交流群
推荐阅读:
▼
喜欢,就给我一个“在看”
10T 技术资源大放送!包括但不限于:云计算、虚拟化、微服务、大数据、网络、Linux、Docker、Kubernetes、Python、Go、C/C++、Shell、PPT 等。在公众号内回复「1024」,即可免费获取!!