分享

重磅:Flink 1.14.0 内存优化汇总(一)

问题导读:
1.你熟悉Flink 吗?
2.堆外内存有哪些优缺点?
3.JVM内存缺陷有哪些?

前言

a1.png


由于 Flink 在大数据流计算中占据非常重要的位置,毫不夸张的说,已经被所有一二线互联网大厂所使用,并且 Flink 组件在 Apache 社区持续占据热榜前五。9月 30 号 Flink 1.14.0 版本发布,基于上面的问题,土哥二话不说,直接安排上。

本篇文章使用 Flink 1.14.0 最新版本 讲解 Flink 内存模型及调优策略,帮助小伙伴在生产环境中学会配置内存参数,轻松玩转 Flink。大纲目录如下:


a2.png



1 JVM

在大数据领域中,有很多开源框架(Hadoop、Spark、Storm)等都是基于 JVM 运行,可见 JVM 在大数据领域扮演的重要角色,所以在了解 Flink 内存时,我们需要先了解一下 JVM 。

JVM 是可运行 Java 代码的假想计算机 ,包括程序计数器、Java 虚拟机栈、本地方法栈、Java 堆 和方法区。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。


1.1 JVM 数据运行区

Java 虚拟机在执行 Java 程序的过程中会把它在主存中管理的内存部分划分成多个区域,每个区域存放不同类型的数据。如下图所示:


a3.png


1.程序计数器:是一个数据结构,用于保存当前正常执行的程序的内存地址。Java 虚拟机的多线程就是通过线程轮流切换并分配处理器时间来实现的,为了线程切换后能恢复到正确的位置,每条线程都需要一个独立的程序计数器,互不影响,该区域线程私有。

2.Java 虚拟机栈:与线程生命周期相同,用于存储局部变量表,操作栈,方法返回值。局部变量表放着基本数据类型,还有对象的引用,该区域线程私有。

3.本地方法栈:跟虚拟机栈很像,不过它是为虚拟机使用到的 Native 方法服务,该区域线程私有。

4.方法区:储存虚拟机加载的类信息,常量,静态变量,编译后的代码,该区域线程共享。

5.Java 堆:存放所有对象的实例。这一块区域在 Java 虚拟机启动的时候被创建,该区域被所有线程所共享,同时也是垃圾收集器的主要工作区域,因此这一部分区域除了被叫 堆内内存以外,也被叫做 GC 堆(Garbage Collected Heap)。


1.2 堆内内存(on-heap memory)

堆内内存是 Java 垃圾收集器的主要工作区域,为了提高垃圾回收的效率,在堆内内存的内部又划分出了新生代、老年代和永久代。在新生代内存中又按照 8:1:1 的比例划分出了 Eden、Survivor1、Survivor2 三个区域。

  • 新生代:新生代有一个 Eden 区和两个 Survivor 区,首先将对象放入 Eden 区,如果空间不足就向 Survivor1 区上放,触发一次 minor GC ,如果仍然放不下就将存活的对象放入 Survivor2 区中,然后清空 Eden 和 Survivor1 区的内存。在某次 GC 过程中,如果发现仍然又放不下的对象,就将这些对象放入老年代内存里去。
  • 老年代:大对象以及长期存活的对象直接进入老年代。
  • 永久代:永久存储区是一个常驻内存区域,用于存放 JDK 自身所携带的 Class、Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

如果出现 java.lang.OutOfMemoryError: PermGen space,说明是 Java 虚拟机对永久代 Perm 内存设置不够。


1.3 GC 算法

由于堆内内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 就提供 GC 功能自动监测对象是否超过作用域从而达到自动回收内存的目的。

关于堆内存和永久区的垃圾回收,Java 提供的 GC 算法包含:引用计数法,标记-清除算法,复制算法,标记-压缩算法,分代收集算法

  • 引用计数法:引用计数器的实现很简单,对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1,当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,则对象 A 就不可能再被使用。

缺点:1. 无法处理循环引用情况,会造成内存泄漏。  2.对系统性能产生影响。

  • 标记-清除算法:将垃圾回收分为两个阶段:标记阶段和清除阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

