Mirinda 发表于 2021-10-11 11:37:14

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

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

前言


由于 Flink 在大数据流计算中占据非常重要的位置,毫不夸张的说,已经被所有一二线互联网大厂所使用,并且 Flink 组件在 Apache 社区持续占据热榜前五。9月 30 号 Flink 1.14.0 版本发布,基于上面的问题,土哥二话不说,直接安排上。本篇文章使用 Flink 1.14.0 最新版本 讲解 Flink 内存模型及调优策略,帮助小伙伴在生产环境中学会配置内存参数,轻松玩转 Flink。大纲目录如下:



1 JVM在大数据领域中,有很多开源框架(Hadoop、Spark、Storm)等都是基于 JVM 运行,可见 JVM 在大数据领域扮演的重要角色,所以在了解 Flink 内存时,我们需要先了解一下 JVM 。JVM 是可运行 Java 代码的假想计算机 ,包括程序计数器、Java 虚拟机栈、本地方法栈、Java 堆 和方法区。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
1.1 JVM 数据运行区Java 虚拟机在执行 Java 程序的过程中会把它在主存中管理的内存部分划分成多个区域,每个区域存放不同类型的数据。如下图所示:


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


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

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




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 总体内存类图如下:


主要包含 JobManager 内存模型和 TaskManager 内存模型。
2.2 JobManager 内存模型
Flink JobManager内存类图如虚线部分:

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

#Flink1.10版本

#The heap size for the JobManager JVM

jobmanager.heap.size:1024m

#Flink1.11版本及以后

#JobManager总进程内存
jobmanager.memory.process.size:4096m

# 作业管理器的 JVM 堆内存大小
jobmanager.memory.heap.size:2048m

#作业管理器的堆外内存大小。此选项涵盖所有堆外内存使用。
jobmanager.memory.off-heap.size:1536m

2.3 TaskManager 内存模型
TaskManager 内存模型如下图所示:


TaskManager 内存模型一共包含 3大部分,分别为总体内存、JVM Heap 堆上内存、Off-Heap 堆外内存等。2.3.1 总体内存Total Process Memory:Flink Java 应用程序(包括用户代码)和 JVM 运行整个进程所消耗的总内存。总进程内存(Total Process Memory) = Flink 总内存 + JVM 元空间 + JVM 执行开销



Total Flink Memory:仅 Flink Java 应用程序消耗的内存,包括用户代码,但不包括 JVM 为其运行而分配的内存。
Flink 总内存 = Framework堆内外 + task 堆内外 + network + managed Memory



2.3.2 JVM Heap (JVM 堆上内存)Framework Heap :框架堆内存
Task Heap : 任务堆内存
如果内存大小没有指定,它将被推导出为总 Flink 内存减去框架堆内存、框架堆外内存、任务堆外内存、托管内存和网络内存。

2.3.3 Off-Heap Mempry(JVM 堆外内存)
[*]Managed memory: 托管内存

由 Flink 管理的原生托管内存,保留用于排序、哈希表、中间结果缓存和 RocksDB 状态后端。托管内存由 Flink 管理并分配为原生内存(堆外)。以下工作负载使用托管内存:流式作业可以将其用于 RocksDB 状态后端。流和批处理作业都可以使用它进行排序、哈希表、中间结果的缓存。流作业和批处理作业都可以使用它在 Python 进程中执行用户定义的函数。托管内存配置时如果两者都设置,则大小将覆盖分数。如果大小和分数均未明确配置,则将使用默认分数。


[*]DirectMemory:JVM 直接内存

1)Framework Off-Heap Memory:Flink 框架堆外内存。即 TaskManager 本身所占用的对外内存,不计入 Slot 资源。

2)Task Off-Heap :Task 堆外内存。专用于Flink 框架的堆外直接(或本机)内存。

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

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

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




未完待续...


最新经典文章,欢迎关注公众号http://www.aboutyun.com/data/attachment/forum/201903/18/215536lzpn7n3u7m7u90vm.jpg
原文链接:https://mp.weixin.qq.com/s/eO7y8nULPv22PQDfgB79qg



页: [1]
查看完整版本: 重磅:Flink 1.14.0 内存优化汇总(一)