DAG 解决了什么问题?
DAG (有向无环图) 是 Spark 的核心架构之一,它的引入主要解决了 Hadoop MapReduce 框架的以下局限性:
- MapReduce 的独立性问题
在 Hadoop MapReduce 中,任务的每一步(Map 和 Reduce)是独立的,系统无法提前知道后续的计算计划。这种方式无法对整个计算流程进行全局优化。
Spark 的 DAG 允许系统通过解析操作链,提前构建完整的计算图,优化整个执行流程。 - 高频硬盘读写的性能瓶颈
在 MapReduce 中,每一步计算的中间结果都会写入硬盘或 HDFS。频繁的磁盘 I/O 操作会导致性能的大幅降低。
Spark 使用内存计算,并通过 DAG 优化操作顺序,避免不必要的磁盘操作,显著提升性能。
DAG 是如何工作的?
DAG 通过以下步骤优化计算和任务调度:
- 操作解析
Operator Graph:当代码被提交到 Spark 的解释器时,系统会生成一个操作图,称为 Operator Graph。它记录了所有的 Transformation 操作及其依赖关系。 - DAG Scheduler
分解阶段 (Stage):当一个 Action 操作(如 collect 或 save)被触发时,DAG Scheduler 会将 Operator Graph 分解为多个 Stage(阶段)。
- 窄依赖(Narrow Dependency):操作如 map 和 filter 不需要数据重新分区,属于同一阶段。
- 宽依赖(Wide Dependency):操作如 reduceByKey 需要数据 Shuffle,不同阶段之间以宽依赖为界。
- Task Scheduler
每个 Stage 会被拆分为多个基于数据分区的 Task(任务)。
Task Scheduler 将这些 Task 分发到集群的 Worker 节点上执行。 - 执行与结果
每个 Worker 节点执行分配的 Task,并将结果返回给 Driver 程序。
DAG 确保各个阶段按依赖顺序执行,并通过内存优化中间结果存储,最大限度减少 I/O 和通信开销。
DAG 的核心特性
-
任务依赖分析
每个 RDD 包含父节点的元信息和依赖关系(窄依赖或宽依赖)。
通过这些依赖,DAG Scheduler 能高效地决定任务的执行顺序。 -
内存计算优化
通过减少 Shuffle 和磁盘读写,DAG 提高了计算效率。 -
全局优化
DAG 确保每个 Stage 都包含最少的任务,避免重复计算。
三种 Spark 分布式部署模式
- Standalone 模式
特点:
- Spark 自带的资源管理器,无需依赖其他系统。
- 支持集群的基本功能,易于部署和维护。
适用场景:
- 小型测试环境或开发环境。
- 不需要与其他框架共享资源。
- Spark on Mesos 模式
特点:
官方推荐,灵活性更高。
支持两种资源分配模式:
- 粗粒度模式 (Coarse-grained):一次性申请资源,适用于长时间运行的任务。
- 细粒度模式 (Fine-grained):按需动态分配资源,适用于短作业任务。
适用场景:
- 需要高效的资源共享和管理。
- 与 Docker 等资源管理工具结合更自然。
- Spark on YARN 模式
特点:
利用 Hadoop YARN 管理资源,适用于 Hadoop 集群。
支持两种模式:
- YARN-cluster:适合生产环境,所有作业运行在集群中。
- YARN-client:适合调试和交互式作业,Driver 在客户端运行。
适用场景:
- 同时运行 Spark 和 Hadoop。
- 对 YARN 的兼容性要求较高。
Spark 共享变量
在Spark中,通常当一个函数被分配到集群中的远程节点执行时,Spark会在每个节点上创建这个函数的独立副本。这样,每个节点都会处理这个函数的副本,而不会与驱动程序或其他节点共享变量。为了优化这种情况,Spark提供了两种共享变量的机制:广播变量(Broadcast Variables)和累加器(Accumulators)。
- 广播变量(Broadcast Variables)
广播变量允许在集群中的每个节点上缓存一个只读变量的副本,而不是在每个任务中传递变量的完整拷贝。这样,能够有效地减少数据传输的开销。
使用场景:当你有一个大数据集,并且希望在多个节点中共享该数据集时,可以使用广播变量。比如,在分布式计算中,每个节点需要访问同一份数据,但不需要重复传输这份数据。
创建广播变量:可以通过SparkContext.broadcast(v)方法创建广播变量。广播变量是原始变量v的包装,创建之后,可以通过.value方法访问广播的内容。
示例代码:
val broadcastVar = sc.broadcast(Array(1, 2, 3)) // 创建广播变量
println(broadcastVar.value) // 输出广播变量的值:Array(1, 2, 3)
注意:广播变量一旦创建后,其值是不可修改的,确保集群中所有节点看到相同的值。
- 累加器(Accumulators)
累加器是专为并行计算设计的变量,它只能通过累加操作进行更新。累加器非常适用于执行计数或求和等任务。Spark原生支持数值类型的累加器,也支持开发者通过自定义类型来扩展累加器。
使用场景:累加器常用于计算任务中,需要对某些值进行统计或累加时。
创建累加器:可以通过SparkContext.accumulator(v)方法创建累加器,其中v是初始值。累加器的值只能通过add方法或+=操作符进行更新,且只能由驱动程序(driver)读取其值。
示例代码:
val accum = sc.accumulator(0) // 创建一个初始值为0的累加器
sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum += x) // 在每个任务中累加
println(accum.value) // 输出累加器的总值:10
自定义累加器:可以通过实现AccumulatorParam接口来自定义累加器的行为。例如,定义一个向量累加器来处理向量类型的数据。
object VectorAccumulatorParam extends AccumulatorParam[Vector] {
def zero(initialValue: Vector): Vector = {
Vector.zeros(initialValue.size) // 返回一个零向量
}
def addInPlace(v1: Vector, v2: Vector): Vector = {
v1 += v2 // 累加操作
}
}
val vecAccum = sc.accumulator(new Vector(...))(VectorAccumulatorParam) // 创建向量累加器
注意:在Python中,累加器目前还不被完全支持。
总结
广播变量:适用于在多个节点之间共享大型只读数据,避免重复传输。
累加器:用于并行计算中的求和或计数,只能通过“加法”操作更新值,并且只能由驱动程序访问其最终值。