1. Spark内存模型概述:Executor内存布局、堆内与堆外内存、内存管理架构演进

做Spark调优这么多年,我始终觉得内存模型是绕不开的第一道坎。很多人上来就调参数,结果越调越乱。说白了,你不搞清楚内存怎么划分的,调优就是瞎蒙。

这一节,我们先把Spark的内存布局讲透。我会结合自己踩过的坑,帮你把这块硬骨头啃下来。

1.1 Executor内存布局:一张图看懂

先看整体结构。一个Executor进程启动后,JVM堆内存被划分为几个区域。我习惯用这张图来跟团队新人讲,今天也分享给你。

核心要点:Spark内存管理的核心是统一内存管理(Unified Memory Management),它将堆内存分为存储内存(Storage Memory)和执行内存(Execution Memory),两者可以互相借用。

Spark Executor 内存布局 JVM 堆内内存(On-Heap Memory) Reserved Memory 300MB(固定) User Memory (spark.memory.fraction) 的剩余 Unified Memory(统一内存) spark.memory.fraction = 0.6(默认) Storage Memory spark.memory.storageFraction Execution Memory Shuffle、Join、Agg 堆外内存(Off-Heap Memory) spark.memory.offHeap.enabled=true + spark.memory.offHeap.size 存储内存 + 执行内存(统一管理,不受JVM GC影响) 可启用 关键参数速查 • spark.memory.fraction:统一内存占堆内存比例(默认0.6) • spark.memory.storageFraction:存储内存占统一内存比例(默认0.5) • spark.memory.offHeap.enabled:是否启用堆外内存(默认false)

1.2 堆内内存(On-Heap)详解

堆内内存就是JVM的堆。Spark把这块空间切成了四块,每块都有自己的使命。

1.2.1 Reserved Memory(保留内存)

这块是硬性保留的,固定300MB。用来存Spark内部对象、元数据。你调不了,也省不下来。我曾经见过有人把executor内存设成512MB,结果保留内存就占了300MB,剩下的空间根本跑不动任务。嗯,这是个坑。

避坑指南:Executor内存建议至少1GB。低于1GB时,保留内存占比过高,留给业务的内存所剩无几。我曾经在测试环境踩过这个坑,一个简单的count操作都OOM。

1.2.2 User Memory(用户内存)

这块存的是用户代码里new出来的对象、UDF里的临时变量、以及RDD的元数据。计算公式很简单:

User Memory = (JVM Heap - Reserved Memory) × (1 - spark.memory.fraction)

举个例子:Executor设4GB,spark.memory.fraction=0.6,那么:

User Memory = (4096 - 300) × 0.4 ≈ 1518 MB

这1.5GB就是给你写代码用的。如果你的UDF里new了一个大数组,或者collect了一个大结果集,都从这里出。

1.2.3 Unified Memory(统一内存)

这是Spark内存管理的核心。它又分成两块:

  • Storage Memory(存储内存):缓存RDD、Broadcast变量、累加器数据
  • Execution Memory(执行内存):Shuffle读写、Join、聚合、排序等计算过程

这两块可以互相借用。什么意思?你想想看,如果缓存占了很多空间,但突然来了一个大的Shuffle操作,执行内存不够了,它可以去抢存储内存的地盘。反过来也一样。

个人经验:我习惯把spark.memory.storageFraction设为0.5,让存储和执行各占一半。但如果你的作业以Shuffle为主(比如大表Join),我会把执行内存比例调高到0.6甚至0.7。反过来,如果大量用Cache,就调低到0.4。

1.3 堆外内存(Off-Heap)

堆外内存是JVM堆之外的一块空间,通过spark.memory.offHeap.enabled=true开启。它有什么好处?

  • 不受GC影响:堆内内存频繁GC会导致STW(Stop The World),堆外没有这个问题
  • 存储和执行统一管理:和堆内的Unified Memory一样,堆外也分Storage和Execution
  • 减少序列化开销:数据可以直接以二进制形式存在堆外

但别急着开。堆外内存需要手动管理,用不好反而更慢。我个人的建议是:

什么时候开堆外?当你的作业GC时间超过10%,或者堆内存频繁OOM时,可以考虑开启。但一定要配合spark.memory.offHeap.size设置大小,默认是0,不开等于没用。

1.4 内存管理架构演进

Spark的内存管理经历了三个阶段。我按时间线给你捋一捋。

版本 管理方式 特点 我的评价
Spark 1.0 - 1.5 Static Memory Manager 存储和执行内存固定划分,不能互相借用 太死板,容易浪费
Spark 1.6+ Unified Memory Manager 存储和执行可以互相借用,动态调整 目前主流,够用
Spark 3.0+ 增强版Unified + Off-Heap 支持堆外统一内存管理,Packed Record Columnar Batch 性能更好,但配置复杂

1.4.1 Static Memory Manager(静态内存管理)

早期版本。存储内存和执行内存的边界是死的。比如你设了存储内存2GB,执行内存2GB。如果存储只用1GB,执行却需要3GB,那执行只能干瞪眼,因为存储那1GB空着也不给你用。说白了,浪费严重。

1.4.2 Unified Memory Manager(统一内存管理)

Spark 1.6引入,一直用到现在。核心思想就是:存储和执行可以互相借。怎么借?有规则:

  • 执行内存可以向存储内存借,但存储内存要回来的时候,执行必须还
  • 存储内存可以向执行内存借,但执行要回来的时候,存储必须把数据写到磁盘

这个设计很聪明。执行内存优先级更高,因为它直接影响任务能不能跑完。存储内存被借走后,大不了把缓存的数据写到磁盘,下次用再读回来。

一个小技巧:如果你发现作业频繁把缓存数据写到磁盘(看日志里的"Evicting block"),说明存储内存经常被执行内存抢占。这时候可以适当调大spark.memory.storageFraction,给存储多留点地盘。

1.4.3 增强版Unified + Off-Heap(Spark 3.0+)

Spark 3.0在统一内存管理的基础上,把堆外内存也纳入了统一管理。以前堆外只能存一些零散的东西,现在存储和执行都可以用堆外。配合Packed Record Columnar Batch,对列式存储和向量化计算有奇效。

我记得有一次帮一个做实时数仓的团队调优,他们的作业GC时间占了30%。开启堆外内存后,GC时间降到了5%以下,整体性能提升了40%。当然,前提是堆外大小要算准,不然反而会OOM。

1.5 本章小结

这一节我们把Spark的内存模型拆开了。记住几个关键点:

  • Executor内存分堆内和堆外,堆内又分保留、用户、统一三块
  • 统一内存是核心,存储和执行可以互相借用
  • 堆外内存适合GC严重的场景,但配置要谨慎
  • 内存管理从静态到统一再到堆外统一,越来越灵活