共计 4466 个字符,预计需要花费 12 分钟才能阅读完成。
问题景象
团队外围利用每次公布完之后,内存会逐渐占用,不重启或者重新部署就会导致整体内存占用率超过 90%。
$$
公布 2 天后的内存占用趋势
$$
摸索起因一
堆内找到起因
呈现这种问题,第一想到的就是集群中随便找一台机器,信手 dump 一下内存,看看是否有堆内存使用率过高的状况。
$$
内存泄露
$$
$$
泄露对象占比
$$
发现 占比 18.8%
问题解决
是 common-division 这个包引入的
暂时性修复计划
- 以后加载俄罗斯 (RU) 国内地址库,改为一个小国家地址库 以色列(IL)
- 以后业务应用场景在补发场景下会应用,增加打点日志,确保是否还有业务在应用该服务,没人在用的话,间接下掉(后发现,确还有业务在用呢)。
完满解决问题,要的就是速度!!公布 ~~ 上线!!顺道记录下同一台机器的前后比照。
公布后短时间内有个内存增长实属失常,后续在做察看。
公布第二天,棘手又 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=detail
jcmd 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 查看内存应用,根本确认是堆外内存泄露。剩下的剖析过程就是确认是否堆外泄露,哪里在泄露。
堆外内存剖析
查了一堆文档基本思路就是
- pmap 查看内存地址 / 大小分配情况
- 确认以后 JVM 应用的内存治理库是哪种
- 剖析是什么中央在用堆外内存。
内存地址 / 大小分配情况
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/bin
ldd 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 install
export 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
外围配置
- make 之后,须要启用 prof,否则会呈现『<jemalloc>: Invalid conf pair: prof:true』相似的关键字
- 配置环境变量
- 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 的内存应用上的确存在高碎片率的问题,然而本次问题的基本还是在利用层面没有正确的敞开流加剧的堆外内存的泄露。
总结的过程,也是学习的过程,上述剖析过程欢送评论探讨。
作者|叔耀
点击立刻收费试用云产品 开启云上实际之旅!
原文链接
本文为阿里云原创内容,未经容许不得转载