valgrind 内存泄漏_Jemalloc及内存泄漏分析

不同层面的内存使用

Operating System

当程序在执行的时候, 可以通过ps, top等系统命令来预先发现内存泄漏的进程, 比如我们关注的是mysqld, 可以使用如下脚本来监控它的内存使用量. 这样能定位到可疑进程, 它占了多少内存, 它的内存是否在上涨.

#!/bin/bash

largest=70

while :; do
    mem=$(ps -p `pidof mysqld` -o %mem | tail -1)
    imem=$(printf %.0f $mem)
    if [ $imem -gt $largest ]; then
        largest=$imem
        echo `date`, $largest >> /tmp/large_mem.log
    fi
    sleep 10
done

即使对可疑进程的实现一无所知, 站在系统层面也是有办法定位问题的. 内存泄漏一般来说就是malloc/free不匹配导致 (其他分配释放函数类似), 只要能找出这种情况即可, 比如ebpf实现的memleak就是这样实现的. 为了减少篇幅, 下面只列了memleak的核心逻辑. 只要能找出泄漏内存的调用栈, 就基本定位问题了, 当然找到调用栈也不是必要条件.

static inline int gen_alloc_exit2(struct pt_regs *ctx, u64 address) {
        info.timestamp_ns = bpf_ktime_get_ns();
        info.stack_id = stack_traces.get_stackid(ctx, STACK_FLAGS);
        allocs.update(&address, &info);
        update_statistics_add(info.stack_id, info.size);
}

static inline int gen_free_enter(struct pt_regs *ctx, void *address) {
        allocs.delete(&addr);
        update_statistics_del(info->stack_id, info->size);
}

还有其他各种各样的系统工具可用于内存泄漏定位, 比如ASAN, Valgrind等, 但是通过ebpf可以在程序正常运行时随时进行调试, 是更为通用的方法. 在没有ebpf支持的场合, 也可以利用systemtap代替.

malloc lib

应用程序一般情况下会通过malloc库来申请/释放内存, 这里假设我们使用jemalloc, jemalloc从性能到调试都是目前比较好的选择. 当我们定位到可疑进程后, 怎么更进一步定位到问题? jemalloc提供了几种手段:

  • profile. 和上面的memleak类似, jemalloc也可以在每次malloc/free的地方收集调用栈, 如果每次搜集调用栈对性能影响太大, 也可以采样搜集. 具体使用方法这里不表
  • jemalloc自己的统计手段. 如果程序在运行, 我们可以简单通过gdb的函数打印
echo 'p malloc_stats_print(0,0,0)' | gdb --quiet -nx -p `pidof mysqld`

ec425e478fbd8ccc35d6a71566e40cb3.png

这里只截取了输出的一小部分, 它还包括jemalloc的版本, 配置选项等等. 通过jemalloc的输出, 可以看出是哪个大小的bin出问题, 比如是2560大小的分配太多, 还是2MB的分配出了问题. 当问题局限到某一个bin的时候, 特别是bin size比较大的情况, 开发者如果对代码比较熟悉, 定位还是比较简单的.

另外一个问题是, 既然ebpf能够在系统运行时搜集所有的分配释放的调用栈, malloc lib是不是也可以做类似的事情用于定位内存泄漏?

Application

一般比较成熟的应用都会内置自己的内存统计信息, 比如MySQL SQL层的内存统计, 从调试角度看这种方式的好处是开销很低, 只要对应用比较熟悉, 不需要借助其他系统的手段, 在很多时候也能较快定位问题.

  1. 通过performance schema可以得到SQL内存的统计信息, 首先需要在my.cnf中enable performance schema:
performance_schema=ON
  1. 并且在mysql命令行中enable相应的内存统计:
UPDATE setup_instruments SET ENABLED = 'YES' WHERE NAME LIKE 'memory/%';
  1. 然后就可以获取内存统计信息
SELECT EVENT_NAME, CURRENT_NUMBER_OF_BYTES_USED/1024/1024 FROM memory_summary_global_by_event_name WHERE EVENT_NAME like 'memory/sql/%' order by CURRENT_NUMBER_OF_BYTES_USED desc limit 10;

a66c11be6a34e7a456c17e619b161d0f.png

Jemalloc的开销

应用自己统计的内存信息可能和malloc lib统计的不一致, 可能原因如下:

  • 应用统计不全
  • 应用使用了mmap, 不经过malloc lib
  • malloc lib自己的开销应用不能感知, 我们现在讨论这个

metadata

jemalloc为了管理内存需要额外的metadata, 这些metadata需要占用物理内存:

Allocated: 52695496136, active: 60743540736, metadata: 449655848 (n_thp 0), resident: 61901737984, mapped: 62091505664, retained: 12955602944

jemalloc 5.0对metadata有较大改动, 之前使用chunk(一般为2MB)为单位管理内存, 5.0之后默认使用extent. 测试结果显示, 新的metadata大小显著减少.

dirty pages

当Application通过free释放内存的时候, 这些内存可能并没有通过munmap或者madivse(以page为单位)返还给OS. 这样做的好处是, reuse这片内存不需要page fault到OS kernel申请内存, 当然坏处就是可能造成内存浪费.

decaying:  time       npages       sweeps     madvises       purged
   dirty:   N/A      4129045       268681     34176851    367223631

我们主要关注dirty npages, 这里可以看到4129045个dirty pages, 这大约16GB内存是预期之外的.

slab utilization

