flink raskmanager 配置java堆内存与非堆 flink怎么设置堆内存_内存管理

一,flink的内存结构

flink raskmanager 配置java堆内存与非堆 flink怎么设置堆内存_内存管理_02

1,堆内存
  • frame heap:flink框架基于jvm运行,本身有很多类对象,存储在堆内存
  • task heap:用户代码创建的java对象存放在对内存,heapstatebackend使用的内存
2,堆外内存
  • managed memory:RockDBStateBackend申请的堆外内存,不受flink和jvm管理,可以通过设置rocksdb内存的大小间接控制;批处理算子的排序也会在这块内存进行
  • direct memory:有flink通过java申请的堆外内存
  • framewor off-heap:
  • framewor off-heap:
  • network buffer:flink任务网络数据传输使用的缓存
  • jvm metaspace:jvm元空间,存放字节码,常量池
  • jvm overhead

flink raskmanager 配置java堆内存与非堆 flink怎么设置堆内存_JVM_03

二,内存管理

1,为什么要单独的内存管理?
  • java对象的低密度存储
  • gc的性能消耗和用户不友好性(stw)
  • oom的用户不友好性
2,积极的内存管理

针对上面三个问题,flink采取有针对性的措施基于jvm进行更高层次的内存管理。

  • 独有的序列化
    针对java对象低密度存储的问题和flink内存计算的特点,flink基于自身特点开发了不同于java序列化框架的技术,flink任务缓存在内存的数据都会被序列化,节省了大量的空间。
  • 走向堆外
    堆内大内存会导致jvm初始化时间长、GC时间长,针对这个问题,flink充分利用堆外内存,将RocksDBBackend所需的内存放在堆外,需要网络传输的数据放在堆外缓冲区。这样会减少gc的次数和oom发生的概率。

基于堆外内存,网络传输还可以实现zero-copy

  • 缓存友好的数据结构
  • flink的二进制数据操作
    排序缓冲区在内部分为两个内存区域:第一个区域保存所有对象的完整二进制数据,第二个区域包含指向完整二进制对象数据的指针(取决于 key 的数据类型)。将对象添加到排序缓冲区时,它的二进制数据会追加到第一个区域,指针(可能还有一个 key)被追加到第二个区域。分离实际数据和指针以及固定长度的 key 有两个目的:它可以有效的交换固定长度的 entries(key 和指针),还可以减少排序时需要移动的数据。如果排序的 key 是可变长度的数据类型(比如 String),则固定长度的排序 key 必须是前缀 key,比如字符串的前 n 个字符。请注意:并非所有数据类型都提供固定长度的前缀排序 key。将对象序列化到排序缓冲区时,两个内存区域都使用内存池中的 MemorySegments 进行扩展。一旦内存池为空且不能再添加对象时,则排序缓冲区将会被完全填充并可以进行排序。Flink 的排序缓冲区提供了比较和交换元素的方法,这使得实际的排序算法是可插拔的。默认情况下, Flink 使用了 Quicksort(快速排序)实现,可以使用 HeapSort(堆排序)。

key通常是指作为排序依据的属性。

补充:为什么需要对外内存才能实现zero copy

参考文章

DirectByteBuffer是一个特殊的ByteBuffer,底层同样需要一块连续的内存,操作模式与普通的ByteBuffer一致,但这块内存是调用unsafe的native方法分配的堆外内存。

直接缓冲区的内存释放也是由unsafe的native方法完成的,DirectByteBuffer指向的内存通过PhantomReference持有,由JVM自行回收。但如果DirectByteBuffer经过数次GC后进入老年代,就很可能由于Full GC间隔较长而长期存活,进而导致指向的堆外内存也无法回收。当需要手动回收时,需要通过反射调用DirectByteBuffer内部的Cleaner的clean私有方法。

为何要使用堆外内存

Java应用一般能够操作的是JVM管理的堆内内存,一段数据从应用中发送至网络需要经过多次复制:

从堆内复制到堆外
从堆外复制到socket缓存
socket缓存flush
考虑到Java内存模型,可能还存在工作内存/主内存之间的复制;

考虑到GC,可能还存在堆内内存之间的复制;

而如果使用堆外内存,则少了一步从堆内到堆外的复制过程。

使用直接缓冲区的优点:

这块缓冲区内存不受JVM直接管理回收
大小不受JVM分配的最大内存限制
一些IO操作可以避免堆外内存和堆内内存间的复制,比如网络传输
某些生命周期较长的大对象可以保存在堆外内存,减少对GC的影响

缺点:

不受JVM直接管理,容易造成堆外内存泄露
由于堆外内存并不能保存复杂对象而只能保存基本类型的包装类(底层都是byte array),因此要保存对象时需要序列化

必须先复制到堆外内存的原因

底层通过write、read、pwrite,pread函数进行系统调用时,需要传入buffer的起始地址和buffer count作为参数。如果使用java heap的话,我们知道jvm中buffer往往以byte[] 的形式存在,这是一个特殊的对象,由于java heap GC的存在,这里对象在堆中的位置往往会发生移动,移动后我们传入系统函数的地址参数就不是真正的buffer地址了,这样的话无论读写都会发生出错。而C Heap仅仅受Full GC的影响,相对来说地址稳定。
JVM规范中没有要求Java的byte[]必须是连续的内存空间,它往往受宿主语言的类型约束;而C Heap中我们分配的虚拟地址空间是可以连续的,而上述的系统调用要求我们使用连续的地址空间作为buffer。

参考文献

1,为什么要堆外内存才能zero copy 2,flink内存管理总结
3,一文搞定flink内存管理4,flink内存模型各模块解释5,flink中文官网-内存管理*6,flink如何操作二进制数据-以排序为例