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调优,我总结了三句话:
- Core不要贪多:每个Executor 5-8个Core,再多就得不偿失。
- 内存要留余量:给操作系统留20-30%的内存,别把机器榨干。
- 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调优,那才是真正让人头疼的地方。