缺点:1. 效率问题,2. 空间问题。标记清除之后会产生大量不连续的内存碎片,空间碎片太多会导致以后程序在运行过程中需要分配较大对象时,无法找到足够的连续内存而提前触发另一次垃圾收集动作。

  • 复制算法:将可用内存按容量划分为大小相等的两块,每次只试用其中的一块,当这一块内存用完时,将存活的对象复制到另外一块内存上面,然后清除使用内存中的所有对象。 适用于初生代。

  • 标记压缩算法:首先标记出所有需要回收的对象,然后让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。适用于老年代

  • 分代收集算法:初生代使用复制算法,老年代使用标记压缩算法。



1.4 堆外内存(off-heap memory)

虽然 Java 提供了多种算法进行垃圾回收,但仍然无法彻底解决堆内内存过大带来的长时间的 GC 停顿的问题,以及操作系统对堆内内存不可知的问题。

基于上述问题,Java 虚拟机开辟出了堆外内存(off-heap memory)。堆外内存意味着把一些对象的实例分配在 Java 虚拟机堆内内存以外的内存区域,这些内存直接受操作系统(而不是虚拟机)管理。这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响。同时因为这部分区域直接受操作系统的管理,别的进程和设备(例如 GPU )可以直接通过操作系统对其进行访问,减少了从虚拟机中复制内存数据的过程。

Java 在 NIO 包中提供了 ByteBuffer 类,使用下面的方式,可以直接开辟指定大小的堆外内存,如下图为创建 128M 堆外内存。

a4.png

优点 :

  • 可以很方便的自主开辟很大的内存空间,对大内存的伸缩性很好;
  • 减少垃圾回收带来的系统停顿时间;
  • 直接受操作系统控制,可以直接被其他进程和设备访问,减少了原本从虚拟机复制的过程;
  • 特别适合那些分配次数少,读写操作很频繁的场景。


缺点 :

  • 容易出现内存泄漏,并且很难排查;
  • 堆外内存的数据结构不直观,当存储结构复杂的对象时,会浪费大量的时间对其进行串行化。

1.5 堆外内存与堆内内存联系

虽然堆外内存本身不受垃圾回收算法的管辖,但是因为其是由 ByteBuffer 所创造出来的,因此这个 buffer 自身作为一个实例化的对象,其自身的信息(例如堆外内存在主存中的起始地址等信息)必须存储在堆内内存中,具体情况如下图所示。


a5.png




1.6 JVM 内存管理缺陷

由于在 JVM 内存中存储大量的数据 (包括缓存和高效处理)时,JVM 内存会面临很多问题,包括如下:

  • Java 对象存储密度低。Java 的对象在内存中存储包含 3 个主要部分:对象头、实例 数据、对齐填充部分。例如,一个只包含 boolean 属性的对象占 16byte:对象头占 8byte, boolean 属性占 1byte,为了对齐达到 8 的倍数额外占 7byte。而实际上只需要一个 bit(1/8 字节)就够了。

  • Full GC 会极大地影响性能。尤其是为了处理更大数据而开了很大内存空间的 JVM 来说,GC 会达到秒级甚至分钟级。

  • OOM 问题影响稳定性。OutOfMemoryError 是分布式计算框架经常会遇到的问题, 当 JVM 中所有对象大小超过分配给 JVM 的内存大小时,就会发生 OutOfMemoryError 错误, 导致 JVM 崩溃,分布式框架的健壮性和性能都会受到影响。

  • 缓存未命中问题。CPU 进行计算的时候,是从 CPU 缓存中获取数据。现代体系的 CPU 会有多级缓存,而加载的时候是以 Cache Line 为单位加载。如果能够将对象连续存储, 这样就会大大降低 Cache Miss。使得 CPU 集中处理业务,而不是空转。



2 Flink 内存管理

基于 JVM 内存存在一些问题,并且在大数据场景下,无法在内存中存储海量数据,计算效率无法提高。Flink 社区采用自主内存管理设计。

