问题景象

团队外围利用每次公布完之后,内存会逐渐占用,不重启或者重新部署就会导致整体内存占用率超过90%。

$$公布2天后的内存占用趋势$$

摸索起因一

堆内找到起因

呈现这种问题,第一想到的就是集群中随便找一台机器,信手dump一下内存,看看是否有堆内存使用率过高的状况。

$$内存泄露$$

$$泄露对象占比$$

发现 占比18.8%

问题解决

是common-division这个包引入的

暂时性修复计划

  1. 以后加载俄罗斯(RU)国内地址库,改为一个小国家地址库 以色列(IL)
  2. 以后业务应用场景在补发场景下会应用,增加打点日志,确保是否还有业务在应用该服务,没人在用的话,间接下掉(后发现,确还有业务在用呢 )。

完满解决问题,要的就是速度!!公布 ~~上线!!顺道记录下同一台机器的前后比照。

公布后短时间内有个内存增长实属失常,后续在做察看。

公布第二天,棘手又dump一下同一台机器的内存

由原来的18.8%到4.07%的占比,升高了14%,牛皮!!

傻了眼,内存又飙升到86%~~ 该死的迷之自信!!

$$公布后内存使用率$$

摸索起因二

没方法汇报了~~~然而问题还是要去看看为什么会占用这么大的内存空间的~

查看过程内存应用

java 过程内存使用率 84.9%,RES 6.8G。

查看堆内应用状况

当期机器配置为 4Core 8G,堆最大5G,堆应用为有余3G左右。

应用arthas的dashboard/memory 命令查看以后内存应用状况:

以后堆内+非堆内存加起来,远有余以后RES的使用量。那么是什么中央在占用内存??

开始初步狐疑是『堆外内存泄露』

开启NMT查看内存应用

笔者是预发环境,正式环境开启需谨慎,本性能有5%-10%的性能损失!!!
-XX:NativeMemoryTracking=detailjcmd pid VM.native_memory

如图有很多内存是Unknown(因为是预发开启,绝对占比仍是很高)。

概念
NMT displays “committed” memory, not "resident" (which you get through the ps command). In other words, a memory page can be committed without considering as a resident (until it directly accessed).

rssAnalyzer 内存剖析

笔者没有应用,因为本性能与NMT作用相似,临时没有截图了~
rssAnalyzer(外部工具),能够通过oss在预发/线上下载。

通过NMT查看内存应用,根本确认是堆外内存泄露。剩下的剖析过程就是确认是否堆外泄露,哪里在泄露。

堆外内存剖析

查了一堆文档基本思路就是

  1. pmap 查看内存地址/大小分配情况
  2. 确认以后JVM应用的内存治理库是哪种
  3. 剖析是什么中央在用堆外内存。

内存地址/大小分配情况

pmap 查看

pmap -x 2531 | sort -k 3 -n -r

剧透:
32位零碎中的话,多为1M64位零碎中,多为64M。

strace 追踪

因为系统对内存的申请/开释是很频繁的过程,应用strace的时候,无奈阻塞到本人想要查看的条目,举荐应用pmap。

strace -f -e"brk,mmap,munmap" -p 2853
起因: 对 heap 的操作,操 作零碎提供了 brk()函数,C 运行时库提供了 sbrk()函数;对 mmap 映射区域的操作,操作系 统提供了 mmap()和 munmap()函数。sbrk(),brk() 或者 mmap() 都能够用来向咱们的过程添 加额定的虚拟内存。Glibc 同样是应用这些函数向操作系统申请虚拟内存。

查看JVM应用内存分配器类型

发现很大量为[anon](匿名地址)的64M内存空间被申请。通过附录参考的一些文档发现很多都提到64M的内存空间问题(glibc内存分配器导致的),抱着试试看的态度,筹备看看是否为glibc。

cd /opt/taobao/java/binldd java

glibc为什么会有泄露

咱们以后应用的glibc的版本为2.17。说到这里可能简略须要介绍一下glibc的发展史。

『V1.0时代』Doug Lea Malloc 在Linux实现,然而在多线程中,存在多线程竞争同一个调配调配区(arena)的阻塞问题。
『V2.0时代』Wolfram Gloger 在 Doug Lea 的根底上改良使得 Glibc 的 malloc 能够反对多线程——ptmalloc。

glibc内存开释机制(可能呈现泄露机会)

调用free()时闲暇内存块可能放入 pool 中,不肯定归还给操作系统。
.膨胀堆的条件是以后 free 的块大小加上前后能合并 chunk 的大小大于 64KB、,并且 堆顶的大小达到阈值,才有可能膨胀堆,把堆最顶端的闲暇内存返回给操作系统。
『V2.0』为了反对多线程,多个线程能够从同一个调配区(arena)中分配内存,ptmalloc 假如线程 A 开释掉一块内存后,线程 B 会申请相似大小的内存,然而 A 开释的内 存跟 B 须要的内存不肯定齐全相等,可能有一个小的误差,就须要不停地对内存块 作切割和合并。

