前言:
遇到一个Linux零碎 glibc内存调配导致的OOM问题,本源是内存回收呈现问题,导致碎片太多,内存无奈回收,零碎认为内存不够用了。
波及到以下知识点:
1、Linux中典型的64M内存区域问题
2、glibc内存分配器ptmalloc2的底层原理
3、glibc的内存调配原理(Arean、Chunk、bins等)
4、malloc_trim对内存回收的影响
1、问题形容
前段时间做POC,在测试的过程中发现一个问题,应用Flink集群Session模式重复跑批处理工作时,集群某些节点TaskManger总是忽然挂掉。
查看挂掉节点的系统日志发现起因是:操作系统内存被耗尽,触发了零碎OOM,导致Flink TaskManager过程被操作系统杀掉了,下图:
从图二能够看到,taskManager过程曾经占了67%的内存40多G内存,持续跑工作还会持续减少
2、配置
2.1 测试环境及配置
测试应用的版本如下所示:
Flink 1.14.3
Icberg 0.13.1
Hive 3.1.2
Hadoop 3.3.1
jdk1.8.0_181
FLink Standalone配置
jobmanager.rpc.address: 127.127.127.127
jobmanager.rpc.port: 6123
env.java.opts: "-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/bigdata/dump.hprof"
jobmanager.memory.process.size: 2000m -- flink JM过程总内存
jobstore.expiration-time: 36000 -- 已实现工作保留工夫,每个工作会耗费50M内存
taskmanager.memory.process.size: 22000m -- Flink TM过程总内存
taskmanager.numberOfTaskSlots: 22
parallelism.default: 100
taskmanager.network.sort-shuffle.min-parallelism: 1 -- 默认应用sort-shuffle,flink 1.15之后默认就是1
taskmanager.network.blocking-shuffle.compression.enabled: true -- 是否启用压缩
taskmanager.memory.framework.off-heap.size: 1000m
taskmanager.memory.framework.off-heap.batch-shuffle.size: 512m
execution.checkpointing.interval: 60000
execution.checkpointing.unaligned: true -- 启用未对齐的检查点,这将大大减少背压下的检查点工夫
execution.checkpointing.mode: AT_LEAST_ONCE -- 配置数据处理次数,至多一次能够缩小背压,放慢处理速度
io.tmp.dirs: /home/testdir -- 临时文件存储地位,批处理和流解决,要留神磁盘空间是否够用
execution.checkpointing.checkpoints-after-tasks-finish.enabled: true -- 关上已实现job不影响checkpoint
能够看到,在配置中,此TaskManager调配的内存为22G,实际上通过TOP看到的后果曾经达到了40+G。
4、定位过程
4.1 查看内存占用状况
通过初步定位发现,在调用icebergStreamWriter的时候内存会猛涨一下,工作跑完之后,这块多进去的内存并不会回收,狐疑是申请了堆外内存做缓存,用完之后未开释
首先通过阿里的Arthas看一下内存状况
curl -O https://arthas.aliyun.com/art...
java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.5.5
[INFO] Process 142388 already using port 3658
[INFO] Process 142388 already using port 8563
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
而后输出1,Enter
而后输出dashboard即可看到以后内存状况
能够发现过程的堆内存只有6.3G,非堆也很小,加起来不到6.5G,那另外30多G内存被谁耗费了,查看JVM内存调配,如下所示
- 堆(Heap):eden、metaspace、old 区域等
- 线程栈(Thread Stack):每个线程栈预留 1M 的线程栈大小
- 非堆(Non-heap):包含 code_cache、metaspace 等
- 堆外内存:unsafe.allocateMemory 和 DirectByteBuffer申请的堆外内存
- native (C/C++ 代码)申请的内存
- 还有 JVM 运行自身须要的内存,比方 GC 等。
初步狐疑这块内存应该是被native内存或者堆外内存消耗掉了
堆外内存能够应用NMT(Native Memory Tracking (NMT) )工具进行查看
NMT必须先通过VM启动参数中关上,不过要留神的是,关上NMT会带来5%-10%的性能损耗。
-XX:NativeMemoryTracking=[off | summary | detail]
off: 默认敞开
summary: 只统计各个分类的内存应用状况.
detail: Collect memory usage by individual call sites.
flink的NMT开关应该设置在flink-conf.yaml中,如下
env.java.opts: "-XX:+HeapDumpOnOutOfMemoryError -XX:NativeMemoryTracking=detail",再次屡次执行工作之后,查看后果:
[root@node11 ~]# jcmd 46538 VM.native_memory
46538:
Native Memory Tracking:
Total: reserved=14674985KB, committed=13459465KB
Java Heap (reserved=10657792KB, committed=10657792KB)
(mmap: reserved=10657792KB, committed=10657792KB)
Class (reserved=1184412KB, committed=149916KB)
(classes #17919) (malloc=27292KB #30478) (mmap: reserved=1157120KB, committed=122624KB)
Thread (reserved=389173KB, committed=389173KB)
(thread #378) (stack: reserved=387392KB, committed=387392KB) (malloc=1243KB #1910) (arena=538KB #739)
Code (reserved=262665KB, committed=81641KB)
(malloc=13065KB #20566) (mmap: reserved=249600KB, committed=68576KB)
GC (reserved=417087KB, committed=417087KB)
(malloc=27699KB #650) (mmap: reserved=389388KB, committed=389388KB)
Compiler (reserved=779KB, committed=779KB)
(malloc=648KB #1797) (arena=131KB #18)
Internal (reserved=1729717KB, committed=1729717KB)
(malloc=1729685KB #79138) (mmap: reserved=32KB, committed=32KB)
Symbol (reserved=27077KB, committed=27077KB)
(malloc=24416KB #216304) (arena=2661KB #1)
Native Memory Tracking (reserved=6055KB, committed=6055KB)
(malloc=459KB #6453) (tracking overhead=5596KB)
Arena Chunk (reserved=228KB, committed=228KB)
(malloc=228KB)
堆外内存占用很少,远远达不到TaskManager过程占用的大小。
因为 NMT 不会追踪 native (C/C++ 代码)申请的内存,如压缩解压局部,到这里根本曾经狐疑是 native 代码导致的。4.2 应用jemalloc剖析内存分配情况
剖析代码无果,决定应用内存剖析工具来剖析一下到底是什么占用这么多内存。
找到了一篇文档,如下:
https://www.evanjones.ca/java-native-leak-bug.html4.2.1 装置和配置 jemalloc
./configure --enable-prof
make
make install
而后在/etc/profile中配置一下:
export LD_PRELOAD=/usr/local/lib/libjemalloc.so
备注:
如果要生成剖析文件须要多加一条配置:
export MALLOC_CONF="prof:true,prof_prefix:/home/bigdata/jeprof.out,lg_prof_interval:30,lg_prof_sample:20"
参数 lg_prof_interval:30,其含意是内存每减少 1GB(2^30,能够依据须要批改,这里只是一个例子),就输入一份内存 profile。这样随着工夫的推移,如果产生了内存的忽然增长(超过设置的阈值),那么相应的 profile 肯定会产生,那么咱们就能够在产生问题的时候,依据文件的创立日期,定位到出问题的时刻,内存到底产生了什么样的调配
执行source /etc/profile
4.2.2 测试内存
4节点的集群,为其中两台配置了jemalloc,另外两台不变。
重新启动flink集群,开始跑批处理作业,跑了几轮之后发现了异常情况。
如下图所示:其中两台节点配置了jemalloc,另外两台仍旧应用Linux默认的ptmalloc。跑了几个工作之后其中关上jemalloc节点的TaskManager内存占用十分失常,内存暴涨之后,待工作完结就能降下来。然而应用glibc malloc的节点的TaskManager内存始终上涨。
联想到jemalloc的一个特色就是能够缩小内存碎片,查了下材料,发现是glibc内存碎片导致的假性内存OOM。
5、Glibc内存治理
参考资料:
https://yuhao0102.github.io/2019/04/24/%E7%90%86%E8%A7%A3glibc_malloc_%E4%B8%BB%E6%B5%81%E7%94%A8%E6%88%B7%E6%80%81%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E5%99%A8%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/
https://zhuanlan.zhihu.com/p/452291093
目前开源社区公开了很多现成的内存分配器(Memory Allocators,以下简称为分配器):
- dlmalloc – 第一个被宽泛应用的通用动态内存分配器;
- ptmalloc2 – glibc 内置分配器的原型;
- jemalloc – FreeBSD & Firefox ,脸书所用分配器;
- tcmalloc – Google 奉献的分配器;
- libumem – Solaris 所用分配器;…
Linux 的晚期版本采纳 dlmalloc 作为它的默认分配器,然而因为 ptmalloc2 提供了多线程反对,所以 起初 Linux 就转而采纳 ptmalloc2 了。多线程反对能够晋升分配器的性能,进而间接晋升利用的性能。
在 dlmalloc 中,当两个线程同时 malloc 时,只有一个线程可能拜访临界区(critical section)——这是因为所有线程共享用以缓存已开释内存的「闲暇列表数据结构」(freelist data structure),所以应用 dlmalloc 的多线程利用会在 malloc 上消耗过多工夫,从而导致整个利用性能的降落。
5.1 内存治理构造
在 ptmalloc2 中,当两个线程同时调用 malloc 时,内存均会得以立刻调配——每个线程都保护着独自的堆,各个堆被独立的闲暇列表数据结构治理,因而各个线程能够并发地从闲暇列表数据结构中申请内存。这种为每个线程保护独立堆与闲暇列表数据结构的行为就「per thread arena」。
在glibc malloc中次要有 3 种数据结构,别离是:
- malloc_state ——Arena header—— 一个 thread arena 能够保护多个堆,这些堆另外共享同一个 arena header。Arena header 形容的信息包含:bins、top chunk、last remainder chunk 等;
- heap_info ——Heap Header—— 一个 thread arena 能够保护多个堆。每个堆都有本人的堆 Header(注:也即头部元数据)。什么时候 Thread Arena 会保护多个堆呢? 个别状况下,每个 thread arena 都只保护一个堆,然而当这个堆的空间耗尽时,新的堆(而非间断内存区域)就会被 mmap 到这个 aerna 里;
- malloc_chunk ——Chunk header—— 依据用户申请,每个堆被分为若干 chunk。每个 chunk 都有本人的 chunk header。
其中arena治理构造如下所示:
每一个arena都被malloc_state治理,malloc_state中蕴含了bins,toptrunk等重要信息
struct malloc_state{ /* Serialize access. */ __libc_lock_define (, mutex); /* Flags (formerly in max_fast). */ int flags; /* Set if the fastbin chunks contain recently inserted free blocks. */ /* Note this is a bool but not all targets support atomics on booleans. */ int have_fastchunks; /* Fastbins */ mfastbinptr fastbinsY[NFASTBINS]; /* Base of the topmost chunk -- not otherwise kept in a bin */ mchunkptr top; /* The remainder from the most recent split of a small request */ mchunkptr last_remainder; /* Normal bins packed as described above */ mchunkptr bins[NBINS * 2 - 2]; /* Bitmap of bins */ unsigned int binmap[BINMAPSIZE]; /* Linked list */ struct malloc_state *next; /* Linked list for free arenas. Access to this field is serialized by free_list_lock in arena.c. */ struct malloc_state *next_free; /* Number of threads attached to this arena. 0 if the arena is on the free list. Access to this field is serialized by free_list_lock in arena.c. */ INTERNAL_SIZE_T attached_threads; /* Memory allocated from the system in this arena. */ INTERNAL_SIZE_T system_mem; INTERNAL_SIZE_T max_system_mem;};
留神:
- Main arena 无需保护多个堆,因而也无需 heap_info。当空间耗尽时,与 thread arena 不同,main arena 能够通过 sbrk 拓展堆段,直至堆段「碰」到内存映射段;
- 与 thread arena 不同,main arena 的 arena header 不是保留在通过 sbrk 申请的堆段里,而是作为一个全局变量,能够在 libc.so 的数据段中找到。
上述内存构造能够通过命令查看,如
应用pmap -x pid 查看内存的散布状况,发现有很多64M左右的内存区域,如图所示
5.2 内存调配过程
Linux下内存治理是由glibc库来与内核交互,即用户空间是通过glibc来进行的零碎调用。glibc提供两种形式来申请内存,别离是brk和mmap,当通过malloc/new申请的内存小于M_MMAP_THRESHOLD(缺省128K)时,glic调用brk来申请内存,当要申请的内存大于M_MMAP_THRESHOLD时,glibc调用mmap来申请内存。这两种形式调配的都是虚拟内存,没有调配物理内存。在第一次拜访已调配的虚拟地址空间的时候,产生缺页中断,操作系统负责调配物理内存,而后建设虚拟内存和物理内存之间的映射关系。
当调用 malloc 分配内存的时候,会先查看以后线程公有变量中是否曾经存在一个调配区 arena。如果存在,则尝试会对这个 arena 加锁
如果加锁胜利,则会应用这个调配区分配内存
如果加锁失败,阐明有其它线程正在应用,则遍历 arena 列表寻找没有加锁的 arena 区域,如果找到则用这个 arena 区域分配内存。
当调用 free 接口开释内存时,会依据肯定的策略缓存起来,或者返还零碎。
因为 ptmalloc2 原本就是一个内存池,为了进步内存调配效率,防止用户态和内核态频繁进行交互,它须要通过一些策略,将局部用户开释(delete/free)的内存缓存起来,不马上返还给零碎。而缓存起来的内存块,通过 fastbinsY 和 bins 这些数组保护起来,数组保留的是闲暇内存块链表。
top 这个内存块指向 top chunk,它对于了解 glibc 从零碎申请内存,返还内存给零碎有着关键作用。
下图时一个内存调配过程,brk是将数据段(.data)的最高地址指针_edata往高地址推,实现虚拟内存调配;而通过mmap零碎调用调配的内存是在堆和栈的两头闲暇地址调配一块虚拟内存,这样开释时能够不受约束地自在开释。这样通过brk调配的内存是间断的一块空间,如下图中顺次brk申请ABD内存,开释的时候,若高地址的内存不开释,低地址的内存是不能开释的,如下图(7);而mmap申请的内存能够自在开释,如下图(6)。
参考:https://blog.csdn.net/u013259321/article/details/112031002
当通过brk开释的内存相邻的加起来达到M_TRIM_THRESHOLD(缺省128K)时,会进行内存压缩,如下图,先开释B,B的内存并没有真正开释,再开释D时,B+D>128K,此时这一块内存组就会开释掉。
上述如图7所示就呈现了内存碎片也叫内存空洞,就是这个导致了操作系统假性OOM。
5.3 内存碎片躲避方法
glibc治理的内存惟一开释的条件是堆顶存在128k(M_TRIM_THRESHOLD)或以上的闲暇区时才会开释,比方上图中只有D才有被偿还零碎的可能,B就老老实实成为内存空洞,尽管将来的调配还能用到,但开释不掉。
而有些32位条件下工作很好的程序,然而到64位后,这个阈值变大的起因(而物理内存其实并没有增大很多),因为总是到不了这个threshold,而总是有新的调配摞上来,这样就失去了开释的机会。
一般来说,通常的教训是
(1)glibc治理的内存绝大多数状况不会开释。因而编程时如果是小内存调配要尽快应用,尽快用完,尽快开释,不要停留,否则始终摞着,线性地址前面的就造成了空洞。
(2)如果是想内存总在管制中,能够调配大内存,自行治理开释和调配。不必的时候能够开释地很洁净
(3)不要调配很小的内存比方几个字节,因为一次malloc至多调配16个字节,如果每次调配都很小,就太亏了。
(4)升高M_MMAP_THRESHOLD,能够让更多的调配走mmap,防止brk得总总问题,特地是64位机器的状况下。
(5)升高M_TRIM_THRESOLD,让堆顶的闲暇内存更容易开释。
(6)定时调用malloc_trim()办法,将碎片的物理内存开释掉,等真正拜访的时候,再触发缺页中断。
以上(4)(5)(6)都不可避免会减少零碎缺页中断,影响零碎性能,应用中须要谨慎。
6、比照测试:
一共进行4轮测试,别离如下:
- Linux默认的glibc malloc
- 配置MALLOC_MMAP_THRESHOLD_=8092
- 配置MALLOC_ARENA_MAX = 4
- 装置配置jemalloc
不同模式别离跑15次批处理,开始测试之前内存应用状况:
6.1 Glibc malloc测试
工作完结后,TM内存间接暴涨到60%
6.2 批改MALLOC_ARENA_MAX值
工作完结后,TM内存间接涨到36%,再跑就不再上涨
export MALLOC_ARENA_MAX=4
6.3 批改内存缓存池调配阈值
export MALLOC_MMAP_THRESHOLD_=8192
工作完结后,内存管制的最好,TM内存维持在20%+,平均速度略微慢一点
6.4 jemalloc测试
须要装置jemalloc,然而性能最佳
工作完结后,内存管制的好,TM内存维持在20%+,平均速度十分好
7、论断
通过比照能够看到jemalloc性能和内存应用状况最优,然而稳定性须要测试一下海量数据的解决。
8、备注
能够应用三种形式来解决这种因为碎片太多导致系统OOM的问题:
8.1 配置jemalloc
- 下载 jemalloc https://github.com/jemalloc/jemalloc
- 解压 tar -xjvf jemalloc-5.2.1.tar.bz2
- 生成makefile文件 ./configure --enable-prof
- make
- make install
- 配置环境变量/etc/profile中
export LD_PRELOAD=/usr/local/lib/libjemalloc.so
export MALLOC_CONF="prof:true,prof_prefix:/home/bigdata/jeprof.out,lg_prof_interval:30,lg_prof_sample:20"
- source /etc/profile
启动flink集群
8.2 批改内存调配参数
- 在/etc/profile文件中配置 export MALLOC_MMAP_THRESHOLD_=8192
- 而后source /etc/profile
- 启动flink集群
该值默认的大小为128k,当申请的内存小于128k时,应用brk形式申请内存,free之后并不会立刻开释,而是治理起来做内存池,大于128k时,应用mmap形式申请内存,free掉之后会立刻还给操作系统
最优解须要依据理论状况进行测试。
备注:
既然堆内碎片不能间接开释,导致疑似“内存泄露”问题,为什么 malloc 不全副应用 mmap 来实现呢(mmap调配的内存能够会通过 munmap 进行 free ,实现真正开释)?而是仅仅对于大于 128k 的大块内存才应用 mmap ?
其实,过程向 OS 申请和开释地址空间的接口 sbrk/mmap/munmap 都是零碎调用,频繁调用零碎调用都比拟耗费系统资源的。并且, mmap 申请的内存被 munmap 后,从新申请会产生更多的缺页中断。例如应用 mmap 调配 1M 空间,第一次调用产生了大量缺页中断 (1M/4K 次 ) ,当munmap 后再次调配 1M 空间,会再次产生大量缺页中断。缺页中断是内核行为,会导致内核态CPU耗费较大。另外,如果应用 mmap 调配小内存,会导致地址空间的分片更多,内核的管理负担更大。 同时堆是一个间断空间,并且堆内碎片因为没有偿还 OS ,如果可重用碎片,再次拜访该内存很可能不需产生任何零碎调用和缺页中断,这将大大降低 CPU 的耗费。 因而, glibc 的 malloc 实现中,充分考虑了 sbrk 和 mmap 行为上的差别及优缺点,默认调配大块内存 (128k) 才应用 mmap 取得地址空间,也可通过 mallopt(M_MMAP_THRESHOLD, ) 来批改这个临界值。
8.3 批改MALLOC_ARENA_MAX
- 在/etc/profile文件中配置 export MALLOC_ARENA_MAX=4
- 而后source /etc/profile
- 启动flink集群
调试MALLOC_ARENA_MAX的数字就是在效率和内存耗费之间做抉择. 应用默认的MALLOC_ARENA_MAX能获得最佳效率, 然而可能耗费更多的内存. 缩小MALLOC_ARENA_MAX能缩小内存应用, 然而效率可能略微低一些.
9、名词解释
jemalloc:
jemalloc 是由 Jason Evans 在 FreeBSD 我的项目中引入的新一代内存分配器。它是一个通用的 malloc 实现,侧重于缩小内存碎片和晋升高并发场景下内存的调配效率,其指标是可能代替 malloc。jemalloc 利用非常宽泛,在 Firefox、Redis、Rust、Netty 等闻名的产品或者编程语言中都有大量应用。具体细节能够参考 Jason Evans 发表的论文 《A Scalable Concurrent malloc Implementation for FreeBSD》
tcmalloc:
tcmalloc 出身于 Google,全称是 thread-caching malloc,所以 tcmalloc 最大的特点是带有线程缓存,tcmalloc 十分闻名,目前在 Chrome、Safari 等出名产品中都有所应有。tcmalloc 为每个线程调配了一个部分缓存,对于小对象的调配,能够间接由线程部分缓存来实现,对于大对象的调配场景,tcmalloc 尝试采纳自旋锁来缩小多线程的锁竞争问题。
ptmalloc:
ptmalloc 是基于 glibc 实现的内存分配器,它是一个规范实现,所以兼容性较好。pt 示意 per thread 的意思。当然 ptmalloc 的确在多线程的性能优化高低了很多功夫。因为过于思考性能问题,多线程之间内存无奈实现共享,只能每个线程都独立应用各自的内存,所以在内存开销上是有很大节约的。
10、参考文档
[
](https://blog.csdn.net/lanzhup...)
https://yuhao0102.github.io/2019/04/24/%E7%90%86%E8%A7%A3glibc_malloc_%E4%B8%BB%E6%B5%81%E7%94%A8%E6%88%B7%E6%80%81%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E5%99%A8%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/
https://zhuanlan.zhihu.com/p/452291093