2024年9月

现象

某个版本开始,我们线上某埋点采集服务实例开始出现不定期OOM重启,造成业务消费延迟抖动

初步排查判断为堆外内存有泄漏,堆内3G无OOM,jvm进程RSS为5G,容器limit也为5G,触发OOM重启

Snipaste_2024-09-09_21-06-43.png

排查

pmap查看进程内存段分配:

pmap -x {your_pid} | sort -k 3 -n | tail -n 20

可以看到堆外有大量几十M的内存段:

1040025030tpasl0p30063iahq0.png

通过gdb dump某些内存段,string看下内容:

先看smaps映射的内存段起止地址

cat /proc/{your_pid}/smaps | grep 7fbe7c

通过gdb attach上jvm进程,把对应段dump下来

gdb attach {your_pid}
dump memory mem.bin 0x7fbe7c000000 0x7fbe7dc24000

dump下来的文件可以通过strings命令看下内容,如果是文本内容基本可以确定是什么组件写的。这种堆外OOM,大概率是某个组件有native调用,c/c++申请的内存,或网络io/broker等组件零拷贝等操作,在堆外申请的内存没有释放。(当然这个case特殊些,组件确实释放了,但是glibc的malloc实现没有实际归还给操作系统)

止损&验证,可以在gdb shell里尝试手动跑下malloc_trim(注意可能会导致jvm crash,小心使用)

call malloc_trim()

如果能手动trim掉,那极有可能是glibc malloc导致内存没有归还给操作系统的问题。简而言之,glibc为了减少调用和切换消耗,用户free释放掉的内存并不会归还给操作系统,而是等下一次再malloc时直接用(能省几次系统调用),进而导致未归还的内存段持续占用内存,直到进程被oom kill。

解决

切换jemalloc,调整相关参数去平衡这个行为。

另: jemalloc接入

#include <mcheck.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
 
 
void __mtracer_on () __attribute__((constructor));
void __mtracer_off () __attribute__((destructor));
void __mtracer_on ()
{
    char *p=getenv("MALLOC_TRACE");
    char tracebuf[1023];
    if(!p)
        p="malloc_trace";
    sprintf(tracebuf, "%s.%d", p, getpid());
    setenv("MALLOC_TRACE",tracebuf, 1);
    atexit(&__mtracer_off);
    mtrace();
}
 
void __mtracer_off ()
{
    muntrace();
}

build:

gcc mtrace.c  -fPIC -shared  -o libmtrace.so
vi /data/infra-apps/xxx/conf/xxx.sh
 
export MALLOC_TRACE="/tmp/malloc_trace.log"
export LD_PRELOAD="/tmp/libmtrace.so"
 
mtrace malloc_trace.log.478
mtrace malloc_trace.log.478 | sed '1,/Caller/d'|awk '{s[$NF]+=strtonum($2);n[$NF]++;}END{for(k in s){print k,n[k],s[k]}}'|column -t | sort -k 3 -n