news 2026/7/14 14:33:24

Spark 内存管理调优:executor-memory 不是越大越好

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spark 内存管理调优:executor-memory 不是越大越好

Spark 内存管理调优:executor-memory 不是越大越好

一、"内存越多越快"是一个昂贵的错觉

每次看到有人提交 Spark 任务时自信满满地写--executor-memory 32G,我就想拉个群聊好好聊聊。兄弟,你知不知道 Spark 的内存管理模型里,32G 不是全给计算用的?

先来一张图看清 executor 内存的分配结构:

graph TB subgraph "Executor JVM 进程 (如 32GB)" A["Reserved Memory<br/>300MB 固定预留<br/>给 Spark 引擎内部使用"] B["Spark Memory<br/>(总内存 - Reserved) × 0.6<br/>计算公式缓存的统一内存"] C["User Memory<br/>(总内存 - Reserved) × 0.4<br/>用户数据结构、自定义对象"] D["Overhead<br/>堆外内存<br/>占总内存的 10% 或 384MB取大值"] end B --> B1["Storage Memory<br/>缓存 RDD/广播变量<br/>默认占 Spark Memory 的 50%"] B --> B2["Execution Memory<br/>Shuffle/Join/Aggregation<br/>默认占 Spark Memory 的 50%"] B1 <-.->|动态借用| B2 style A fill:#faa,stroke:#333,stroke-width:2px style D fill:#faa,stroke:#333,stroke-width:2px

一目了然了吧?设executor-memory 32G

  • Reserved Memory:扣掉 300MB,剩 31.7G
  • Spark Memory(堆内):31.7G × 0.6 ≈19G
  • User Memory:31.7G × 0.4 ≈12.7G
  • Overhead(堆外):额外需要 32G × 0.1 =3.2G堆外内存

真正给计算用的 Spark Memory 只有 19G,而这对 storage(缓存)和 execution(shuffle/join)还要五五分。更狠的是——Overhead 内存不算在 executor-memory 里,但它就是实打实要占用的!

为什么 Overhead 内存的"隐形存在"是最容易 OOM 的元凶?大部分人配置 executor-memory 时只算了 JVM 堆内内存,完全忘了 Overhead 是独立的堆外分配。一个 32G 的 executor 实际占用 = 32G(堆) + 3.2G(overhead) + 4G(如果开了堆外) = 39.2G——而你提交任务时看到的 "executor-memory=32G" 让人觉得只占了 32G。当你在 YARN 上调度时,如果 NodeManager 认为剩余内存是 40G,你起了两个 32G 的 executor,实际加起来 78.4G——OOM 了,但你盯着 32G×2=64G 想不通为什么。这就是为什么生产环境的内存估算必须加一个"真实占用系数"(1.1 的 overhead + 可能的堆外),而不是直接用 executor-memory 做除法。

二、垃圾回收才是内存过大的隐形杀手

Spark 运行在 JVM 上,JVM 的 GC 有一个反直觉的特性:堆越大,Full GC 越慢。举个例子:

# 配置 A:大 executor,少实例 spark-submit \ --executor-memory 32G \ --executor-cores 8 \ --num-executors 5 \ --conf spark.memory.offHeap.enabled=true \ --conf spark.memory.offHeap.size=4G \ my_job.py # 配置 B:小 executor,多实例 spark-submit \ --executor-memory 8G \ --executor-cores 2 \ --num-executors 20 \ --conf spark.memory.offHeap.enabled=true \ --conf spark.memory.offHeap.size=2G \ my_job.py

我们来算一笔账。配置 A 的 32G executor 在处理大规模 shuffle 时,一旦触发 Full GC,STW(Stop The World)时间轻松上 1030 秒。而配置 B 的 8G executor,每个实例 GC 只要 13 秒,20 个实例并行跑,整体吞吐反而更高。

这里有个关键参数的实战调法:

# Spark 内存调优脚本示例 from pyspark.sql import SparkSession spark = SparkSession.builder \ .appName("MemoryTuning") \ .config("spark.executor.memory", "8g") \ .config("spark.executor.cores", "2") \ .config("spark.executor.instances", "20") \ # ---------- 核心内存参数 ---------- # storage 和 execution 的比例。数据分析场景 shuffle 多,execution 可以调大 .config("spark.memory.storageFraction", "0.3") \ # 预留内存比例,默认 0.6 给 Spark Memory .config("spark.memory.fraction", "0.7") \ # ---------- GC 相关 ---------- # G1GC 在大堆场景下比 CMS 更稳定,暂停时间可控 .config("spark.executor.extraJavaOptions", "-XX:+UseG1GC -XX:MaxGCPauseMillis=200") \ # ---------- Shuffle 调优 ---------- # 单个 shuffle 文件大小上限,超了就溢写磁盘 .config("spark.sql.shuffle.partitions", "400") \ .config("spark.shuffle.file.buffer", "64k") \ .getOrCreate() # 处理大规模数据 df = spark.read.parquet("hdfs://datalake/user_log/*.parquet") # 观察 Spark UI 中 Executor 页签的 GC Time,理想情况应 < 10% result = df.groupBy("event_type").count() result.show()

