Spark中的堆外和堆内内存以及内部行数据表示UnsafeRow

背景

本文基于 Spark v4.0.0
写此文章的目的是为了说明 Spark计算引擎在堆内和堆外内存的处理,以及在内部行处理的优化。

分析

堆外和堆内内存

Spark中的内存分为执行内存和存储内存,
执行内存用来在用来缓存在shufle过程中的一些数据,比如说 shuffle 用来排序的内存。
存储内存用来缓存RDD数据以及broadcast的数据等。

具体的代码在 MemoryManager 中:

 private[memory] final val tungstenMemoryAllocator: MemoryAllocator = {
    tungstenMemoryMode match {
      case MemoryMode.ON_HEAP => MemoryAllocator.HEAP
      case MemoryMode.OFF_HEAP => MemoryAllocator.UNSAFE
    }
  }
...

 MemoryAllocator UNSAFE = new UnsafeMemoryAllocator();

 MemoryAllocator HEAP = new HeapMemoryAllocator();
  • 执行内存中如何被使用
    ShuffleExternalSorter举例,在进行shuffle数据写入的时候,会经过如下数据流:
UnsafeShuffleWriter.write
    ||
    \/
insertRecordIntoSorter
    ||
    \/
ShuffleExternalSorter.insertRecord
    ||
    \/
acquireNewPageIfNecessary
    ||
    \/
allocatePage
    ||
    \/
TaskMemoryManager.allocatePage
    ||
    \/
memoryManager.tungstenMemoryAllocator().allocate(acquired)
    ||
    \/
MemoryAllocator.allocate // UnsafeMemoryAllocator 或者 HeapMemoryAllocator
  • 存储内存如何被使用
    以缓存RDD的结果为例,经过的数据流如下:
 Executor.run
    ||
    \/
 env.blockManager.putByte
    ||
    \/
 BlockStoreUpdater.save()
    ||
    \/
 saveSerializedValuesToMemoryStore
    ||
    \/
 MemoryManager.putBytes
    ||
    \/
 MemoryManager.acquireStorageMemory
    ||
    \/
 storagePool.acquireMemory // StorageMemoryPool(this, MemoryMode.ON_HEAP)或者StorageMemoryPool(this, MemoryMode.OFF_HEAP)
  • 堆内堆外内存的分配和释放

UnsafeMemoryAllocator.allocate 的方法调用Unsafe.allocateMemory的方法,得到一个native的地址offeset:

 long address = Platform.allocateMemory(size);
 MemoryBlock memory = new MemoryBlock(null, address, size);

UnsafeMemoryAllocator.free直接调用 Platform.freeMemory(memory.offset);进行内存的释放:

Platform.freeMemory(memory.offset);

HeapMemoryAllocator.allocate的方法直接new一个Long类型的数组:

long[] array = new long[numWords];
MemoryBlock memory = new MemoryBlock(array, Platform.LONG_ARRAY_OFFSET, size);

HeapMemoryAllocator.free的方法是通过释放java对象的引用来达到释放该内存对象的目的:

 memory.setObjAndOffset(null, 0);
 LinkedList<WeakReference<long[]>> pool =
          bufferPoolsBySize.computeIfAbsent(alignedSize, k -> new LinkedList<>());
        pool.add(new WeakReference<>(array));

并且通过弱引用来引用该对象,方便二次利用(如果该对象没有被gc释放的话)。

UnsafeRow

这个UnsafeRow是Spark InternalRow的一种实现,他的作用

  1. 减少 JVM GC 的压力
  2. 将 JVM 对象序列化为字节数组进行存储,且不影响正常的数据操作,减少了数据存储内存,可以精确计算内存的使用情况
  3. 减少了 Shuffle 过程中序列化和反序列化的的消耗
  • 减少GC压力
    相比SpecificInternalRow / GenericInternalRow 都是以Array进行存储的的,而且Scala中所有的数据类型都是对象,没有java中原生类型的概念。
    比如说 Int final abstract class Int extends _root_.scala.AnyVal 其实是一个抽象类。
    这种采用了对象存储的方式,会天然存在GC的问题
    而 UnsafeRow 采用字节数组的形式,只有单个字节数组的对象,不像SpecificInternalRow 中的的每一个字段都是可以GC的对象,这样在GC标记可达对象阶段,可以减少时间

  • 不影响正常的数据操作,减少了数据存储内存,精确计算内存的使用情况
    BaseGenericInternalRow都有对应的 setXXXgetXXX方法,用来设置或者获取对应数据类型的值,
    UnsafeRow 也有 setXXXgetXXX方法,背后是对应Unsafe.putXXXUnsafe.getXXX方法,所以说不影响正常的数据操作
    正常的jvm对象在堆中会包含对象头,实例数据,对齐填充等信息,而unsafeRow只存储对应的数据本身,节约了额外信息
    由于UnsafeRow只存储了数据,对应的占用大小就可以直接计算出来; 而jvm对象不容易计算,因为对象中的字段可能还会包含其他的引用对象

  • 减少了 Shuffle 过程中序列化和反序列化的的消耗
    在Shuffle write写磁盘阶段,会将对应的Row数据序列化,对于是 UnsafeRow 这种序列化的话,直接把当前的byte数组的数据写入就行(参考UnsafeShuffleWriter.write方法实现),用的 ShuffleExchangeExec.serializer 的序列化 ,也就是 UnsafeRowSerializer
    而不像 JavaSerializer 这种序列化,还需要重新序列化整个对象

