https://zhuanlan.zhihu.com/p/138886684
当程序在执行的时候, 可以通过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库来申请/释放内存, 这里假设我们使用jemalloc, jemalloc从性能到调试都是目前比较好的选择. 当我们定位到可疑进程后, 怎么更进一步定位到问题? jemalloc提供了几种手段:
echo 'p malloc_stats_print(0,0,0)' | gdb --quiet -nx -p `pidof mysqld`
这里只截取了输出的一小部分, 它还包括jemalloc的版本, 配置选项等等. 通过jemalloc的输出, 可以看出是哪个大小的bin出问题, 比如是2560大小的分配太多, 还是2MB的分配出了问题. 当问题局限到某一个bin的时候, 特别是bin size比较大的情况, 开发者如果对代码比较熟悉, 定位还是比较简单的.
另外一个问题是, 既然ebpf能够在系统运行时搜集所有的分配释放的调用栈, malloc lib是不是也可以做类似的事情用于定位内存泄漏?
一般比较成熟的应用都会内置自己的内存统计信息, 比如MySQL SQL层的内存统计, 从调试角度看这种方式的好处是开销很低, 只要对应用比较熟悉, 不需要借助其他系统的手段, 在很多时候也能较快定位问题.
performance_schema=ON
UPDATE setup_instruments SET ENABLED = 'YES' WHERE NAME LIKE 'memory/%';
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;
应用自己统计的内存信息可能和malloc lib统计的不一致, 可能原因如下:
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大小显著减少.
当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内存是预期之外的.
当Application通过free释放内存的时候, 如果只是free了一个page的一部分, 或者slab的一部分, 这个时候肯定不会返还给OS的.
还有一些fragmentation, 比如申请9字节, 实际分配了16字节, 这个在jemalloc log中都有反映.
除去以上的内存, active还是远大于allocated内存, 我们需要理解这些额外的内存到底什么用途.
当分配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: %p\n", 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: %p\n", 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
上面主要是从分配释放角度去定位内存泄漏问题, 很多时候需要进程还是活的, 如果只有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++里面的虚表等.