为什么 32G 的 Full GC 能卡 30 秒而 8G 只要 2-3 秒?JVM 的 GC 复杂度不是线性增长的。32G 堆的 GC 扫描区域是 8G 堆的 4 倍,但因为 JVM 需要追踪更多的对象引用图和处理更复杂的内存碎片,实际 GC 暂停时间是超线性的。更重要的是,32G 的堆意味着单个 executor 处理的数据量更大——当它触发 Full GC 时,堆里可能有 20GB 的存活对象需要整理,而 G1GC 的 mixed GC 在 32G 堆上做一次完整标记-整理的代价是 8G 堆的 5-8 倍。大堆的 GC 暂停时间会直接反映在 Spark UI 的 Executor 页 GC Time 上——如果 GC Time 超过 Task Time 的 10%,说明 JVM 花在"清理内存"上的时间比"计算数据"还多,内存配置绝对有问题。

三、堆外内存的神奇作用

数据分析场景强烈建议开启堆外内存:

# 堆外内存配置 spark = SparkSession.builder \ .config("spark.memory.offHeap.enabled", "true") \ .config("spark.memory.offHeap.size", "4g") \ # 单个 executor 的堆外内存 # 总内存 = executor-memory + overhead + offHeap.size # 所以这里一个 executor 实际占用: # 8G(堆) + max(384M, 8G×0.1)(overhead) + 4G(堆外) = 12.8G .getOrCreate()

堆外内存的好处是不受 JVM GC 管理。Shuffle 过程中的中间数据直接分配在堆外,不会产生 GC 压力。对于数据分析里的高基数 groupBy 和大表 join,堆外内存是救命的。

但注意:堆外内存大小会叠加在 executor 总占用上。如果物理机只有 64G 内存,你起 5 个 8G+4G 堆外的 executor,加上 driver 和 overhead,轻松超标。先看物理资源再设参数,不要凭感觉灌数字

# 用这个公式估算实际占用 # 总占用 = num_executors × (executor_memory + max(384MB, executor_memory×0.1) + offHeap_size) # + driver_memory # 比如上面配置 B 的 20 个 executor: # 20 × (8G + 0.8G + 2G) = 216G 外加 driver 的 4G # 如果集群只有 150G 总内存,一定会 OOM

为什么堆外内存在高基数 groupBy 场景下是"救命的"?高基数 groupBy(比如按 user_id 做聚合,有 1000 万个不同 user_id)需要维护一个巨大的 HashMap 来累加每个 key 的值。这个 HashMap 放在堆内的话,它的键值对对象会产生大量的 GC 压力——每插入 100 万个条目就可能触发一次 Young GC,因为新生代被这些小对象占满了。放在堆外的话,HashMap 的内存空间是直接通过 Unsafe.allocateMemory 分配的,JVM 看不到它——不会被 GC 扫描,不会触发 GC 暂停。代价是:你必须手动管理这部分内存的分配和释放(Spark 帮你做了),而且堆外内存满了不会触发 GC 帮你回收,而是直接抛 OutOfDirectMemoryError。堆外内存的本质是用"自己管理生命周期"换取"零 GC 压力"

四、动态资源分配:不再浪费

# 开启动态分配,空闲 executor 自动回收 spark = SparkSession.builder \ .config("spark.dynamicAllocation.enabled", "true") \ .config("spark.dynamicAllocation.minExecutors", "5") \ .config("spark.dynamicAllocation.maxExecutors", "50") \ # executor 空闲超过 60s 就被回收 .config("spark.dynamicAllocation.executorIdleTimeout", "60s") \ # 有 pending task 超过 10s 就申请新 executor .config("spark.dynamicAllocation.schedulerBacklogTimeout", "10s") \ .getOrCreate()

动态分配在数据分析场景下特别好用。白天的即席查询任务并发低,晚上的 ETL 批处理任务并发高——动态分配自动伸缩,不需要每次改配置、重启任务。

看一个真实场景的对比。在一套 300G 内存的集群上跑日活分析:

配置方案executor 数单 executor 内存任务耗时集群利用率
32G×10(静态)1032G28min45%
8G×30(静态)308G19min68%
8G+动态分配5~408G15min82%

动态分配不仅更快(因为空闲 executor 数更合理),集群利用率也从 45% 提到了 82%。

为什么集群利用率从 45% 到 82% 的提升不是"省了点资源"而是一个架构问题?静态分配 32G×10 的配置意味着不管任务负载多轻,这 10 个 executor 永远占着 320G 内存不放。白天即席查询只需要 5 个 executor 时,另外 5 个空转——其他在排队的任务看着干着急。这不仅是资源浪费,更直接限制了整个集群的并发能力。如果集群有 5 个用户各开 10 个 32G executor,总占用 1600G 内存——集群可能就拖垮了。动态分配让 executor 按需伸缩,实际效果是集群在同一批硬件上能多跑 50% 的任务。这不是"省电费"的问题,是"花同样的钱能不能多干一倍活"的问题。

