spark性能优化6:内存管理

Spark 的内存管理机制(特别是 Tungsten 项目引入的堆外内存和统一内存管理)是其高效运行的基石,而合理的参数配置和代码优化则是释放性能的关键。

下面我将从内存模型原理核心参数调优代码与算子优化 以及常见问题诊断四个方面为你深度解析。

🧠 一、Spark 内存模型深度解析

Spark 的内存管理主要针对 Executor JVM。要优化它,首先要看懂它的"钱包"是怎么划分的。

1. 内存全景图

一个 Executor 申请到的总内存(由 --executor-memory 设定,假设为 4GB)在内部被划分为多个区域,其关系大致如下:
YARN Container / 物理总上限
Spark Executor JVM 进程
非堆/系统开销
堆内内存 On-Heap
可用内存划分
扩展统一内存
内存动态争抢机制
执行内存申请逻辑
有空闲
空间不足


是 (且Storage并未锁定)

Execution 需要内存?
直接使用
Storage 区域有空闲?
借用 Storage 空闲区
Storage 占用是否 > storageFraction?
强制驱逐/Spill Storage 数据

收回借出的内存
无法驱逐 -> Spill 落盘
存储内存申请逻辑
有空闲
空间不足


Storage 需要内存?
直接使用
Execution 区域有空闲?
借用 Execution 空闲区
申请失败/LRU驱逐旧Storage块
堆外内存 Off-Heap - Tungsten
spark.memory.offHeap.size
如果开启:

也会被纳入 Unified Memory 管理

直接用于 Storage 或 Execution
Unified Memory 统一内存池
动态伸缩区
动态借用/归还
Storage Memory

存储内存
Execution Memory

执行内存
大小 = spark.memory.fraction * Usable

默认 0.6 * Usable
保护阈值: spark.memory.storageFraction

默认 0.5

在此阈值内的 Storage 数据不易被驱逐
Total Container Memory

YARN 分配上限
Overhead Memory

spark.executor.memoryOverhead

默认为: Max(384MB, heap*10%)
用于: NIO Buffer, Thread Stacks,

Netty 直接内存等
spark.executor.memory (Xmx)
Reserved Memory 预留内存

固定 300MB
Usable Memory 可用内存

(Xmx - 300MB)
User Memory 用户内存

(1 - spark.memory.fraction)

默认约 40%

用于: UDF, RDD算子内部对象
核心规则 1:

Execution 可以强制驱逐 Storage 的数据

前提是 Storage 借用了 Execution 的空间
核心规则 2:

Storage 永远无法 强制驱逐 Execution 的数据

计算任务复杂, 中断代价太大

区域 大小比例 (典型) 核心职责 调优关注点
Reserved Memory max(384MB, executor-memory * 0.1) JVM 自身开销、字符串常量池、内部对象等。用户代码不可用 通常无需调整,但设置过小的 executor-memory 会导致此部分占比过高,实际可用内存少。
User Memory (1 - spark.memory.fraction) * (Heap - Reserved) 用户代码中创建的对象、数据结构、UDF 中的变量等。OOM 频发区 优化代码,避免在算子函数中创建大量临时对象。
Spark Memory (Storage + Execution) spark.memory.fraction * (Heap - Reserved) Spark 托管的核心区域,用于缓存和计算。 调优的核心战场 ,通过 storageFraction 调节存储与计算的平衡。
Off-Heap Memory spark.memory.offHeap.size 堆外内存,由 Spark 直接管理,不经过 JVM GC。用于 Tungsten 的二进制数据、堆外排序等。 利用堆外内存可以规避 GC 停顿 ,但需配置 spark.memory.offHeap.enabled=true
2. 统一内存管理的动态借用机制

自 Spark 1.6 起,默认使用 Unified Memory Manager ,其核心思想是 "动态共享"

  • Execution Memory 可以借用 Storage Memory:当 Shuffle、Join 等计算任务需要更多内存时,Execution Memory 可以占用 Storage Memory 的空闲部分。如果 Storage Memory 中有数据被使用,则不能被回收(除非内存严重不足)。
  • Storage Memory 只能"被动"借用 Execution Memory :Storage Memory 自身的大小由 spark.storageFraction * (Storage + Execution) 定义(即一个最低保障线)。当实际存储占用低于这个线时,它可以使用 Execution Memory 的剩余空闲部分。但当 Execution Memory 需要回收这部分内存用于计算时,它会直接"抢占"。
  • 原则 :Execution Memory 的优先级高于 Storage Memory。计算任务(如 Shuffle)是"实时"的,必须保证其内存需求;而缓存的数据可以被驱逐到磁盘(MEMORY_AND_DISK)或丢弃(MEMORY_ONLY)。

💡 调优启示 :如果你的作业计算密集(Shuffle/Join 多) ,可适当调高 spark.memory.fraction(如 0.7-0.8) ,给 Execution Memory 更多空间。如果缓存密集 ,可调高 spark.storageFraction(如 0.6),保证数据不被频繁驱逐。


