Spark运行原理与架构:作业提交流程、Driver与Executor角色、RDD依赖与DAG调度
说实话,很多Spark初学者上来就写代码,跑通了就觉得自己会了。但一旦遇到性能问题,或者作业莫名其妙挂掉,就完全摸不着头脑。我见过太多人把Spark当成一个黑盒在用——这其实挺危险的。
这一章,咱们就把这个黑盒拆开看看。你搞懂了Spark的运行原理,调优才有方向。不然你连瓶颈在哪都不知道,调什么呢?
1. Spark作业提交流程:从代码到集群
你写了一个WordCount程序,然后敲下spark-submit。这背后发生了什么?
我习惯把这个过程分成四个阶段:
- 客户端提交:
spark-submit脚本启动,加载你的配置和代码。 - 申请资源:向集群管理器(YARN、Mesos或Kubernetes)申请资源。
- 启动Driver:集群管理器在某个节点上启动Driver进程。
- 启动Executor:Driver反向向集群管理器申请Executor资源,然后启动它们。
嗯,这里要注意一个细节。很多人以为spark-submit提交完就完事了。其实不是。提交模式不同,Driver跑的位置也不同。
| 部署模式 | Driver位置 | 适用场景 |
|---|---|---|
| client模式 | 提交作业的客户端机器 | 调试、交互式开发 |
| cluster模式 | 集群内部某个节点 | 生产环境 |
我在项目中遇到过一个问题:用client模式跑生产作业,结果网络波动导致客户端和集群断开,整个作业就挂了。后来全部改成cluster模式,Driver跑在集群里,稳多了。
2. Driver与Executor:Spark的两大核心角色
Spark集群里,有两个最重要的角色:Driver和Executor。说白了,Driver是大脑,Executor是手脚。
2.1 Driver:作业的总指挥
Driver负责三件事:
- 解析代码:把你的RDD操作转换成DAG(有向无环图)。
- 调度任务:把DAG切分成Stage,再把Stage拆成Task,分发给Executor。
- 管理状态:记录每个Task的执行结果,处理失败重试。
你想想看,如果Driver挂了,整个作业就完了。所以Driver的内存配置很关键。我一般建议给Driver分配至少1-2GB内存,如果用了collect()这类操作,还要更大。
2.2 Executor:真正的执行者
Executor是跑在Worker节点上的进程。它负责:
- 执行Task:运行Driver分配过来的任务。
- 存储数据:缓存RDD、Shuffle数据等。
- 汇报状态:定期向Driver报告心跳和任务进度。
Executor的数量和大小,直接决定了你的作业能跑多快。我见过一个调优案例:有人把Executor内存设得特别大,但只开了2个。结果GC频繁,性能反而很差。后来改成8个Executor,每个4GB,性能提升了3倍。
3. RDD依赖与DAG调度:Spark的精髓
这部分是Spark最核心的东西。你搞懂了RDD依赖和DAG调度,就掌握了Spark的命脉。
3.1 RDD依赖:宽依赖与窄依赖
RDD之间有依赖关系。我把它分成两种:
- 窄依赖(Narrow Dependency):每个父RDD的分区最多被子RDD的一个分区使用。比如
map()、filter()。 - 宽依赖(Wide Dependency):每个父RDD的分区可能被子RDD的多个分区使用。比如
groupByKey()、reduceByKey()。
为什么要区分这个?因为宽依赖会触发Shuffle。Shuffle是Spark性能的杀手。你想想看,数据要在网络间重新分区,磁盘IO、网络传输、序列化,每一步都可能成为瓶颈。
reduceByKey()代替groupByKey(),前者会在map端做预聚合,减少Shuffle数据量。
3.2 DAG调度:从代码到物理执行
Driver拿到你的代码后,会做三件事:
- 构建DAG:把RDD的操作链画成一个有向无环图。
- 划分Stage:遇到宽依赖就切一刀,分成不同的Stage。
- 生成Task:每个Stage内部,根据分区数生成对应的Task。
我画了一张图,帮你理解这个过程:
3.3 任务调度:Stage内部怎么跑?
每个Stage被切分成多个Task。Task的数量等于该Stage最后一个RDD的分区数。Driver会把Task分发到各个Executor上。
这里有个关键点:数据本地性。Driver会尽量把Task调度到数据所在的节点上,避免网络传输。优先级是这样的:
- PROCESS_LOCAL:数据和Task在同一进程(最佳)。
- NODE_LOCAL:数据和Task在同一节点。
- RACK_LOCAL:数据和Task在同一机架。
- ANY:任意节点(最差)。
我遇到过一个问题:某个作业一直卡在NODE_LOCAL级别,数据本地性上不去。后来发现是Executor数量太少,数据分布不均匀。加了几个Executor后,大部分Task都变成了PROCESS_LOCAL,性能提升明显。
ANY或RACK_LOCAL,说明数据本地性不好。可以尝试增加Executor数量,或者调整spark.locality.wait参数(默认3秒)。
4. 总结:一张图看懂Spark运行原理
好了,咱们把这一章的核心内容串起来:
- 提交作业:
spark-submit→ 集群管理器 → 启动Driver和Executor。 - Driver:构建DAG → 划分Stage → 生成Task → 调度到Executor。
- Executor:执行Task → 缓存数据 → 汇报状态。
- RDD依赖:窄依赖(无Shuffle) vs 宽依赖(触发Shuffle)。
- DAG调度:宽依赖是Stage的分界线,Stage内部Task并行执行。
你把这些搞清楚了,后面讲调优的时候,我提到“减少Shuffle”、“优化数据本地性”、“调整并行度”,你就能立刻明白背后的原理。而不是盲目地改参数。