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的一个分区使用 mapfilterunion
宽依赖(Wide Dependency) 每个父RDD分区可能被子RDD的多个分区使用 groupByKeyreduceByKey

窄依赖的操作可以在同一个Stage里「管道化」执行,不需要shuffle。宽依赖则意味着需要shuffle,也就是Stage的分界线。

我的经验:我曾经在一个实时推荐系统里,因为没注意宽依赖的代价,导致一个简单的groupByKey操作把整个集群的网络打爆了。后来改成reduceByKey,性能提升了10倍不止。记住:能用reduceByKey就别用groupByKey,前者会在map端做预聚合。

DAG调度器的工作流程大致是这样的:

  1. 用户代码触发一个行动操作(如collect()count()
  2. 调度器从最后一个RDD往前回溯,构建完整的DAG
  3. 根据宽依赖切分Stage,窄依赖的操作合并到同一个Stage
  4. 每个Stage内部生成多个Task,提交给Executor执行

4.3 Spark运行架构:Driver与Executor

聊完了数据模型和调度逻辑,咱们来看看Spark到底是怎么跑起来的。整个运行架构就两个核心角色:DriverExecutor

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作业执行流程是这样的:

  1. 用户提交应用,启动Driver进程
  2. Driver向集群管理器(如YARN、Standalone)申请资源
  3. 集群管理器启动Executor进程
  4. Driver将用户代码转换成DAG,再切分成Stage和Task
  5. Driver将Task发送给Executor执行
  6. Executor执行Task,将结果返回给Driver
  7. 所有Task完成后,Driver将最终结果返回给用户

这里有个细节很多人会忽略:Executor之间的数据交换。宽依赖需要shuffle,而shuffle的本质就是Executor之间通过网络传输数据。这也是Spark性能瓶颈最常见的地方。

我的建议:调优Spark作业时,先看两个指标:Shuffle Read SizeGC Time。如果Shuffle Read Size很大,说明数据倾斜或者分区不合理;如果GC Time占比超过10%,说明内存不够或者对象创建太频繁。这两个指标能帮你快速定位90%的性能问题。

好了,Spark的核心原理就这些。RDD是数据模型,DAG是调度逻辑,Driver/Executor是运行架构。这三者环环相扣,理解了它们,你就掌握了Spark的「内功心法」。下一节咱们聊聊Spark SQL,看看它是怎么把SQL翻译成RDD操作的。