Spark配置参数详解:spark-defaults.conf核心参数、动态资源分配、序列化与内存模型配置

说实话,很多Spark跑得慢,不是代码写得烂,是配置压根没调过。我见过太多团队,集群搭好以后就扔在那,默认配置一用到底。结果呢?数据量一上来,OOM、GC频繁、任务卡死,全来了。

这一章,咱们就把配置这件事彻底讲透。我会从spark-defaults.conf的核心参数说起,再到动态资源分配、序列化选型,最后把内存模型掰开揉碎。你跟着走一遍,至少能解决80%的配置问题。

spark-defaults.conf:你的第一道防线

这个文件是Spark启动时自动加载的默认配置。说白了,你在这里写好的参数,每个任务都会继承。我个人习惯是把它放在$SPARK_HOME/conf/目录下,集群里所有节点保持一致。

先看几个最关键的参数:

参数名 默认值 我的建议值 说明
spark.executor.memory 1g 4g-8g 每个Executor的堆内存,别给太少,也别超过节点物理内存的75%
spark.executor.cores 1 2-4 每个Executor的CPU核数,太多会导致HDFS I/O瓶颈
spark.driver.memory 1g 2g-8g Driver内存,collect操作多的话一定要加
spark.sql.shuffle.partitions 200 根据数据量动态设 Shuffle后的分区数,200是通用值,大表建议500-1000
spark.default.parallelism 取决于集群 Executor数×cores×2~3 RDD操作的默认并行度,设太少了任务跑不满
我的经验: 我曾经接手过一个项目,Executor内存设了32g,结果GC停顿动不动就十几秒。后来发现是堆太大,Full GC频繁。我建议单Executor内存不要超过8g,不够就多加几个Executor,别堆在一个进程里。

动态资源分配:别让资源闲着

默认情况下,Spark会一次性申请你指定的所有资源。但很多时候,任务有波峰波谷。比如凌晨跑批的时候负载高,白天查询少。这时候动态资源分配就派上用场了。

开启方式很简单:

spark.dynamicAllocation.enabled           true
spark.dynamicAllocation.minExecutors      1
spark.dynamicAllocation.maxExecutors      50
spark.dynamicAllocation.initialExecutors  5
spark.shuffle.service.enabled             true

这里有个坑——spark.shuffle.service.enabled必须设为true。为什么?因为Executor动态增减时,Shuffle数据需要有个外部服务来管理,不然数据就丢了。我记得有一次没开这个,结果Executor缩容后,后续Stage死活找不到Shuffle输出,排查了半天。

注意: 动态资源分配在YARN模式下效果最好。如果你用的是Standalone模式,需要额外配置External Shuffle Service。别问我怎么知道的——踩过坑。

序列化:选对方式,性能翻倍

Spark里数据要在网络间传输,要落盘,要缓存,这些都离不开序列化。默认用的是Java序列化,但说实话,它又慢又占空间。

我强烈建议换成Kryo序列化:

spark.serializer                 org.apache.spark.serializer.KryoSerializer
spark.kryo.registrationRequired true
spark.kryo.referenceTracking    false
spark.kryoserializer.buffer.max 256m

你可能会问:为什么要注册类?因为Kryo默认不记录类名,只记录ID,这样更省空间。但如果你不注册,它就得用全类名字符串,效果大打折扣。

注册方式也很简单:

// 在Driver端注册
val conf = new SparkConf()
  .registerKryoClasses(Array(
    classOf[MyClass1],
    classOf[MyClass2]
  ))
性能对比: 我做过测试,Kryo序列化比Java序列化快10倍以上,数据体积缩小3-5倍。尤其是在Shuffle数据量大的场景下,效果立竿见影。

内存模型:别再让OOM追着你跑

Spark的内存管理,很多人觉得复杂。其实说白了,就两大块:执行内存和存储内存。执行内存用来做Shuffle、Join、Aggregation这些计算操作;存储内存用来缓存RDD、DataFrame。

默认情况下,Spark把堆内存的60%分给这两块,剩下的40%留给用户代码和元数据。这60%里,执行和存储各占一半,但可以互相借用。

我画了一张图,帮你理清这个结构:

Spark 堆内存模型 JVM 堆内存 (spark.executor.memory) 预留内存 (300MB, 保护JVM) 用户内存 (40%) — 用户代码、UDF、元数据 默认: (总内存 - 预留) × 0.4 统一内存 (60%) — 执行 + 存储 默认: (总内存 - 预留) × 0.6 spark.memory.fraction 控制比例 执行内存 (Execution) Shuffle、Join、Aggregation spark.memory.storageFraction = 0.5 存储内存 (Storage) 缓存RDD、DataFrame、Broadcast 可被执行内存借用 互相借用 核心参数: spark.memory.fraction (默认0.6) | spark.memory.storageFraction (默认0.5)

这里有个关键点:执行内存可以抢占存储内存。如果你的任务里Cache了很多数据,但突然来了一个大的Shuffle操作,执行内存不够了,它会从存储内存里抢。抢走的块会被驱逐(Evict),如果被驱逐的块还没计算完,就得重新算。

避坑指南: 我曾经遇到一个场景,Cache了大量的中间结果,结果后面一个Join操作把Cache全踢出去了,导致后续Stage反复重算。后来我把spark.memory.storageFraction从0.5调到了0.7,给存储内存留了更多空间,问题就解决了。

几个实战调优建议

说了这么多,我总结几条立竿见影的配置:

  • Executor内存别贪大:4-8g最合适,超过8g考虑多加Executor
  • Kryo一定要开:注册好类,性能提升肉眼可见
  • 动态资源分配配External Shuffle Service:缺一不可
  • Shuffle分区数别死用200:大表建议用spark.sql.adaptive.coalescePartitions.enabled=true让Spark自动调整
  • 内存比例根据场景调:Cache多就提高storageFraction,计算多就降低
一个小技巧: 你可以在Spark UI的"Environment"标签页里看到所有生效的配置。我每次调优完都会去那里确认一下,防止配置没生效。