3. 资源调优(上):Executor核心数与内存的黄金比例、YARN队列资源规划、动态资源分配实战

资源调优,说白了就是给Spark作业分配合适的“人”和“粮”。人不够,活干得慢;粮不够,人饿着肚子干,还容易出问题。我见过太多团队,代码写得挺漂亮,结果资源配得一塌糊涂,跑个几T的数据,要么OOM,要么CPU利用率不到20%。

这一章,咱们就聊聊资源调优最核心的三个问题:Executor到底配多少核心、多少内存才算合理?YARN队列怎么规划才能不打架?动态资源分配到底怎么用才不翻车?

核心观点:资源调优不是“越大越好”,而是“刚刚好”。过犹不及,配多了浪费集群资源,配少了作业跑不动。

资源调优核心知识体系 资源调优核心知识体系 资源调优 Executor核心与内存 YARN队列资源规划 动态资源分配 核心数 内存分配 队列容量 用户限制 优先级 最小/最大 黄金比例:1个核心配4-8G内存 YARN队列:容量调度器 + 动态资源分配

3.1 Executor核心数与内存的黄金比例

先问个问题:一个Executor配多少核心、多少内存最合适?

我个人的习惯是,1个核心配4-8G内存。这个比例不是拍脑袋定的,背后有原因。

你想想看,Spark的每个Task本质上是一个线程。一个Executor能并行跑多少个Task,取决于它有多少个核心。如果核心数太多,比如一个Executor配了32个核心,那它就要同时跑32个Task。每个Task都需要自己的内存来存数据、做计算。内存不够,就会频繁GC,甚至OOM。

反过来,如果核心数太少,比如一个Executor只配1个核心,那并行度就上不去,白白浪费了机器资源。

经验之谈:我建议Executor的核心数控制在3-5个之间。少于3个,资源利用率低;多于5个,内存竞争激烈,GC开销大。

那内存怎么分?一个Executor的内存主要分为三块:

  • 执行内存(Execution Memory):用于shuffle、join、aggregation等操作。这部分是干活的主力。
  • 存储内存(Storage Memory):用于缓存RDD、broadcast变量。如果缓存数据多,这块要留大一点。
  • 用户内存(User Memory):用于UDF、自定义数据结构等。一般占20%左右。

默认情况下,Spark把堆内内存的60%分给执行+存储(两者可互相借用),40%留给用户内存。我个人觉得这个比例在大多数场景下是合理的,但如果你用了大量的broadcast变量,可以调高存储内存的比例。

举个例子,假设你给一个Executor配了5个核心、20G内存:

spark.executor.cores=5
spark.executor.memory=20g
spark.memory.fraction=0.6   # 执行+存储占60%,即12G
spark.memory.storageFraction=0.5  # 存储占执行+存储的一半,即6G

这样算下来,每个核心能分到4G内存(20G / 5),符合黄金比例。

注意:不要给Executor配超过64G内存。为什么?因为JVM的GC在堆内存超过32G时就开始吃力,超过64G基本就是灾难。我见过一个团队给Executor配了128G,结果Full GC一次要几十秒,作业根本跑不动。

3.2 YARN队列资源规划

YARN队列规划,说白了就是怎么分蛋糕。集群资源是有限的,多个团队、多个作业要共享,怎么保证公平又不浪费?

我个人推荐使用容量调度器(Capacity Scheduler)。它支持多级队列、容量保证、弹性伸缩,非常适合生产环境。

先看一个典型的队列规划:

队列名称 容量 最大容量 用户限制 用途
root.prod 60% 80% 50% 生产作业,稳定性优先
root.dev 20% 50% 30% 开发测试,弹性使用
root.adhoc 20% 40% 20% 临时查询,随用随跑

这里有几个关键点:

  • 容量(Capacity):保证每个队列最少能拿到多少资源。比如prod队列保证60%,即使其他队列再忙,它也能拿到60%。
  • 最大容量(Maximum Capacity):防止某个队列把集群资源全占完。比如prod队列最多能用到80%,剩下的20%留给其他队列。
  • 用户限制(User Limit):防止单个用户把队列资源占满。比如prod队列中,单个用户最多用50%。