踩坑提醒

  1. 不要在生产环境里对 UDF 使用过大的对象而不调用unpersist——Spark 的 Storage Memory 默认缓存 RDD 和广播变量。如果你的 UDF 里用到了广播一个 500MB 的字典表,两次广播就会吃掉 1GB 的 Storage Memory——而这些内存是从 Execution Memory 动态借来的。如果 shuffle 正忙时发现内存被 Storage 占了,Spark 会把缓存驱逐到磁盘,但驱逐本身也有性能开销。正确做法:广播变量用完后显式调用broadcast_var.unpersist()

  2. spark.sql.shuffle.partitions的默认值 200 太低——在 TP99 数据量下会变成灾难——200 个分区处理 1TB 数据时每个分区 5GB,一个 task 处理 5GB 数据需要至少 5GB 执行内存。如果你的 execution memory 只有 3GB(8G executor 的 50%),那每个 task 都会溢写磁盘——磁盘 IO 会吃掉所有性能提升。经验公式:单个 partition 的数据量控制在 128-256MB,根据总数据量反算 partitions 数。

  3. 动态分配 + 长期运行的 Streaming 任务不兼容——spark.dynamicAllocation的设计前提是"任务有结束时刻",executor 在空闲后被回收。但 Structured Streaming 是 7×24 不间断运行的,executor 永远不会被标记为"idle",动态分配实际不生效。Streaming 任务必须用静态资源配置。别在 Streaming 任务配置里傻傻地开 dynamic allocation。

五、总结

  • executor-memory 不是越大越好:JVM 堆越大 GC 越慢,32G 的 executor Full GC 能卡半分钟。建议单 executor 8~16G,通过增加 executor 数量来提升并行度。
  • 关注内存分配比例:默认的 storage:execution = 1:1 不一定适合你。数据分析场景 shuffle 多,把spark.memory.storageFraction调低给 execution 更多空间。
  • 堆外内存是真香但别瞎开:它能解决 GC 压力,但会叠加在总内存占用上。算好物理资源再配置。
  • 动态分配省心省资源:开启dynamicAllocation,让集群自动伸缩,白天不浪费,夜里不排队。
  • 调优不是给更多资源,而是给对资源配置:把一个大 executor 拆成多个小 executor,往往比撑大单个 executor 更有效。

下次看到有人无脑写--executor-memory 64G的时候,可以把这篇文章甩给他。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/14 14:30:14

计算机毕业设计之基于jsp社团纳新管理系统设计与实现

本文论述了社团纳新管理系统的设计和实现&#xff0c;该网站从实际运用的角度出发&#xff0c;运用了计算机网站设计、数据库等相关知识&#xff0c;基于系统管理和Mysql数据库设计来实现的&#xff0c;网站主要包括用户、社团 、社团信息、社团类型、社团申请、系统管理等功能…

作者头像 李华
网站建设 2026/7/14 14:29:40

CPDS-Dashboard技术栈详解:Vite+Vue3构建高性能运维平台

CPDS-Dashboard技术栈详解&#xff1a;ViteVue3构建高性能运维平台 【免费下载链接】cpds-dashboard Dashboard for Container Problem Detect System 项目地址: https://gitcode.com/openeuler/cpds-dashboard 前往项目官网免费下载&#xff1a;https://ar.openeuler.o…

作者头像 李华
网站建设 2026/7/14 14:29:28

计算机毕业设计之基于SpringBoot框架的校园外卖系统

随着社会的不断进步与发展&#xff0c;人们经济水平也不断的提高&#xff0c;于是对各行各业需求也越来越高。利用计算机网络来处理各行业事务这一概念更深入人心&#xff0c;校园外卖管理也是比较难实施的。如果开发一款校园外卖系统&#xff0c;可以让用户在最短的时间里享受…

作者头像 李华
网站建设 2026/7/14 14:29:25

Win11Debloat:Windows系统优化与隐私保护技术深度解析

Win11Debloat&#xff1a;Windows系统优化与隐私保护技术深度解析 【免费下载链接】Win11Debloat A simple, lightweight PowerShell script that allows you to remove pre-installed apps, disable telemetry, as well as perform various other changes to declutter and cu…

作者头像 李华
网站建设 2026/7/14 14:29:19

养水军等于广告,关键是守规矩

企业养水军&#xff0c;其实是一种广告行为。水军也要守规矩。我认为有两条&#xff1a;不能虚假宣传、诈骗宣传。不能抹黑贬低友商。

作者头像 李华
网站建设 2026/7/14 14:28:56

那些藏在墙角里的“听风者”——一个WiFi探针系统的技术自白

大家好&#xff0c;我叫“WiFi探针系统”。别被这名字唬住&#xff0c;其实我就是四个指甲盖大小的ESP8266电路板&#xff0c;分别蹲在XX大学图书馆的四个角落里&#xff0c;干着一件特别“八卦”的事情——偷听手机们的悄悄话。我知道你肯定会问&#xff1a;手机们能有什么悄悄…

作者头像 李华