2. Spark运行架构:Driver与Executor、Cluster Manager、任务调度流程、作业与阶段划分
好,咱们今天来聊聊Spark的运行架构。说实话,这个主题是理解Spark的基石。你如果搞不懂Driver和Executor在干嘛,那后面调优、排错基本就是抓瞎。我刚开始接触Spark时,也踩过不少坑,今天就把这些经验掰开揉碎了讲给你听。
2.1 核心组件:Driver与Executor
Spark应用跑起来,本质上就靠两个角色:Driver 和 Executor。你可以把Driver想象成项目经理,Executor就是干活的工人。
Driver:大脑与指挥官
Driver是Spark应用的入口。它负责三件事:
- 解析用户代码:你写的Spark作业,Driver会把它翻译成一个个任务。
- 调度任务:决定哪个任务该由哪个Executor执行。
- 管理状态:跟踪每个任务的执行进度,处理失败重试。
我个人习惯把Driver所在的节点称为“调度节点”。它不存数据,只做决策。嗯,这里要注意:Driver如果挂了,整个应用就完了。所以生产环境里,我建议给Driver分配足够的内存,别让它成为瓶颈。
Executor:真正的苦力
Executor是真正干活的地方。每个Executor是一个JVM进程,负责:
- 执行任务:运行Driver分配过来的计算任务。
- 存储数据:缓存RDD或DataFrame,加速后续计算。
- 汇报进度:定期告诉Driver“我还活着,任务快跑完了”。
我在项目中遇到过一个问题:某个Executor频繁OOM。后来发现是数据倾斜,某个分区数据量特别大,Executor扛不住了。解决办法是调整分区数,或者用salting技术打散数据。你想想看,如果不懂Executor的职责,这种问题排查起来就像无头苍蝇。
核心要点:Driver管调度,Executor管计算。两者通过RPC通信,Executor的数量决定了你的并行计算能力。
2.2 Cluster Manager:资源大管家
Cluster Manager负责分配集群资源。说白了,就是决定“哪个Worker节点上能跑几个Executor”。Spark支持多种Cluster Manager:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| Standalone | Spark自带的简单调度器 | 小集群、测试环境 |
| YARN | Hadoop生态标配,资源隔离好 | 生产环境,与HDFS配合 |
| Kubernetes | 容器化部署,弹性伸缩 | 云原生场景 |
| Mesos | 粗粒度/细粒度调度 | 已逐渐被K8s取代 |
我个人最常用的是YARN。为什么?因为大多数公司都有Hadoop集群,YARN能跟HDFS无缝配合。我曾经在Standalone模式下跑一个大作业,结果资源分配不均,Executor有的忙死有的闲死。换成YARN后,资源管理就稳多了。
避坑指南:选择Cluster Manager时,别只看功能。要考虑运维成本。YARN虽然老,但稳定;K8s虽然新,但需要额外的容器化改造。我建议先从YARN上手,等团队熟悉了再考虑迁移。
2.3 任务调度流程:从代码到执行
你写了一段Spark代码,它到底是怎么变成一个个任务的?这个过程我拆成四步讲:
- 构建DAG:Driver解析你的代码,生成一个有向无环图。每个RDD的转换操作(如map、filter)都会变成DAG中的一个节点。
- 划分Stage:DAGScheduler登场。它会根据宽依赖(shuffle)把DAG切成多个Stage。每个Stage包含一组可以流水线执行的任务。
- 生成TaskSet:每个Stage被拆成多个Task,每个Task对应一个分区。TaskSet就是这些任务的集合。
- 分配与执行:TaskScheduler把TaskSet里的任务分配给空闲的Executor。Executor拿到任务后,开始执行。
为什么会这样设计?说白了,就是为了容错和并行。如果某个Task失败了,只需要重跑那个Task,而不是整个Stage。我见过有人把整个DAG画出来调试,虽然麻烦,但确实能发现一些隐藏的性能问题。
关键点:任务调度是Spark的“心脏”。DAGScheduler负责逻辑划分,TaskScheduler负责物理执行。两者配合,才能高效跑通你的作业。
2.4 作业与阶段划分:宽依赖是关键
Spark里有个概念叫“作业”(Job),一个Action操作(如count、saveAsTextFile)会触发一个Job。而Job内部,会被DAGScheduler划分成多个Stage。
划分的依据是什么?宽依赖。说白了,就是看有没有shuffle。比如groupByKey、reduceByKey这些操作,会导致数据跨分区重分布,这就是宽依赖。窄依赖(如map、filter)则不会触发Stage划分。
我举个例子:
val rdd = sc.textFile("hdfs://data.txt")
val words = rdd.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val counts = pairs.reduceByKey(_ + _)
counts.saveAsTextFile("output")
这段代码里,reduceByKey是宽依赖。所以DAGScheduler会划分出两个Stage:
- Stage 0:从textFile到map,全是窄依赖,可以流水线执行。
- Stage 1:reduceByKey和saveAsTextFile,需要shuffle,单独一个Stage。
我在项目中遇到过一个问题:某个作业Stage特别多,每个Stage都很小。后来发现是代码里用了多次groupByKey,导致频繁shuffle。解决办法是尽量用reduceByKey替代groupByKey,减少Stage数量。你想想看,Stage越多,调度开销越大,性能自然就下来了。
注意:宽依赖是性能杀手。每次shuffle都会涉及磁盘IO和网络传输。我建议你在写代码时,尽量用combine操作(如reduceByKey)代替groupByKey,减少数据移动。
2.5 实战经验总结
讲了这么多,我总结几条实战经验:
- Driver别太弱:Driver内存不够,调度会变慢。我一般给Driver分配4-8GB内存。
- Executor数量要合理:不是越多越好。Executor太多,调度开销会增大。我建议每个节点跑2-4个Executor。
- 监控Stage划分:在Spark UI里看Stage数量。如果Stage太多,说明shuffle频繁,需要优化代码。
- Cluster Manager选型:小团队用Standalone,大集群用YARN,云原生用K8s。别盲目追新。
嗯,Spark的运行架构就讲到这里。下一章咱们聊聊RDD的依赖关系和血统机制,那才是Spark容错的核心。到时候我会分享一个我踩过的血统断裂的坑,保证让你印象深刻。