查看原文
其他

spark 自己的内存管理——Tungsten 探秘

孙彪彪 张江打工人 2021-09-05

首发个人公众号  spark技术分享 ,  同步个人网站  coolplayer.net ,未经本人同意,禁止一切转载

自己管理内存的必要性

spark 的对手flink从最开始就是自己管理内存, spark 历史上都是使用 java  jvm堆来保存对象, 但是经常会碰到oom 或者 gc 的问题, 所以从 1.5 开始, spark 开始引入自己的内存管理项目 tungsten

这里有一个经常被拿来说明问题的例子, 在jvm中保存一个 “abcd” 这样的字符串, 使用 java object layout 工具输出看是这样的

java.lang.String object internals: OFFSET  SIZE   TYPE DESCRIPTION                    VALUE     0     4        (object header)                ...     4     4        (object header)                ...     8     4        (object header)                ...    12     4 char[] String.value                   []    16     4    int String.hash                    0    20     4    int String.hash32                  0 Instance size: 24 bytes (reported by Instrumentation API)

一个4字节的对象,在jvm中需要48个字节来存储, 能忍么, 对于spark developer 来讲,当然不能忍,

可以说, spark 更了解自己数据对象的生命周期, 所以 spark 比 jvm 更懂应该什么时候来 clean 垃圾数据,自己管的比jvm 的垃圾收集机制更有效率。

spark 内存管理的黑魔法

为了统一对in-heap,off-heap进行建模,避免上层应用要自己区分自己处理的是in-heap还是off-heap内存块, 所以这个时候就提出了一个Page的概念,并且使用逻辑地址来做指针,通过这个逻辑地址可以定位到特定一条记录的物理地址, 是不是很熟悉这个概念, 跟linux 内核中的逻辑地址和物理地址映射机制很像, linux 使用逻辑地址来映射 内存空间和磁盘交换空间, 上层应用程序中看到的地址都是逻辑地址, 如果一个逻辑地址对应的物理地址在磁盘上, linux 内部就会产生一个异常,并且把磁盘上的这块数据交换到内存中来,上层应用不会有任何感知。

那么现在,我来讲下spark 这里的内存映射管理。  spark 需要管理的是两种内存,in-heap内存 和 off-heap内存 Java是一门安全的编程语言,防止程序员犯很多愚蠢的错误,它们大部分是基于内存管理的。但是,有一种方式可以有意的执行一些不安全、容易犯错的操作,那就是使用Unsafe类。 我们可以使用 sun.misc.Unsafe公共API 来像c语言的 malloc 方法那样直接从 os 分配内存, 这类memory不再由JVM托管,而是类似与c语言的内存管理,可以显示的在分配到的binary datga上进行操作而不是操作java object。可以避免上文提到的jvm object overhead 内存空间浪费, 和 gc 的开销。 这两种内存地址有什么特点呢

  • in-heap: 不太容易获得物理地址, 但是可以使用 array object的引用和  数组的偏移地址来获取保存在 array 中的数据。

  • off-heap: 直接就是一个绝对地址。

我们可以看下 TaskMemoryManager 类的说明,  这个类就是用来统一这两种内存的,

Most of the complexity in this class deals with encoding of off-heap addresses into 64-bit longs. In off-heap mode, memory can be directly addressed with 64-bit longs. In on-heap mode, memory is addressed by the combination of a base Object reference and a 64-bit offset within that object. This is a problem when we want to store pointers to data structures inside of other structures, such as record pointers inside hashmaps or sorting buffers. Even if we decided to use 128 bits to address memory, we can't just store the address of the base object since it's not guaranteed to remain stable as the heap gets reorganized due to GC. Instead, we use the following approach to encode record pointers in 64-bit longs: for off-heap mode, just store the raw address, and for on-heap mode use the upper 13 bits of the address to store a "page number" and the lower 51 bits to store an offset within this page. These page numbers are used to index into a "page table" array inside of the MemoryManager in order to retrieve the base object. This allows us to address 8192 pages. In on-heap mode, the maximum page size is limited by the maximum size of a long[] array, allowing us to address 8192 * 2^32 * 8 bytes, which is approximately 35 terabytes of memory.

