3. 资源调优(上):Executor核心数与内存的黄金比例、YARN队列资源规划、动态资源分配实战
资源调优,说白了就是给Spark作业分配合适的“人”和“粮”。人不够,活干得慢;粮不够,人饿着肚子干,还容易出问题。我见过太多团队,代码写得挺漂亮,结果资源配得一塌糊涂,跑个几T的数据,要么OOM,要么CPU利用率不到20%。
这一章,咱们就聊聊资源调优最核心的三个问题:Executor到底配多少核心、多少内存才算合理?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不会给你那么多资源。