Flink 并不是将大量对象存在堆内存上,而是将对象都序列化到一个预分配的内存块上, 这个内存块叫做 MemorySegment,它代表了一段固定长度的内存(默认大小为 32KB),也是 Flink 中最小的内存分配单元,并且提供了非常高效的读写方法,很多运算可以直接操作 二进制数据,不需要反序列化即可执行。每条记录都会以序列化的形式存储在一个或多个 MemorySegment 中。如果需要处理的数据多于可以保存在内存中的数据,Flink 的运算符会将部分数据溢出到磁盘。


2.1 Flink 内存模型

Flink 总体内存类图如下:


a6.png


主要包含 JobManager 内存模型和 TaskManager 内存模型。


2.2 JobManager 内存模型

Flink JobManager内存类图如虚线部分:


a7.png


在 1.11 中,Flink  对 JM 端的内存配置进行了修改,使它的选项和配置方式与 TM 端的配置方式保持一致。

  • 配置JobManager的总进程内存

  1. #Flink1.10版本
  2. #The heap size for the JobManager JVM
  3. jobmanager.heap.size:1024m
  4. #Flink1.11版本及以后
  5. #JobManager总进程内存
  6. jobmanager.memory.process.size:4096m
  7. # 作业管理器的 JVM 堆内存大小
  8. jobmanager.memory.heap.size:2048m
  9. #作业管理器的堆外内存大小。此选项涵盖所有堆外内存使用。
  10. jobmanager.memory.off-heap.size:1536m
  11. 2.3 TaskManager 内存模型
  12. TaskManager 内存模型如下图所示:
复制代码


a8.png


TaskManager 内存模型一共包含 3大部分,分别为总体内存、JVM Heap 堆上内存、Off-Heap 堆外内存等。

2.3.1 总体内存

Total Process Memory:Flink Java 应用程序(包括用户代码)和 JVM 运行整个进程所消耗的总内存。

总进程内存(Total Process Memory) = Flink 总内存 + JVM 元空间 + JVM 执行开销


a9.png



Total Flink Memory:仅 Flink Java 应用程序消耗的内存,包括用户代码,但不包括 JVM 为其运行而分配的内存。

Flink 总内存 = Framework堆内外 + task 堆内外 + network + managed Memory


a10.png



2.3.2 JVM Heap (JVM 堆上内存)

Framework Heap :框架堆内存

a11.png

Task Heap : 任务堆内存

如果内存大小没有指定,它将被推导出为总 Flink 内存减去框架堆内存、框架堆外内存、任务堆外内存、托管内存和网络内存。


a12.png

2.3.3 Off-Heap Mempry(JVM 堆外内存)

  • Managed memory: 托管内存

由 Flink 管理的原生托管内存,保留用于排序、哈希表、中间结果缓存和 RocksDB 状态后端。

托管内存由 Flink 管理并分配为原生内存(堆外)。以下工作负载使用托管内存:

流式作业可以将其用于 RocksDB 状态后端。流和批处理作业都可以使用它进行排序、哈希表、中间结果的缓存。流作业和批处理作业都可以使用它在 Python 进程中执行用户定义的函数。

托管内存配置时如果两者都设置,则大小将覆盖分数。如果大小和分数均未明确配置,则将使用默认分数。


a13.png

  • DirectMemory:JVM 直接内存

1)Framework Off-Heap Memory:Flink 框架堆外内存。

即 TaskManager 本身所占用的对外内存,不计入 Slot 资源。


a14.png

2)Task Off-Heap :Task 堆外内存。

专用于Flink 框架的堆外直接(或本机)内存。


a15.png

3)Network Memory:网络内存。

网络数据交换所使用的堆外内存大小,如网络数据交换 缓冲区。


a16.png

JVM metaspace:JVM 元空间。

Flink JVM 进程的元空间大小,默认为256MB。


a17.png

JVM Overhead :JVM执行开销。

JVM 执行时自身所需要的内容,包括线程堆栈、IO、 编译缓存等所使用的内存,这是一个上限分级成分的的总进程内存。


a18.png




未完待续...



最新经典文章,欢迎关注公众号





没找到任何评论,期待你打破沉寂

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条