意思是说, 因为 jvm gc 的原因,jvm 对象的地址是变动的,很难使用绝对地址来定位java 对象。  所以spark 自己提出了一种解决方案, 对应 off-heap 内存, 直接就使用物理地址, 而对于 on-heap 内存,使用高13位来存储 page number, 低51位存储在页内的偏移,  也就是这样的 [13-bit page num][51-bit offset], 然后每个 taskManager 维护一个 页表来存储映射, 从 page number 来获取 MemoryBlock 对象, 根据 MemoryBlock 对象和 offset 就可以定位到物理地址,无论是on-heap 还是 off-heap。

[13-bit page num][51-bit offset]   前面 13位可以 表示 8192个page,  后面51 bit可以表示pb级别的,绝对够用了, 但是 long[] 数组有个限制, 那么long的长度最大被限制为 Int的最大值,2^32 * 8,也就是32GB。然后所有的Page加起来,大约35个TB。足够大了 其实。

一个存储组件提供无非以下几个功能,

  • 分配一个存储块, 也就是申请一个页

  • 在页上的一个偏移地址进行存储一条数据,然后返回一个 64 逻辑地址, 不用关心下层是 on-heap 还是 off-heap, 页加偏移地址(物理地址) ->  64 long adress 这个过程叫做对地址进行编码

  • 根据 64 位的逻辑地址拿到 物理地址,然后获取物理地址上的数据, 64 long adress -> 页加偏移(物理地址) 这个过程叫做解码

下面我们来看下对应 on-heap 和 off-heap 内存, 上面几个功能是怎么实现的。申请一个Page的流程为:

  • 申请到空闲的Page number号,这个页号使用 BitSet 数据结构的java实现的nextClearBit方法来递增页号,真是省啊。

  • 进行实际的内存分配,得到一个MemoryBlock,这里会根据你使用的 heap 模式还是unsafe 模式来选择使用 on-heap 分配内存(分配器为 HeapMemoryAllocator )还是 off-heap 分配内存(分配器为UnsafeMemoryAllocator),

  • 将Page number 赋给MemoryBlock

MemoryBlock 继承 MemoryLocation 代表着对内存的定位,这个对象可以把off-heap 和on-heap 进行统一, MemoryLocation 对于off-heap的memory,obj为null,offset则为绝对的内存地址,对于on-heap的memory,obj则是JVM对象的基地址,offset则是相对于改对象基地址的偏移。 记住这句话,因为很关键。

org.apache.spark.unsafe.memory.MemoryBlock 有四个字段

obj  如果是off-heap,则为null。否则则为一个array数组 offset 如果是off-heap 则为绝对偏移量,否则为  Platform.LONG_ARRAY_OFFSET pageNumber 页号 length 申请的内存的长度,这个in/off-heap 是一致的。

我画两个图来表示一下, 对于 off-heap 是这样的:

分配页的时候, 分配一个 13bit的page number, 创建一个 MemoryBlock 对象, offset 字段为 64bit的 绝对偏移量, 以 page number 为索引放在 page table 中。

逻辑地址的组成,是把page number 作为 high 13bit, 编码的时候,要使用数据写入地址的64位绝对地址减去页的64位绝对地址得出来51bit的offset 作为 逻辑地址的 low 51bit

解码根据地址读数据的时候是这样的:

对于 on-heap 是这样的:

分配页的时候,分配一个 13bit的page number,创建一个 MemoryBlock 对象,创建一个 array[long] 类型的对象, MemoryBlock 的 obj 对象,  以 page number 为索引放在 page table 中。

逻辑地址的组成,是把page number 作为 high 13bit, 然后用 51bit的数组内的偏移作为  low 51bit

解码根据地址读数据的时候是这样的:


欢迎关注 spark技术分享:

                                 

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存