⚙️ 二、核心参数调优指南

基于上述模型,通过调整关键参数来优化内存使用和性能。

1. 内存比例参数

这是最直接有效的调优手段。

参数 默认值 调优建议与场景
spark.memory.fraction 0.6 计算密集型 (Shuffle 多、Join/Sort 重):调高至 0.7~0.8 ,增大 Execution 内存池,减少溢写。 缓存密集型 :保持 0.6 或略低。
spark.memory.storageFraction 0.5 需要大量缓存 (如多次迭代 ML 算法):调高至 0.6~0.7 ,为缓存提供更多保障。 极少缓存 :可调低至 0.3~0.4,将更多内存留给 Execution。
spark.memory.offHeap.enabled false 开启堆外内存 。设置为 true,并设置 spark.memory.offHeap.size(如 2GB)。用于 Tungsten,避免 GC 扫描大对象

⚠️ 注意spark.memory.fractionspark.memory.storageFraction 是乘积关系,且最终受限于 spark.memory.fraction

即,Storage可用最大内存 = spark.memory.fraction * (Heap - Reserved) * spark.storageFraction

例如,Heap=4G, Reserved=300MB, spark.memory.fraction=0.6, spark.storageFraction=0.5:
Spark Memory = (4G - 300MB) * 0.6 ≈ 2.28GB
Storage Memory (保障线) = 2.28GB * 0.5 ≈ 1.14GB (但最高可达 2.28GB)
Execution Memory (初始) = 2.28GB - 1.14GB ≈ 1.14GB (但最高可达 2.28GB)

2. JVM 垃圾回收 (GC) 优化参数

频繁的 Full GC 是 Spark 性能杀手。可以通过 Spark 的 --conf 参数传递给 JVM。

参数 说明 推荐值
spark.executor.extraJavaOptions 传递给 Executor JVM 的参数。 "-XX:+UseG1GC" (推荐 G1GC,平衡吞吐与停顿) "-XX:InitiatingHeapOccupancyPercent=45" (更早触发并发标记,降低停顿) "-XX:MaxGCPauseMillis=200" (目标最大停顿时间)
spark.yarn.executor.memoryOverhead spark.yarn.driver.memoryOverhead YARN 模式下的堆外内存预留,用于 JVM 自身开销、字符串、NIO Buffer 等。 默认为 executor-memory 的 10%,最小 384MB。如果发现频繁 Container 被杀 (由于超出物理内存),可显式调小此值,增加 executor-memory 的实际可用量。
spark.testing.memory 用于测试的内存参数,可强制设置各部分大小。 调试时使用,生产环境慎用。

💡 GC 调优目标 :减少 Full GC 频率,将停顿时间控制在几十到几百毫秒内。通过 Spark UI 的 Executors 标签页,观察 GC Time 列,如果占比很高(如 >10%),则需要优化。


🧩 三、代码与算子层面的内存优化

除了参数,编码习惯和算子选择对内存消耗影响巨大。

1. 数据序列化优化

Spark 默认使用 Java 序列化,效率低且空间占用大。

  • 使用 Kryo 序列化

    scala 复制代码
    conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
    conf.registerKryoClasses(Array(classOf[MyClass]))

    优势:序列化速度提升 10 倍,数据体积减少,减少网络和内存开销【turn0search7】。

  • 避免在 RDD 中使用未序列化的 Java 对象 :尽量使用基本类型(Int, Long, String)或 Spark 提供的 Writable 类型。

2. 缓存策略优化

cache()/persist() 是一把双刃剑,用好了提升性能,用不好则严重挤占 Execution 内存 导致 OOM。

  • 选择合适的存储级别
    • MEMORY_ONLY:只存内存,数据大时极易 OOM。
    • MEMORY_AND_DISK:推荐。内存优先,不足则溢写到磁盘,兼顾性能与安全
    • DISK_ONLY:不占内存,但性能差,几乎不用。
  • 及时释放缓存 :用完后立即 unpersist(),避免内存长期被占用。
  • 优先缓存过滤后的数据rdd.filter(...).cache() 而不是 rdd.cache().filter(...)
3. 算子选择与优化
  • 优先使用"预聚合"算子 :使用 reduceByKey / aggregateByKey 代替 groupByKey
    • 原理:groupByKey 会将所有 Key 的 Value 全部拉到内存,极易 OOM;而 reduceByKey 在 Map 端进行局部合并,大幅减少 Shuffle 数据量。
  • 避免创建大型中间集合
    • ❌ 错误:val list = rdd.collect() (在 Driver 上),或 rdd.map(...).collect() 拉取大量数据。
    • ✅ 正确:使用 rdd.foreachPartition(...) 对每个分区的数据进行迭代处理,避免一次性加载所有数据。
  • 使用广播变量 :在 Join 时,如果一张表很小(如 < 几百 MB),将其广播到所有 Executor,避免 Shuffle,显著减少网络和内存开销。
