导读|蒙受内存泄露往往是令开发者头疼的问题,传统剖析工具 gdb、Valgrind 在解决内存泄露问题上效率较低。本文特地邀请到了腾讯后盾开发工程师邢孟棒以 TDSQL 理论生产中 mysql-proxy 内存泄露问题作为剖析对象,分享其基于动静追踪技术的通用内存泄露(增长)分析方法。其中将具体介绍内存分配器行为剖析、缺页异样事件剖析,涵盖应用程序内存调配的常见过程。浏览完本文后,开发者仅需关注多数可能导致内存泄露的代码门路,就能无效晋升定位内存泄露(增长)问题的效率。
背景
某个 TDSQL 私有化环境中,中间件 mysql-proxy 进行大量申请转发时,内存占用量持续增长导致 OOM 景象,最终影响了用户业务的失常应用。自己剖析该问题的过程中发现一个较为广泛的业务痛点:传统剖析工具(gdb、Valgrind 等)效率绝对较低,在私有化场景中尤其突出。针对这一痛点,我将提供绝对通用的内存泄露(增长)分析方法,帮助各位开发者更高效地定位产生泄露的代码门路,以期最大化缩小人力投入老本并升高对用户业务体验的影响。
根底概念
在开展讲述内存泄露(增长)分析方法之前,咱们先理解一些相干的根底概念。
内存泄露包含 内核内存泄露 、 应用程序内存泄露 两大类。内核内存泄露能够通过 kmemleak 进行检测,本文次要关注应用程序的内存泄露。应用程序的内存泄露又能够细分为:堆内存(Heap)泄露、内存映射区(Memory Mappings)泄露。咱们平时提及的 内存泄露 , 次要是指物理内存的泄露(继续调配、映射理论的物理内存,且始终未开释),危害较大,须要立刻修复。
另外,虚拟内存的泄露(继续调配虚拟内存,但未调配、映射理论的物理内存)容易被忽视,尽管危害绝对较小,但也需额定关注(过程的内存映射区总数量有下限,默认 1w)。
通常,应用程序内存调配波及的步骤大抵如下图所示:
第一,应用程序通过内存分配器(例如 libc)提供的 malloc 及其变体函数申请内存,free 函数开释相应内存。第二,内存分配器(例如 libc)外部通过零碎调用 brk 扩大堆内存(小块内存调配)。第三,内存分配器(例如 libc)外部通过零碎调用 mmap 分配内存映射区域(大块内存调配,默认不小于 128 KB)第四,二或三已申请的虚拟内存在首次写入时触发缺页异样,OS 调配理论物理页面,并将虚拟内存与其相关联,记录至页表。
其中,步骤一至三均为虚拟内存,步骤四调配理论物理内存并创立相应页表。
传统剖析工具 gdb、Valgrind
在定位 mysql-proxy 内存泄露(增长)问题的过程中,开发人员尝试应用了 Valgrind Memcheck、gdb 进行帮助剖析。最终前者实际效果不太现实;我通过后者剖析出泄露起因,但整个过程消耗了较多工夫。
gdb 是罕用的程序调试工具 ,益处不必赘述。但对于内存泄露或增长问题,gdb 毛病 也较为显著,大抵如下:烦扰程序失常运行,不适宜生产环境间接定位比拟艰难,且要求对源码有肯定理解。
Valgrind Memcheck 是一款知名度较高的内存泄露剖析工具,十分弱小,开发调试过程中可能疾速发现场景的内存泄露问题。不过开发者在应用之前,倡议对以下状况有所理解:
第一,须要重启程序,且作为 Valgrind 子过程运行。不适宜剖析正在产生内存增长的过程。
第二,代替默认的 malloc/free 等调配函数,指标过程运行速度减慢 20~30 倍。
第三,不能很好的反对 tcmalloc、jemalloc 内存分配器。(mysql-proxy 采纳了 jemalloc 内存分配器)
基于动静追踪的通用分析方法
对于正在运行、内存持续增长的利用来说,gdb、Valgrind Memcheck 工具其实都挺难施展价值。相比而言,动静追踪技术提供了一种通用且易用的形式。内存分配器相干函数调用、零碎调用、缺页异样等,都能够看作一个个事件。通过对这些事件的追踪、统计等,咱们能够剖析无关内存应用状况的具体代码门路,在不深刻源码细节的前提下疾速放大泄露产生的范畴。
本文波及两种基于动静追踪的通用分析方法:内存分配器行为剖析 、 缺页异样事件剖析,涵盖应用程序内存调配的常见过程。
1)内存分配器行为剖析
内存分配器(glibc、jemalloc 等)行为剖析整体思路如下:
首先,站在利用视角,重点关注应用程序内存调配的代码门路。
其次,动静追踪内存调配相干函数,统计未开释内存调配的调用栈与总字节数量,造成剖析工具 memstacks。
-
开发新工具 memstacks
该工具反对生成两种类型的火焰图:
一种是仅追踪 malloc 及其变体函数,不做 free 对消,后果可用于生成全量内存调配火焰图。
另一种是追踪 malloc 及其变体函数、free 函数,计算出追踪期间未开释的内存调配,后果可用于生成未开释内存调配火焰图。
其实现原理大抵如下:
借鉴现有 BCC 工具 memleak、mallocstacks,反对生成折叠栈,可生成全量内存调配火焰图、未开释内存调配火焰图。
借助 uprobes 动静追踪 malloc(以及变体 cmalloc、realloc)、free。
如上图所示,现有 BCC 工具 memleak、mallocstacks 各有优劣。新工具 memstacks 联合两者长处,容许有选择性的生成全量内存调配火焰图或者未开释内存调配火焰图须要的折叠栈格局。
-
全量内存调配火焰图
执行以下命令,追踪 mysql-proxy 过程所有 malloc 及其变体调用 60s,并生成全量内存调配火焰图。
# 步骤 1. 追踪 60s,生成全量内存调配折叠栈
# 其中,参数 -a 示意追踪所有的 malloc 及其变体,但不追踪 free 进行互相对消。参数 -f 示意生成折叠栈,用于步骤 2 生成火焰图。./memstacks -p $(pgrep -nx mysql-proxy) -af 60 > all_mallocs.stacks
# 步骤 2. 执行下述命令生成全量内存调配火焰图,输入至文件 all_mallocs.svg。./flamegraph.pl --color=mem --title="All malloc() bytes Flame Graph" --countname="bytes" < all_mallocs.stacks > all_mallocs.svg
火焰图如下所示,能够帮助开发者了解 mysql-proxy 调用 malloc 及其变体的要害代码门路。
-
未开释内存调配火焰图
执行以下命令,追踪 mysql-proxy 过程未开释 malloc 及其变体调用 60s,并生成内存调配火焰图。
# 步骤 1. 追踪 60s,生成未开释内存调配折叠栈
# 其中,参数 -f 示意生成折叠栈,用于步骤 2 生成火焰图。memstacks -p $(pgrep -nx mysql-proxy) -f 60 > unfreed_mallocs.stacks
# 步骤 2. 执行下述命令生成未开释内存调配火焰图,输入到文件 unfreed_mallocs.svg。./flamegraph.pl --color=mem --title="Unfreed malloc() bytes Flame Graph" --countname="bytes" < unfreed_mallocs.stacks > unfreed_mallocs.svg
火焰图如下所示,其中:
未开释内存共计 27.75 MB(追踪期间,通过 pidstat 察看到 mysql-proxy 过程 RSS 增量靠近 27 MB,与未开释内存统计量 27.75 MB 基本一致)。
已调配但未开释的代码门路次要有两处。其中,据研发反馈,tdsql::Item\_param::set\_str 正是导致 mysql-proxy 内存泄露产生的中央。而另一处并非真正的泄露。该工具有肯定的副作用,因为追踪的最初阶段有一些刚调配的内存还未来得及开释,须要进一步浏览源码甄别。另外,倡议多运行几次比照下后果,排除那些常常变动的调配门路。
对已调配但未开释的代码门路开展,后果如下:
相比全量内存调配火焰图,数据量缩小近 60 倍,须要重点关注的代码门路的缩小也比拟显著。因而,举荐优先应用未开释内存调配火焰图进行剖析。
2)缺页异样事件剖析
相比内存分配器行为剖析,缺页异样事件剖析提供了另一种视角,整体思路如下:
首先,站在内核视角,关注的是首次写入触发缺页异样的代码门路,而不是触发内存调配的代码门路。前者是过程 RSS 增长的起因,后者仅调配了虚拟内存,尚未映射物理内存。
其次,追踪缺页异样事件,统计未开释物理内存的调用栈与总页面数量,造成剖析工具 pgfaultstacks。
-
现有剖析工具
传统工具 perf,基于软件事件 page-faults
perf record -p $(pgrep -nx mysql-proxy) -e page-faults -c 1 -g -- sleep 60
BCC 工具 stackcount
基于动态追踪点 exceptions:page\_fault\_user。
stackcount -p $(pgrep -nx mysql-proxy) -U t:exceptions:page_fault_user
现有剖析工具尽管不便,然而以增量的形式去统计,不思考追踪过程中被开释的物理内存,最终统计的后果通常会偏大,对内存泄露(增长)的剖析会造成烦扰。
-
缺页异样火焰图(现有版)
执行以下命令,追踪 mysql-proxy 过程所有缺页事件 60s,并生成缺页异样火焰图。
perf record -p $(pgrep -nx mysql-proxy) -e page-faults -c 1 -g -- sleep 60 > pgfault.stacks
./flamegraph.pl --color=mem --title="Page Fault Flame Graph" --countname="pages" < pgfault.stacks > pgfault.svg
火焰图具体如下,共计 420,342 次缺页事件,但不是每一次缺页事件都调配一个新的物理页面(大多数状况下未调配),mysql-proxy RSS 理论增长量仅 60 多 MB。
-
开发新工具 pgfaultstacks
该工具的实现原理大抵如下:
第一,改良现有缺页事件统计形式(过滤物理页面已存在的缺页事件,并在追踪实现后读取指标过程的内存映射列表,通过计算将已开释的物理页面排除在外),仅关注真正泄露的物理内存。
第二,借助 tracepoint 或 kprobe 动静追踪 page faults 事件,个别状况下性能开销可忽略不计。
-
缺页异样火焰图
执行以下命令,追踪 mysql-proxy 过程满足过滤条件的缺页事件 60s,并生成缺页火焰图。
# 步骤 1. 追踪 60s,生成缺页异样折叠栈。其中,参数 -f 示意生成折叠栈,用于步骤 2 生成火焰图。pgfaultstacks -p $(pgrep -nx mysql-proxy) -f 60 > pgfault.stacks
# 步骤 2. 生成缺页火焰图,输入到文件 pgfault.svg。./flamegraph.pl --color=mem --title="Page Fault Flame Graph" --countname="pages" < pgfault.stacks > pgfault.svg
缺页火焰图如下,其中:
共计减少 17801 个物理页面(与 mysql-proxy 过程 RSS 增量基本一致)。
重点关注函数 g\_string\_append\_printf。(注:非内存泄露产生的环境,仅用来演示缺页异样火焰图)
相比现有版,该版本的数据量缩小 20 多倍,须要重点关注的代码门路缩小也比拟显著。
总结
本文以 TDSQL 理论生产中 mysql-proxy 内存泄露问题作为剖析对象,摸索基于动静追踪技术的通用内存泄露(增长)分析方法:内存分配器行为剖析 、 缺页异样事件剖析,并针对现有剖析工具进行改良,造成相应的剖析工具 memstacks、pgfaultstacks,欢送各位开发者尝试去开发。工具使用者仅需关注多数可能导致内存泄露的代码门路,无效晋升定位内存泄露(增长)问题的效率。如果你正在蒙受内存泄露(减少)的困扰,无妨尝试下本文提及的分析方法和工具,心愿有所帮忙。
腾讯工程师技术干货中转:
1、万字避坑指南!C++ 的缺点与思考(下)
2、全网首次揭秘:微秒级“复活”网络的 HARP 协定及其关键技术
3、一文读懂 Go 函数调用
4、H5 开屏从龟速到闪电,企微是如何做到的