4、Spark核心原理:RDD概念、DAG调度、Spark运行架构(Driver/Executor)
好,咱们今天聊聊Spark的核心。说实话,很多初学者把Spark当成一个「更快版」的MapReduce来学,这其实是个误区。Spark真正的灵魂,在于它提出了一个非常优雅的抽象——RDD。我当年第一次接触RDD时,第一反应是:这不就是个数组吗?后来踩了无数坑才明白,这玩意儿远比数组聪明得多。
4.1 RDD:弹性分布式数据集
RDD的全称是Resilient Distributed Dataset,翻译过来就是「弹性分布式数据集」。名字挺长,但核心就三个词:不可变、可分区、可并行计算。
说白了,RDD就是一个只读的、分区的数据集合。你没法修改它,只能通过转换操作生成新的RDD。这设计跟函数式编程里的不可变对象一个道理——避免了复杂的并发控制。
RDD的五大特性:
- 分区列表:数据被切分成多个分区,分布在集群的不同节点上
- 计算函数:每个分区都有一个计算函数,负责生成该分区的数据
- 依赖关系:记录当前RDD是从哪些父RDD转换而来的
- 分区器(可选):对于key-value类型的RDD,可以指定分区方式
- 优先位置列表(可选):计算时优先在数据所在节点执行,减少网络传输
我在项目中遇到过这样一个场景:有个同事把RDD当成了数据库表,反复读取同一个RDD去做不同的计算。结果每次读取都重新计算一遍,性能惨不忍睹。嗯,这里要注意——RDD默认是不缓存的,每次行动操作都会重新计算。如果你要复用,记得调用.cache()或.persist()。
4.2 DAG调度:Spark的「脑回路」
RDD之间通过依赖关系形成了一张有向无环图,也就是DAG(Directed Acyclic Graph)。Spark的调度器就是靠这张图来决定怎么执行任务的。
你想想看,如果每个RDD都独立计算,那跟MapReduce有什么区别?Spark的高明之处在于:它会根据RDD的依赖关系,把多个操作「流水线」起来。
依赖关系分两种:
| 依赖类型 | 特点 | 举例 |
|---|---|---|
| 窄依赖(Narrow Dependency) | 每个父RDD分区最多被子RDD的一个分区使用 | map、filter、union |
| 宽依赖(Wide Dependency) | 每个父RDD分区可能被子RDD的多个分区使用 | groupByKey、reduceByKey |
窄依赖的操作可以在同一个Stage里「管道化」执行,不需要shuffle。宽依赖则意味着需要shuffle,也就是Stage的分界线。
我的经验:我曾经在一个实时推荐系统里,因为没注意宽依赖的代价,导致一个简单的groupByKey操作把整个集群的网络打爆了。后来改成reduceByKey,性能提升了10倍不止。记住:能用reduceByKey就别用groupByKey,前者会在map端做预聚合。
DAG调度器的工作流程大致是这样的:
- 用户代码触发一个行动操作(如
collect()、count()) - 调度器从最后一个RDD往前回溯,构建完整的DAG
- 根据宽依赖切分Stage,窄依赖的操作合并到同一个Stage
- 每个Stage内部生成多个Task,提交给Executor执行
4.3 Spark运行架构:Driver与Executor
聊完了数据模型和调度逻辑,咱们来看看Spark到底是怎么跑起来的。整个运行架构就两个核心角色:Driver 和 Executor。
Driver:大脑
Driver是Spark应用的「主控节点」。它负责:
- 解析用户代码,构建DAG
- 将DAG切分成Stage,生成Task
- 调度Task到Executor上执行
- 收集结果,返回给用户
说白了,Driver就是那个「动嘴不动手」的角色。它不处理数据,只负责指挥。
避坑指南:我曾经在一个生产环境里,把Driver的内存设得太小,结果任务一多就OOM。记住:Driver不仅要存DAG元数据,还要收集最终结果。如果你的结果集很大(比如collect一个上亿条数据的RDD),Driver的内存一定要给够。
Executor:苦力
Executor是真正干活的。每个Worker节点上可以运行多个Executor,每个Executor是一个独立的JVM进程。它负责:
- 执行Driver分配过来的Task
- 将计算结果缓存到内存或磁盘
- 向Driver汇报任务状态和心跳
Executor的数量和资源分配,直接决定了你的Spark作业能跑多快。我见过有人把Executor的内存设成跟Driver一样大,结果数据量一大就频繁GC。其实Executor才是消耗内存的大头,因为数据都在它这里处理。
任务执行流程
一个完整的Spark作业执行流程是这样的:
- 用户提交应用,启动Driver进程
- Driver向集群管理器(如YARN、Standalone)申请资源
- 集群管理器启动Executor进程
- Driver将用户代码转换成DAG,再切分成Stage和Task
- Driver将Task发送给Executor执行
- Executor执行Task,将结果返回给Driver
- 所有Task完成后,Driver将最终结果返回给用户
这里有个细节很多人会忽略:Executor之间的数据交换。宽依赖需要shuffle,而shuffle的本质就是Executor之间通过网络传输数据。这也是Spark性能瓶颈最常见的地方。
我的建议:调优Spark作业时,先看两个指标:Shuffle Read Size 和 GC Time。如果Shuffle Read Size很大,说明数据倾斜或者分区不合理;如果GC Time占比超过10%,说明内存不够或者对象创建太频繁。这两个指标能帮你快速定位90%的性能问题。
好了,Spark的核心原理就这些。RDD是数据模型,DAG是调度逻辑,Driver/Executor是运行架构。这三者环环相扣,理解了它们,你就掌握了Spark的「内功心法」。下一节咱们聊聊Spark SQL,看看它是怎么把SQL翻译成RDD操作的。