4. 利用堆外内存与 Tungsten
  • 确保 spark.sql.inMemoryColumnarStorage.enabledspark.sql.codegen.wholeStage 等参数启用(通常默认开启),这能让 Spark SQL 使用高效的 Tungsten 引擎,利用堆外内存进行编译和执行。

🔍 四、诊断与排查常见内存问题

当遇到 OOM 或性能瓶颈时,如何定位原因?

1. OOM(内存溢出)错误分析
错误类型 可能原因与排查思路
java.lang.OutOfMemoryError: Java heap space 堆内内存 OOM 。 原因:Task 处理的数据量过大、缓存过多、存在大型对象。 排查:查看 Spark UI 中该 Stage 的 Task 详情,看是哪个 Task OOM。检查是否有 collect() 大数据、数据倾斜导致单个 Task 数据量暴增。调大 --executor-memory 或优化算子。
Container killed by YARN for exceeding memory limits 物理内存超限 (堆内 + 堆外 > Container 申请上限)。 原因:spark.executor.memory + spark.yarn.executor.memoryOverhead > YARN 队列配置的 yarn.scheduler.maximum-allocation-mb。 排查:调低 spark.yarn.executor.memoryOverhead 或增大 YARN 容器内存限制。
java.lang.OutOfMemoryError: GC overhead limit exceeded GC 开销过大导致 OOM 。 原因:JVM 用于 GC 的时间或内存超限,通常与堆外内存使用有关。 排查:优化 GC 参数,或调整堆外内存设置。
2. 利用 Spark UI 诊断内存问题
  • Storage 标签页
    • 查看 RDD 的缓存大小。如果一个 RDD 缓存后占用了 50% 的内存,但实际使用率很低,说明该缓存可能浪费内存,考虑降低缓存级别或释放它。
  • Executors 标签页
    • Memory Metrics :关注 Used MemoryPeak Memory。如果接近上限,说明内存压力大。
    • GC Time :关注 GC 时间Full GC 次数。如果 GC Time 占 Task 总时间的 10% 以上,说明内存分配不合理或对象创建/销毁太频繁,需要优化代码或参数。
  • Stages 标签页
    • 点击某个耗时长的 Stage,查看其 Tasks 列表。
    • Input SizeShuffle Read/Write Size:可以帮你估算单个 Task 的数据量,判断是数据倾斜还是单纯数据量过大。

💎 总结:Spark 内存优化最佳实践清单

  1. 资源配置
    • 根据数据量计算类型 设置合理的 --executor-memory(如 8G-16G),避免过大(GC 压力)或过小(资源浪费)。
    • 根据集群总核心数设置合适的 --num-executors,确保 num-executors * executor-cores 充分利用集群 CPU。
  2. 参数调优
    • 开启 Kryospark.serializer=org.apache.spark.serializer.KryoSerializer
    • 启用堆外内存spark.memory.offHeap.enabled=true
    • 动态调整内存比例 :计算密集型调高 spark.memory.fraction;缓存密集型调高 spark.storageFraction
  3. 代码优化
    • 减少对象创建 :在算子函数中复用对象,避免在循环中 new
    • 使用高效算子reduceByKey 代替 groupByKey
    • 明智地使用缓存 :选择合适的 StorageLevel,并及时 unpersist()
    • 优先广播小表:避免不必要的 Shuffle。
  4. 持续监控
    • 频繁查看 Spark UI:关注 Executor 的内存使用率、GC 时间和 Task 的 Shuffle 读写量。
    • 根据监控数据动态调整:没有"一劳永逸"的配置,随着数据量和逻辑的变化,持续调优是关键。

💡 建议 :Spark 的内存管理是一个动态平衡的过程 。它需要在减少溢写/磁盘 I/O(需要更多内存) 和 **减少 GC 压力(需要更少对象、更少内存)**之间找到最佳平衡点。理解其背后的原理,能让你在调优时更有方向感,而不是盲目修改参数。

相关推荐
雷电法拉珑2 小时前
五日量价金二维分析
大数据
AI营销实验室2 小时前
2025年AI CRM系统前瞻:原圈科技智能线索分配机制解析
大数据·人工智能
Hello.Reader2 小时前
Flink ML 数据类型:Vector 向量类型入门(DenseVector / SparseVector)
大数据·flink
aigcapi9 小时前
RAG 系统的黑盒测试:从算法对齐视角解析 GEO 优化的技术指标体系
大数据·人工智能·算法
cui178756810 小时前
排队免单模式深度拆解:闭环逻辑、裂变内核与落地法则
大数据
热爱专研AI的学妹11 小时前
数眼搜索API与博查技术特性深度对比:实时性与数据完整性的核心差异
大数据·开发语言·数据库·人工智能·python
可可南木12 小时前
ICT测试日志 --5--日志记录的格式 下
功能测试·测试工具·pcb工艺
方向研究12 小时前
管仲治国
大数据
成长之路51412 小时前
【实证分析】数据资产信息披露程度数据集-含原始数据及do代码(2007-2024年)
大数据