3. Spark资源模型:Executor、Core、Memory的分配哲学

资源分配这件事,说白了就是「给每个任务分多少地、分多少粮」。我见过太多团队,代码写得漂漂亮亮,结果资源配得一塌糊涂——要么集群跑不满,要么内存炸得飞起。今天咱们就聊聊Executor、Core、Memory这三者到底该怎么搭。

3.1 Executor:Spark的执行单元

Executor是啥?你可以把它理解成Spark在Worker节点上启动的一个JVM进程。每个Executor负责运行Task,并且把数据存在内存或磁盘上。

我个人习惯把Executor比作「工位」。你想想看,一个工位配几个人、配多大桌子,直接决定了干活效率。

核心参数:

  • spark.executor.instances:Executor的数量
  • spark.executor.cores:每个Executor的CPU核心数
  • spark.executor.memory:每个Executor的内存大小

我在项目中遇到过一个问题:某次给一个数据管道配了200个Executor,每个Executor只给1个Core。结果呢?任务调度开销比实际计算还大,整个集群都在忙着切换上下文。嗯,这就是典型的「工位太多,人太少」。

3.2 Core:并行度的基石

Core决定了你的Spark任务能同时跑多少个Task。一个Core在同一时间只能处理一个Task,这是铁律。

你可能会问:「那我是不是Core越多越好?」

不一定。我见过有人给每个Executor配了32个Core,结果GC时间占了总运行时间的40%。为什么?因为JVM的垃圾回收在大并发下会变成瓶颈。说白了,Core太多,内存反而成了短板。

我的经验值:

每个Executor配5-8个Core是比较稳妥的选择。我一般用5个,留一个给操作系统和JVM本身用。

3.3 Memory:内存的分配哲学

内存分配是门艺术。Spark把内存分成了三块:

内存区域 默认比例 用途
Storage Memory 50% 缓存RDD、广播变量
Execution Memory 50% Shuffle、Join、Aggregation
Reserved Memory 300MB Spark内部使用

这里有个坑:Storage和Execution是「软边界」。什么意思?如果Execution不够用,它可以抢Storage的地盘。反过来也一样。但抢来抢去,性能就下来了。

避坑指南:

我曾经给一个ETL任务配了64GB内存,结果Shuffle的时候疯狂溢写磁盘。查了半天才发现,Execution Memory被Storage占了大半。后来我把spark.memory.storageFraction从0.5调到了0.3,问题就解决了。

3.4 分配哲学:三个黄金法则

做了这么多年Spark调优,我总结了三句话:

  1. Core不要贪多:每个Executor 5-8个Core,再多就得不偿失。
  2. 内存要留余量:给操作系统留20-30%的内存,别把机器榨干。
  3. Executor数量要匹配数据量:小数据用大Executor,大数据用小Executor。

举个例子。假设你有一个100节点的集群,每台机器128GB内存、32个Core。你会怎么配?

我一般这么干:

spark.executor.instances = 200
spark.executor.cores = 5
spark.executor.memory = 20g
spark.executor.memoryOverhead = 4g

为什么是20g?因为128GB / 5个Executor ≈ 25GB,再留5GB给系统。为什么是200个?因为100台机器 × 2个Executor/台 = 200。这样每台机器跑2个Executor,每个Executor 5个Core,刚好用满10个Core,剩下的22个Core给系统和其他进程。

小技巧:

如果你用YARN,记得把spark.yarn.executor.memoryOverhead加上。这个参数用来存JVM之外的内存开销,比如NIO buffer、线程栈啥的。我一般设成executor memory的10-20%。

3.5 动态分配:懒人必备

如果你懒得手动算,可以用动态分配。Spark会根据负载自动调整Executor数量。

spark.dynamicAllocation.enabled = true
spark.dynamicAllocation.minExecutors = 10
spark.dynamicAllocation.maxExecutors = 200
spark.dynamicAllocation.initialExecutors = 50

但注意,动态分配不是万能的。我遇到过一个问题:某个任务突然来了个数据倾斜,动态分配疯狂加Executor,结果每个Executor只处理了几条数据,调度开销反而把集群拖垮了。所以,动态分配适合负载波动不大的场景,数据倾斜严重的任务还是手动配吧。

3.6 总结:记住这张表

参数 推荐值 说明
executor.cores 5-8 太多会导致GC压力大
executor.memory 16-32g 太大GC慢,太小溢写多
executor.instances 根据集群算 每台机器2-3个为宜
memoryOverhead 10-20% 别忘了加这个

嗯,资源分配这事,没有银弹。每个集群、每个任务都不一样。我的建议是:先按经验值配,跑一遍看看监控,再微调。别指望一次配对,调优是个迭代的过程。

下一章咱们聊聊Shuffle调优,那才是真正让人头疼的地方。