在shuffle 读阶段,会将数据反序列化为 内部Row, 对于 UnsafeRow 的话,直接读取对应的的字节数据到 UnsafeRow 就行了(参考 ShuffleManager.getReader 实现),用的是ShuffleExchangeExec.serializer.deserializeStream.asKeyValueIterator方法.

Byte 大小端

大端模式和小端模式是指CPU对多字节数据的存储方式。
小端序(Little-Endian),将一个多位数的低位放在较小的地址处,高位放在较大的地址处,则称小端序。小端序与人类的阅读习惯相反,但更符合计算机读取内存的方式,因为CPU读取内存中的数据时,是从低地址向高地址方向进行读取的。
在Spark中 以Long的方式在读取Byte数组数据的时候,就会涉及到大小端的问题,这样才能正确读取Long类型数据,比如在ByteArray.getPrefix中就会对大端和小端分别进行处理:

private static final boolean IS_LITTLE_ENDIAN =
      ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN;
 ...
static long getPrefix(Object base, long offset, int numBytes) {
    // Since JVMs are either 4-byte aligned or 8-byte aligned, we check the size of the bytes.
    // If size is 0, just return 0.
    // If size is between 1 and 4 (inclusive), assume data is 4-byte aligned under the hood and
    // use a getInt to fetch the prefix.
    // If size is greater than 4, assume we have at least 8 bytes of data to fetch.
    // After getting the data, we use a mask to mask out data that is not part of the bytes.
    final long p;
    final long mask;
    if (numBytes >= 8) {
      p = Platform.getLong(base, offset);
      mask = 0;
    } else if (numBytes > 4) {
      p = Platform.getLong(base, offset);
      mask = (1L << (8 - numBytes) * 8) - 1;
    } else if (numBytes > 0) {
      long pRaw = Platform.getInt(base, offset);
      p = IS_LITTLE_ENDIAN ? pRaw : (pRaw << 32);
      mask = (1L << (8 - numBytes) * 8) - 1;
    } else {
      p = 0;
      mask = 0;
    }
    return (IS_LITTLE_ENDIAN ? java.lang.Long.reverseBytes(p) : p) & ~mask;
  }

对于转换的问题可以参考SparkSQL InternalRow

### Spark 堆内内存内存设置方法及参数配置 在 Spark 中,堆内内存内存的配置可以通过一系列参数来实现。这些参数控制着内存分配的比例以及动态调整的行为。 #### 配置堆内内存的关键参数 1. **`spark.executor.memory`**: 设置每个 Executor 的总内存大小(单位为 MB 或 GB)。这是整个 Executor 可用的最大内存。 2. **`spark.memory.fraction`**: 控制用于存储执行操作的内存比例,默认值为 `0.6`。这意味着 60% 的 JVM 内存会被划分为统一的内存池[^4]。 3. **`spark.memory.storageFraction`**: 表示存储数据使用的内存占上述统一内存池的比例,默认值为 `0.5`。这表示一半的统一内存池会优先保留给缓存的数据结构[^3]。 计算公式如下: - 统一内存池大小 = `(executor memory * spark.memory.fraction)` - 存储内存上限 = `(统一内存池大小 * spark.memory.storageFraction)` - 执行内存上限 = `(统一内存池大小 * (1 - spark.memory.storageFraction))` #### 配置内存的关键参数 1. **`spark.memory.offHeap.enabled`**: 启用或禁用内存功能。默认情况下是关闭的 (`false`)。如果要启用,则需将其设为 `true`[^1]。 2. **`spark.memory.offHeap.size`**: 定义每台机器上可用的内存量(单位为字节),该值应小于等于系统的物理 RAM 总量减去 JVM 使用的部分。当此选项未指定时,默认不使用任何额资源。 需要注意的是,在启用了内存之后,Executor 就能够同时利用两种类型的内存来进行工作负载处理,并且它们之间互不影响;此时总的 Execution Storage 内存容量将是两者相加的结果。 对于具体的数值设定还需要考虑实际应用场景中的需求平衡等因素: ```bash # Example configuration via command line arguments or properties file. --conf spark.executor.memory=8g \ --conf spark.memory.fraction=0.7 \ --conf spark.memory.storageFraction=0.4 \ --conf spark.memory.offHeap.enabled=true \ --conf spark.memory.offHeap.size=4g ``` 以上命令片段展示了如何通过 CLI 参数或者属性文件形式完成相关配置项定义的过程。 ### 动态内存管理机制概述 自 Spark 1.6 版本起引入了统一的内存管理模式(Unified Memory Management),它允许存储区(Storage)执行区(Execution)共享同一个逻辑上的内存区域并支持按需自动扩展彼此边界的能力。这种改进有效解决了早期版本中存在的诸多局限性问题比如固定分区容易造成浪费等情况发生。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值