为什么是64M

回到后面说的问题,为什么会创立这么多的64M的内存区域。这个跟glibc的内存分配器无关下的,间作介绍。

V2.0版本的glibc内存分配器,将调配区域调配主调配区(main arena)和非主调配区(non main arena)(在v1.0时代,只有一个主调配区,每次进行调配的时候,须要对主调配区进行加锁,2.0反对了多线程,将调配区通过环形链表的形式进行治理),每一个调配区利用互斥锁使线程对于该调配区的拜访互斥。

主调配区:能够通过sbrk/mmap进行调配。
非主调配区,只能够通过mmap进行调配。
其中,mmap每次申请内存的大小为HEAP_MAX_SIZE(32 位零碎上默认为 1MB,64 位零碎默 认为 64MB)。

哪里在泄露

既然晓得了存在堆外内存泄露,就要查一下到低是什么中央的内存泄露。参考历史材料,能够应用jemalloc工具进行排查。

配置dump内存工具(jemalloc)

因为零碎装载的是glibc,所以能够本人在不降级jdk的状况下编译一个jemalloc。

github下载比较慢,上传到oss,再做下载。
sudo yum install -y git gcc make graphviz     wget -P /home/admin/general-aftersales https://xxxx.oss-cn-zhangjiakou.aliyuncs.com/jemalloc-5.3.0.tar.bz2 &&  \    mkdir   /home/admin/general-aftersales/jemalloc && \    cd  /home/admin/general-aftersales/ && \    tar -jxcf  jemalloc-5.3.0.tar.bz2 && \    cd  /home/admin/xxxxx/jemalloc-5.3.0/ && \    ./configure  --enable-prof && \    make && \    sudo make installexport LD_PRELOAD=/usr/local/lib/libjemalloc.so.2  MALLOC_CONF="prof:true,lg_prof_interval:30,lg_prof_sample:17,prof_prefix:/home/admin/general-aftersales/prof_prefix

外围配置

  1. make之后,须要启用prof,否则会呈现『<jemalloc>: Invalid conf pair: prof:true』相似的关键字
  2. 配置环境变量
  • LD_PRELOAD 挂载本次编译的库
  • MALLOC_CONF 配置dump内存的机会。
  • "lg_prof_sample:N",均匀每调配出 2^N 个字节 采一次样。当 N = 0 时,意味着每次调配都采样。
  • "lg_prof_interval:N",调配流动中,每流转 1 « 2^N 个字节,将采样统计数据转储到文件。

重启利用

./appctl restart

监控内存dump文件

如果上述配置胜利,会在本人配置的prof_prefix 目录中生成相应的dump文件。

而后将文件转换为svg格局

jeprof --svg /opt/taobao/java/bin/java prof_prefix.36090.9.i9.heap > 36090.svg

而后就能够在浏览器中浏览了

与参阅文档中后果统一,有通过Java java.util.zip.Inflater 调用JNI申请内存,进而导致了内存泄露。

既然找到了哪里存在内存泄露,找到应用的中央就很简略了。

发现首恶

通过arthas 的stack命令查看某个办法的调用栈。

statck java.util.zip.Inflater <init>
java.util.zip.InflaterInputStream

如上源码能够看出,如果应用InflaterInputStream(InputStream in) 来结构对象usesDefaultInflater=true, 否则全副为false;

在流敞开的时候。

end()是native办法。

只有在『usesDefaultInflater=true』的时候,才会调用free()将内存归尝试偿还OS,根据下面的内存开释机制,可能不会偿还,进而导致内存泄露。

comp.taobao.pandora.loader.jar.ZipInflaterInputStream

二方包扫描

ZipInflaterInputStream 的流敞开应用的是父类java.util.zip.InflaterInputStream,结构器应用public InflaterInputStream(InputStream in, Inflater inf, int size)

这样如上『usesDefaultInflater=false』,在敞开流的时候,不会调用end()办法,导致内存泄露。

com.taobao.pandora.loader.jar.ZipInflaterInputStream 源自pandora ,征询了相干负责人之后,发现2年前就曾经修复此内存泄露问题了。

最低版本要求
sar包里的 pandora 版本,要大于等于 2.1.17

问题解决

降级ajdk版本

须要征询一下jdk团队的同学,须要应用jemalloc作为内存分配器的版本。

降级pandora版本

如上所说,版本高于2.1.17即可。

咱们是团队是对立做的根底镜像,找相干的同学做了dockerfile from的降级。

公布部署&察看

这此真的难受了~

总结

探索了glibc的工作原理之后,发现相比jemalloc的内存应用上的确存在高碎片率的问题,然而本次问题的基本还是在利用层面没有正确的敞开流加剧的堆外内存的泄露。

总结的过程,也是学习的过程,上述剖析过程欢送评论探讨。

作者|叔耀

点击立刻收费试用云产品 开启云上实际之旅!

原文链接

本文为阿里云原创内容,未经容许不得转载