关于java:利用jemalloc解决flink的内存溢出问题

32次阅读

共计 11301 个字符,预计需要花费 29 分钟才能阅读完成。

前言:

遇到一个 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.html

    4.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 轮测试,别离如下:

  1. Linux 默认的 glibc malloc
  2. 配置 MALLOC_MMAP_THRESHOLD_=8092
  3. 配置 MALLOC_ARENA_MAX = 4
  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

  1. 下载 jemalloc https://github.com/jemalloc/jemalloc
  2. 解压 tar -xjvf jemalloc-5.2.1.tar.bz2
  3. 生成 makefile 文件 ./configure –enable-prof
  4. make
  5. make install
  6. 配置环境变量 /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″

  1. source /etc/profile
  2. 启动 flink 集群

    8.2 批改内存调配参数

  3. 在 /etc/profile 文件中配置 export MALLOC_MMAP_THRESHOLD_=8192
  4. 而后 source /etc/profile
  5. 启动 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

  1. 在 /etc/profile 文件中配置 export MALLOC_ARENA_MAX=4
  2. 而后 source /etc/profile
  3. 启动 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

正文完
 0