本节摘要:Executor 内存被切成统一内存(执行+存储)、用户内存与系统保留三块,统一内存内部动态互借。本节给出内存份额的计算方法、缓存与溢写的行为推演、并行度与 Shuffle 参数的配对思路,并过一遍动态资源分配的触发条件。
上一节把 Executor 安置进了集群,本节打开 Executor 的 JVM 看内存布局。调参的第一原则:不知道参数动的是哪块内存,调了也是撞运气。
spark-submit --conf spark.executor.memory=8g \ --conf spark.executor.memoryOverhead=1g \ --conf spark.memory.fraction=0.6 \ --conf spark.executor.cores=4
按这份配置算账。总预算 9g(堆 8g + 堆外 overhead 1g)。堆内 8g 里,先扣 300MB 保留区,剩约 7.7g,再按 memory.fraction 切出 0.6,约 4.6g 进入统一内存——执行内存(Shuffle、join、排序的缓冲)与存储内存(缓存块)在这 4.6g 里动态互借,边界由 spark.memory.storageFraction=0.5 定初始位置但可浮动。剩下的约 3.1g 是用户内存:你的数据结构、UDF 里的对象、不经过引擎管账的一切都住这里。
| 区域 | 份额示例 | 谁在用 | 挤爆了会怎样 |
|---|---|---|---|
| 执行内存 | 4.6g 的一半起浮 | 排序缓冲、Shuffle 聚合 | 溢写磁盘,任务变慢但不死 |
| 存储内存 | 与执行互借 | cache 的块 | 缓存被逐出,重算 |
| 用户内存 | 约 3.1g | UDF 对象、自定义结构 | OOM,任务失败 |
| 堆外 overhead | 1g | 网络缓冲、堆外缓存 | 容器被 YARN 杀,Executor lost |
这张表最后一列就是排障速查表:慢而不死查执行内存,重算变多查存储,直接 OOM 查用户内存,整容器消失查 overhead——YARN 杀容器看的是进程总内存,堆没事但 overhead 涨了照样杀。
# 迭代作业缓存特征向量:MEMORY_AND_DISK 意味着装不下落盘而不是丢弃 df = spark.read.parquet("features") df.persist(StorageLevel.MEMORY_AND_DISK) # 观察手段:Spark UI Storage 页 # cached Size 列逼近统一内存的存储份额时,开始出现 disk 部分
背景案例:特征表 18g,6 个 Executor 各 4.6g 统一内存,存储理论上限约 14g。操作:直接 persist 后跑迭代。结果:第一轮 60% 的块落盘,二轮起每轮都从磁盘读一部分,迭代 20 轮总耗时 3 倍于全内存。解读:存储份额不够时 MEMORY_AND_DISK 不会失败但会慢,账面上"缓存命中"掩盖了磁盘读。变式:把序列化缓存换成 MEMORY_ONLY_SER,体积压到三分之一,全部回到内存,迭代提速 2.4 倍——压缩 5% CPU 换 60% 磁盘读,划算。
--conf spark.default.parallelism=200 # RDD 算子的默认分区数 --conf spark.sql.shuffle.partitions=200 # SQL 与 DataFrame 的 Shuffle 分区
经验起点:每个核 2 到 3 个任务,即 executor 总核数乘 2 到 3。200 分区配 80 核,每个核平均消化 2.5 个任务,末尾波次短、抗倾斜。分区过少则单任务过重且无法平摊倾斜;过多则任务调度开销与碎文件齐涨,Shuffle 读端一次要打开几百个小文件。AQE(自适应查询执行)在生产上能自动合并小分区、拆分倾斜分区,开启它相当于给这套手算上了一道保险。
--conf spark.dynamicAllocation.enabled=true \ --conf spark.shuffle.service.enabled=true # 由外部服务保管 Shuffle 文件 --conf spark.dynamicAllocation.minExecutors=5 \ --conf spark.dynamicAllocation.maxExecutors=50
上调条件:有待排队的任务且资源未到上限。回收条件:Executor 空闲超过 60 秒(可调)。回收的前提是 Shuffle 文件已经外置到辅助服务,否则杀掉 Executor 就弄丢下游还要读的中间数据——这是开启动态分配必须配套 auxService 的原因,理解了数据依赖就理解了这条铁律。
⚠️ 常见坑:把 executor.memory 一路加到 30g 单体大 Executor。GC 停顿随堆增大而拉长,4 到 8g 的中等 Executor 多放几个,通常比一个巨无霸稳。另一个坑:堆外 overhead 默认只有 10%,重 Shuffle 或堆外缓存作业必涨,否则"莫名"被 YARN 杀。
💡 关键直觉:资源调优的顺序是先让作业不死(overhead 与用户内存),再让它不慢(并行度与缓存级别),最后才谈省钱(动态分配)。顺序反了会在错误层反复打转。
资源铺好了,下一节装上仪表盘:指标怎么看、日志怎么读、故障按什么流程层层剥。