当Application通过free释放内存的时候, 如果只是free了一个page的一部分, 或者slab的一部分, 这个时候肯定不会返还给OS的.

8504fec168bb0b13417f423876846682.png

还有一些fragmentation, 比如申请9字节, 实际分配了16字节, 这个在jemalloc log中都有反映.

extra active pages

除去以上的内存, active还是远大于allocated内存, 我们需要理解这些额外的内存到底什么用途.

f30fb20197b5a541b08654b1daf55274.png

当分配large object (>=16KB) 的时候, 默认情况下jemalloc并不会分配对齐的地址, 导致额外的空间开销.

const int N = 16;
void *buf[N];
int i;

for (i = 0; i < N; i++) { buf[i] = malloc(0x4000); }
for (i = 0; i < N - 1; i++) {
    printf("diff-16k: %lx, base: %pn", buf[i + 1] - buf[i], buf[i]);
}
for (i = 0; i < N; i++) { free(buf[i]); }

for (i = 0; i < N; i++) { buf[i] = malloc(0x2000); }
for (i = 0; i < N - 1; i++) {
    printf("diff-8k: %lx, base: %pn", buf[i + 1] - buf[i], buf[i]);
}
for (i = 0; i < N; i++) { free(buf[i]); }

运行情况如下:

$ LD_PRELOAD=libjemalloc.so ./a.out 2>&1 | tee 1
diff-16k: 5840, base: 0x7f1231053480
diff-16k: 4780, base: 0x7f1231058cc0
diff-16k: 4e00, base: 0x7f123105d440
diff-16k: 5a00, base: 0x7f1231062240
diff-16k: 4780, base: 0x7f1231067c40
diff-16k: 5840, base: 0x7f123106c3c0
diff-16k: 45c0, base: 0x7f1231071c00
diff-16k: 4f80, base: 0x7f12310761c0
diff-16k: 5a00, base: 0x7f123107b140
diff-16k: 4580, base: 0x7f1231080b40
diff-16k: 5c80, base: 0x7f12310850c0
diff-16k: 4400, base: 0x7f123108ad40
diff-16k: 50c0, base: 0x7f123108f140
diff-16k: 5600, base: 0x7f1231094200
diff-16k: 55c0, base: 0x7f1231099800
diff-8k: 2000, base: 0x7f12310a3000
diff-8k: 2000, base: 0x7f12310a5000
diff-8k: 2000, base: 0x7f12310a7000
diff-8k: 2000, base: 0x7f12310a9000
diff-8k: 2000, base: 0x7f12310ab000
diff-8k: 2000, base: 0x7f12310ad000
diff-8k: 2000, base: 0x7f12310af000
diff-8k: 2000, base: 0x7f12310b1000
diff-8k: 2000, base: 0x7f12310b3000
diff-8k: 2000, base: 0x7f12310b5000
diff-8k: 2000, base: 0x7f12310b7000
diff-8k: 2000, base: 0x7f12310b9000
diff-8k: 2000, base: 0x7f12310bb000
diff-8k: 2000, base: 0x7f12310bd000
diff-8k: 2000, base: 0x7f12310bf000

减少jemalloc自身的内存开销

  • metadata: 可以通过升级到新版jemalloc减少
  • dirty pages: 可以尝试多种途径
  • 设置dirty_decay_ms=0, 这种方法完全消除dirty pages, 不过对性能可能会有影响
  • 减少arena number, 因为purge dirty pages的执行点是N次进入jemalloc的函数并且是arena级别, 减少arena的个数应该能够有所帮助
  • 设置background_thread, 通过后台线程purge, 从而避免因为arena当前没有足够的内存操作delay purge
  • jemalloc提供了手动purge的函数
  • slab utilization: 这个可做的不多
  • fragmentation: 减少fragmentation大小的分配, 比如尽量减少16KB+1的malloc
  • extra active pages: 可以尝试2种办法, 需要性能验证
  • jemalloc --disable-cache-oblivious
  • 使用对齐的内存分配方法, 比如posix_memalign

从内容的角度

上面主要是从分配释放角度去定位内存泄漏问题, 很多时候需要进程还是活的, 如果只有core文件呢? 首先我们还是有办法解析出来里面jemalloc的统计信息, 我们可以基本定位到泄漏的bin size, 如果能够扫描该bin size的所有内容, 是不是有机会发现更多的蛛丝马迹? 最直接的一个想法是, 如果每个泄漏的对象都带一个标记, 比如magic number的话, 只要能在core里面找到大量的该magic number, 那么就能基本定位泄漏的对象, 从而最终定位泄漏的问题

struct Magic {
        Magic() : magic(0x1234) {}
        int magic;
};

int main() {
        for (int i = 0; i < 5; i++)
                Magic *p = new Magic();
        return 0;
}

在core里, 我们可以找到magic 0x1234的位置

(gdb) find /w 0x7ffff6e00000,0x7ffff7600000,0x1234
0x7ffff7208008
0x7ffff7208010
0x7ffff7208018
0x7ffff7208020
0x7ffff7208028

当然上面是通过正面找的方式, 也就是说已经知道0x1234已经泄漏了, 很多时候我们其实并不知道是哪里泄漏了, 一堆内存在那怎么找呢? 最暴力的方式, 比如按照4字节为单位扫描所有内存, 再计算它们的个数, 这其实是一种可行的方法. 但这种办法依赖一个前提, 每个数据结构有自己的标识, 比如上面的magic number, 或者c++里面的虚表等.

引用

http://jemalloc.net/jemalloc.3.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值