1. Executor内存逻辑架构
- 堆内存,由JVM分配和回收,由spark.executor.memory控制大小,JVM中序列化的对象是以字节流形式,其占用内存大小可直接计算,对于非序列化对象,其占用的内存是通过周期性地采样近似估算,且被spark标记为释放的对象实例也有可能并没有被JVM回收,所以spark并不能准确记录实际可用堆内存,也就无法避免内存溢出
- 非堆内存,不受JVM管理,有两部分,其中一部分通常是yarn模式中通过spark.executor.memoryOverhead配置,该部分内存用于虚拟机自身的开销(字符串、NIO和其它一些本地开销);另一部分通过spark.memory.offHeap.enable/size结合配置,该部分由spark直接使用于存储内存和任务内存,从2.0开始不再依赖第三方内存系统Tachyon,而是基于JDK自带的Unsafe API实现堆外内存管理,堆外内存可以精确地申请和释放,减少了不必要的额外开销。
- 系统内存(systemMemory):这里指的是JVM可用的最大内存,可通过Runtime.getRuntime.maxMemory获得该值,系统内存并不等于分配的堆内存,由于年轻代GC采用复制算法,所以有一块survivor内存区需要保留,即
systemMemory=堆内存-survivor
- 可用内存(usableMemory):这部分内存是用户代码能直接影响到的,
可用内存=系统内存 - Reserved
,其中Reverved为固定300M的保留内存,用于spark系统内部使用。 - 应用内存:主要用于存储用户代码生成的数据对象,这些数据对象被缓存之前就是处于应用内存空间
- 存储内存与执行内存:存储内存用于缓存数据,执行内存主要用于满足 Shuffle、 Join、 Sort、 Aggregation 等计算过程中对内存的需求,通过spark.memory.storageFraction控制两者比例,默认平分,两部分内存之间还可以进行动态占用:
- 执行内存的空间被对方占用后,可让对方将占用的部分转存到硬盘,然后归还借用空间
- 存储内存空间被对应占用后,无法让对方归还,因为shuffle过程中的很多因素无法实现
2. Executor 界面内存计算
在spark任务监控界面的Executors菜单中有一列“Storage Memory”显示当前“已用/总可用存储内存”情况,当使用静态内存机制时,此列总大小确实为存储内存,但使用统一内存机制时:
显示总内存 = 存储内存+执行内存
虽然两部分内存可以动态占用,但应该总不会把执行内存全部借光。此处分析统一内存该值是怎么个计算方式,既然知道显示的总内存是指存储内存和执行内存总和,那么结合逻辑架构图可以直观地看到组成方式:
显示总内存 =(堆内存储内存+堆内执行内存)+(堆外存储内存+堆外执行内存)
=(可用内存-应用内存)+ offHeap
= 可用内存 * spark.memory.fraction + offHeap
=(系统内存-300M)* 0.6 + offHeap
=(spark.executor.memory – survivor – 300M)* 0.6 + offHeap
=(Runtime.getRuntime.maxMemory – 300M)* 0.6 + offHeap
这公式中survivor内存大小是核心,如果确定了就能确定总内存大小。按理解,可以通过NewRatio和SurvivorRatio计算出survivor大小的,但是通常指定了最大堆内存,但是jvm并不会初始化时就会申请到最大内存,则是动态增加的,所以survivor大小也只是一个估计值(约为90%)。可以通过指定Xms等于最大堆内存或者禁用UseAdaptiveSizePolicy,这样survivor就可以按比例计算出来。
另外需要注意一点,页面是通过请求”allexecutors”接口返回的数据,总内存对应的返回字段是maxMemory,单位是字节,而展现为GB时直接除1000而非1024,且最终结果小数位是五舍四入。
3. UnrollMemory理解
spark的rdd在缓存到存储内存之前,每条数据的对象实例都处于JVM进堆内内存的应用内存,即便同一个分区内的数据在内存空间也不是连续的(更可能不在同一物理节点?),具体分布由JVM管理,上层通过scala中的迭代器来访问。当rdd持久化储存内存之后,partition对应转换为block,此时数据在存储内存空间(堆内或堆外)中将连续的存储,这里将分区由不连续的存储空间转换为连续的存储空间的过程,就是unroll操作
在静态内存机制中,内存的构成部分有一个叫unroll内存,该部分从存储内存中独立划分 约占20%。但是在统一内存机制中,unroll内存却不存在了,其实在spark抽象的内存管理器(MemoryManger)中抽象了三个方法:
acquireStorageMemory
acquireUnrollMemory
acquireExecutionMemory
无论是统一内存(UnifiedMemoryManager)还是静态内存(StaticMemoryManager)按自己的逻辑实现了3个方法,统一内存中已经把存储内存和unroll内存合并,代码实现层面上UnifiedMemoryManager.acquireUnrollMemory
其实也只是简单地调用了一下acquireStorageMemory
4. 参考
《spark sql内核剖析》