• Spark 中 RDD的运行机制


    1. RDD 的设计与运行原理

    Spark 的核心是建立在统一的抽象 RDD 之上,基于 RDD 的转换和行动操作使得 Spark 的各个组件可以无缝进行集成,从而在同一个应用程序中完成大数据计算任务。

    在实际应用中,存在许多迭代式算法和交互式数据挖掘工具,这些应用场景的共同之处在于不同计算阶段之间会重用中间结果,即一个阶段的输出结果会作为下一个阶段的输入。而 Hadoop 中的 MapReduce 框架都是把中间结果写入到 HDFS 中,带来了大量的数据复制、磁盘 IO 和序列化开销,并且通常只支持一些特定的计算模式。而 RDD 提供了一个抽象的数据架构,从而让开发者不必担心底层数据的分布式特性,只需将具体的应用逻辑表达为一系列转换处理,不同 RDD 之间的转换操作形成依赖关系,可以实现管道化,从而避免了中间结果的存储,大大降低了数据复制、磁盘 IO 和序列化开销。

    1.1. RDD 概念

    一个 RDD 就是一个分布式对象集合,提供了一种高度受限的共享内存模型,其本质上是一个只读的分区记录集合,不能直接修改。每个 RDD 可以分成多个分区,每个分区就是一个数据集片段,并且一个 RDD 的不同分区可以保存到集群中不同的节点上,从而可以在集群中的不同节点上进行并行计算。

    RDD 提供了一组丰富的操作以支持常见的数据运算,分为“行动”(Action)和“转换”(Transformation)两种类型,前者用于执行计算并指定输出的形式,后者指定 RDD 之间的相互依赖关系。RDD 提供的转换接口都非常简单,都是类似 mapfiltergroupByjoin 等粗粒度的数据转换操作,而不是针对某个数据项的细粒度修改。因此,RDD 比较适合对于数据集中元素执行相同操作的批处理式应用,而不适合用于需要异步、细粒度状态的应用,比如 Web 应用系统、增量式的网页爬虫等。

    RDD 的典型的执行过程如下:

    1. 读入外部的数据源(或者内存中的集合)进行 RDD 创建;
    2. RDD 经过一系列的 “转换” 操作,每一次都会产生不同的 RDD,供给下一个转换使用;
    3. 最后一个 RDD 经过 “行动” 操作进行处理,并输出指定的数据类型和值。

    RDD 采用了惰性调用,即在 RDD 的执行过程中,所有的转换操作都不会执行真正的操作,只会记录依赖关系,而只有遇到了行动操作,才会触发真正的计算,并根据之前的依赖关系得到最终的结果。

    uploading-image-563318.png

    下面以一个实例来描述 RDD 的实际执行过程,如下图所示,开始从输入中创建了两个 RDD,分别是 A 和 C,然后经过一系列的转换操作,最终生成了一个 F,这也是一个 RDD。注意,这些转换操作的执行过程中并没有执行真正的计算,基于创建的过程也没有执行真正的计算,而只是记录的数据流向轨迹。当 F 执行了行为操作并生成输出数据时,Spark 才会根据 RDD 的依赖关系生成有向无环图(DAG),并从起点开始执行真正的计算。正是 RDD 的这种惰性调用机制,使得转换操作得到的中间结果不需要保存,而是直接管道式的流入到下一个操作进行处理。

    uploading-image-61893.png

    1.2. RDD 特性

    总体而言,Spark 采用 RDD 以后能够实现高效计算的主要原因如下:

    1. 高效的容错性。在 RDD 的设计中,只能通过从父 RDD 转换到子 RDD 的方式来修改数据,这也就是说我们可以直接利用 RDD 之间的依赖关系来重新计算得到丢失的分区,而不需要通过数据冗余的方式。而且也不需要记录具体的数据和各种细粒度操作的日志,这大大降低了数据密集型应用中的容错开销。

    2. 中间结果持久化到内存。数据在内存中的多个 RDD 操作之间进行传递,不需要在磁盘上进行存储和读取,避免了不必要的读写磁盘开销;

    3. 存放的数据可以是 Java 对象,避免了不必要的对象序列化和反序列化开销。

    1.3. RDD 之间的依赖关系

    RDD 中的不同的操作会使得不同 RDD 中的分区会产生不同的依赖关系,主要分为窄依赖(Narrow Dependency)与宽依赖(Wide Dependency)。其中,窄依赖表示的是父 RDD 和子 RDD 之间的一对一关系或者多对一关系,主要包括的操作有 mapfilterunion 等;而宽依赖则表示父 RDD 与子 RDD 之间的一对多关系,即一个父 RDD 转换成多个子 RDD,主要包括的操作有 groupByKeysortByKey 等。

    uploading-image-606443.png

    对于窄依赖的 RDD,可以以流水线的方式计算所有父分区,不会造成网络之间的数据混合。对于宽依赖的 RDD,则通常伴随着 Shuffle 操作,即首先需要计算好所有父分区数据,然后在节点之间进行 Shuffle。因此,在进行数据恢复时,窄依赖只需要根据父 RDD 分区重新计算丢失的分区即可,而且可以并行地在不同节点进行重新计算。而对于宽依赖而言,单个节点失效通常意味着重新计算过程会涉及多个父 RDD 分区,开销较大。此外,Spark 还提供了数据检查点和记录日志,用于持久化中间 RDD,从而使得在进行失败恢复时不需要追溯到最开始的阶段。在进行故障恢复时,Spark 会对数据检查点开销和重新计算 RDD 分区的开销进行比较,从而自动选择最优的恢复策略。

    1.4. 阶段的划分

    Spark 通过分析各个 RDD 的依赖关系生成了 DAG ,再通过分析各个 RDD 中的分区之间的依赖关系来决定如何划分阶段,具体划分方法是:在 DAG 中进行反向解析,遇到宽依赖就断开,遇到窄依赖就把当前的 RDD 加入到当前的阶段中;将窄依赖尽量划分在同一个阶段中,可以实现流水线计算。例如在下图中,首先根据数据的读取、转化和行为等操作生成 DAG。然后在执行行为操作时,反向解析 DAG,由于从 A 到 B 的转换和从 B、F 到 G 的转换都属于宽依赖,则需要从在宽依赖处进行断开,从而划分为三个阶段。把一个 DAG 图划分成多个 “阶段” 以后,每个阶段都代表了一组关联的、相互之间没有 Shuffle 依赖关系的任务组成的任务集合。每个任务集合会被提交给任务调度器(TaskScheduler)进行处理,由任务调度器将任务分发给 Executor 运行。

    uploading-image-772555.png

    1.5. RDD 运行过程

    通过上述对 RDD 概念、依赖关系和阶段划分的介绍,结合之前介绍的 Spark 运行基本流程,这里再总结一下 RDD 在 Spark 架构中的运行过程(如下图所示):

    1. 创建 RDD 对象;
    2. SparkContext 负责计算 RDD 之间的依赖关系,构建 DAG;
    3. DAGSchedule 负责把 DAG 图反向解析成多个阶段,每个阶段中包含多个任务,每个任务会被任务调度器分发给工作节点上的 Executor 上执行。

    uploading-image-64896.png

  • 相关阅读:
    UVa 1151 Buy or Build【最小生成树】
    UVa 216 Getting in Line【枚举排列】
    UVa 729 The Hamming Distance Problem【枚举排列】
    HDU 5214 Movie【贪心】
    HDU 5223 GCD
    POJ 1144 Network【割顶】
    UVa 11025 The broken pedometer【枚举子集】
    HDU 2515 Yanghee 的算术【找规律】
    Java基本语法
    Java环境变量,jdk和jre的区别,面向对象语言编程
  • 原文地址:https://www.cnblogs.com/ydcode/p/11009323.html
Copyright © 2020-2023  润新知