我曾经遇到过一个场景:某个团队在prod队列里跑了一个超大的ETL作业,把整个队列的资源全占了,导致其他实时作业全部排队。后来加了用户限制,每个用户最多用50%,问题就解决了。

配置示例(capacity-scheduler.xml):

<property>
  <name>yarn.scheduler.capacity.root.queues</name>
  <value>prod,dev,adhoc</value>
</property>
<property>
  <name>yarn.scheduler.capacity.root.prod.capacity</name>
  <value>60</value>
</property>
<property>
  <name>yarn.scheduler.capacity.root.prod.maximum-capacity</name>
  <value>80</value>
</property>
<property>
  <name>yarn.scheduler.capacity.root.prod.user-limit-factor</name>
  <value>2</value>
</property>

注意user-limit-factor这个参数。它控制单个用户能用的最大资源比例。默认是1,表示最多用队列容量的1倍。如果设成2,表示最多用2倍。我建议生产环境设成1或1.5,别太大。

3.3 动态资源分配实战

动态资源分配,是Spark 1.2引入的一个很实用的特性。它能让Spark根据作业的负载,自动调整Executor的数量。说白了,就是“按需分配”。

为什么要用动态资源分配?

  • 避免浪费:作业刚启动时,不需要那么多Executor;作业快结束时,也不需要那么多。动态分配可以自动缩容。
  • 提高利用率:空闲的Executor可以释放给其他作业用,集群整体利用率更高。
  • 简化配置:不用手动估算每个作业需要多少Executor,交给Spark自己判断。

但动态资源分配不是银弹。用不好,反而会出问题。

我曾经踩过的坑:有一次,我开了动态资源分配,但没配spark.dynamicAllocation.executorIdleTimeout,默认是60秒。结果作业在shuffle阶段,Executor频繁被回收又申请,导致大量数据重算,作业反而变慢了。

所以,动态资源分配的关键参数要配好:

参数 默认值 建议值 说明
spark.dynamicAllocation.enabled false true 开启动态资源分配
spark.dynamicAllocation.minExecutors 0 1 最少保留的Executor数,避免全部释放
spark.dynamicAllocation.maxExecutors Infinity 根据集群资源设定 最大Executor数,防止占满集群
spark.dynamicAllocation.initialExecutors minExecutors 根据数据量估算 初始Executor数,建议设大一点
spark.dynamicAllocation.executorIdleTimeout 60s 120-300s Executor空闲多久后回收
spark.dynamicAllocation.cachedExecutorIdleTimeout Infinity 根据缓存数据量设定 有缓存数据的Executor空闲多久后回收

实战配置示例:

spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.minExecutors=2
spark.dynamicAllocation.maxExecutors=50
spark.dynamicAllocation.initialExecutors=10
spark.dynamicAllocation.executorIdleTimeout=180s
spark.dynamicAllocation.cachedExecutorIdleTimeout=600s

这里有几个要点:

  • initialExecutors:我建议根据数据量估算一个初始值。比如数据量100G,每个Executor处理5G,那就设20个。这样作业启动后不用等扩容,直接开干。
  • executorIdleTimeout:设太短,Executor频繁回收;设太长,资源浪费。我个人习惯设120-300秒,具体看作业的stage时长。
  • cachedExecutorIdleTimeout:如果作业有缓存数据,这个值要设大一点。否则缓存数据被清掉,后面再用就要重新计算。

小技巧:如果作业有大量的shuffle操作,建议把spark.dynamicAllocation.shuffleTracking.enabled设为true。这样Spark会跟踪shuffle数据的位置,避免Executor回收后shuffle数据丢失。

最后,说一个很多人忽略的点:动态资源分配和YARN队列的配合。如果你在YARN队列里设了最大容量,那动态资源分配的maxExecutors也要相应调整。比如队列最大容量是100个vcore,每个Executor用2个vcore,那maxExecutors最多设50。设多了也没用,YARN不会给你那么多资源。