Spark 编程模型 RDD

Spark 编程模型有两个主要的抽象,第一个是弹性数据集 RDD(Resilient Distributed Dataset),第二个是共享变量:广播变量和累加器。首先了解以下 RDD。

Spark RDD

基于 Spark 的大数据计算平台,建立在统一的抽象 RDD 之上,是一种具有容错性的基于内存的数据集抽象计算方法。一个 RDD 本质上相当于数据的一个元数据结构,存储数据分区,逻辑结构映射关系和依赖转换关系,具有以下特征(内部属性):

  • 分区(partition),有一个数据分片列表,将数据进行切分后并行计算,是数据集的原子组成部分。
  • 函数(compute),计算每个分片的函数,即在父 RDD 上执行什么操作的到一个可遍历的结果。
  • 依赖(dependency),每个 RDD 对父 RDD 的依赖列表,通过依赖关系描述血缘(lineage)。源 RDD 没有依赖。
  • 优先位置(可选),每一个分片的优先计算位置,如 HDFS 上分片所属 Block 的所在位置是优先计算位置。
  • 分区策略(可选),描述分区策略、分区数和数据存放位置。类似与 MapReduce 的 Paritioner 接口,对 Key/Value 的数据集 RDD,根据 Key 决定数据所在分区(如 Hash 值)。

RDD 依赖

RDD 一个很重要的属性是对父 RDD 的依赖,主要分为两种:窄依赖(narrow denependcy)和宽依赖(wide dependency)。

RDD Dependency

窄依赖

窄依赖指父 RDD 的每个分区最多被一个子 RDD 分区使用(一个子 RDD 可以使用多个父 RDD 的分区),如:map、filter、union 等算子。

如上图,对数据进行协同划分(co-partitioned)的 Join 也属于窄依赖,多个父 RDD 分区的所有 key 落入子 RDD 的同一个分区。

宽依赖

宽依赖指子 RDD 的每个分区依赖于父 RDD 的所有或多个分区,如:groupby、partition 等算子。

如上图,未对数据进行协同划分(co-partitioned)的 Join 属于宽依赖,一个父 RDD 分区的 key 落入子 RDD 的多个分区。

区分依赖关系

对于数据的计算,窄依赖的 RDD 可以在集群上的一个节点以 pipeline 的方式计算,不要在多个节点间交换数据;宽依赖的 RDD 会涉及数据的混合,需要计算好父分区的数据,然后在节点之间进行 Shuffle。

对于数据的恢复,窄依赖能够有效的进行节点恢复,只需要重新计算丢失 RDD 分区的所有父分区,不同节点之间可以并行计算;宽依赖单个节点的丢失可能导致 RDD 的所有祖先重新计算。

创建 RDD

既然 RDD 是 Spark 计算的核心,那么 RDD 的创建就非常的重要,支持三种创建方式:

  • 将一个集合对象并行化成为 RDD
  • 从 Hadoop 文件系统作为数据创建 RDD
  • 父 RDD 经过转换得到新的 RDD
val sc: SparkContext = ...

// 从集合创建
val data = Array(1,2,3,4,5)
val distData: RDD[Int] = sc.parallelize(data)

// 从文件系统创建
val distFile: RDD[String] = sc.textFile("dfs://text.txt")

// 创建新的 RDD
val newRdd: RDD[Int] = distFile.map(s => s.length)
                               .reduce((a, b) => a + b)

RDD 转换

RDD 提供了一个抽象的分布式数据架构,我们不必关系底层数据的分布式特性,应用逻辑可以表达为一系列转换处理。应用逻辑可以使用操作算子来表达,分为两大类算子:Transformation 和 Action。

Spark DAG 根据 Action 算子来划分执行 Stage,之后会详细介绍 Spark 的执行流程。目前只需要直到这两类算子分别包括哪些具体的算子。

Transformation(转换)算子

可以理解为对一个数据集生成另一个数据集的操作,是数据集的逻辑操作,并没有真正的计算。

常用的基础转换操作

操作 说明
map(func) 每个元素经过func函数转换后,形成一个新的数据集返回
filter(func) 每个元素经过func函数,保留返回值为 true 的元素,形成一个新的数据集返回
flatMap(func) 类似于map,每一个输入元素,会被映射为0到多个输出元素(Seq)
mapPartitions(func) 类似于map,针对数据集的每个分区执行转换
mapPartitionsWithIndex(func) 类似于mapPartitions,额外提供一个代表分区的索引的整数值
sample(withReplacement, fraction, seed) 对数据集中的元素,随机抽样
union(otherDataset) 两个数据集的元素合并,形成一个新的数据集返回
distinct([numTasks]) 返回一个原数据集去重后的数据集
repartition(numPartitions) 在原数据集上随机 Shuffle,创造出更多或更少的分区

针对 Key/Value 类型的数据转换操作,在一个(K,V)对组成的数据集上使用

操作 说明
groupByKey([numTasks]) K相同的所有V组成序列返回(K,Iterable[V])对数据集
reduceByKey(func,[numTasks]) K相同的V使用func函数依次聚合返回(K,V)数据集
sortByKey([ascending], [numTasks]) 返回一个以K排序的(K,V)对数据集
join(otherDataset, [numTasks]) 参数是(K,W)对数据集,返回一个(K,(V,W))对数据集
groupWith(otherDataset, [numTasks]) 参数是(K,W)对数据集,返回一个(K,Iterable[V],Iterable[W]))

Action(执行)算子

Action 算子会触发一次真正的计算,Action 以及与前一个 Action 之间的所有 Transformation 组成一个 Job 进行计算。

常用的执行操作

操作 说明
reduce(func) 通过函数func聚集数据集中的所有元素
collect() 在 Driver 的程序中,以数组的形式,返回数据集的所有元素。
通常会返回一个足够小的数据子集再使用,否则可能出现 OOM
count() 返回数据集的元素个数
take(n) 在 Driver 的程序中,返回一个数组,由数据集的前n个元素组成
first() 在 Driver 的程序中,返回数据集的第一个元素。
效果类似于take(1)
countByKey() 在一个(K,V)对组成的数据集,对每个K进行计数。返回(K, Int)的 Hashmap
foreach(func) 在数据集的每一个元素上,运行函数func

存储执行操作,数据允许保存到本地文件系统、HDFS或者任何其他 Hadoop 支持的文件系统

操作 说明
saveAsTextFile(path) 将数据集的元素,作为文本文件保存。
Spark 将会调用每个元素的 toString 方法,并将它转换为文件中的一行文本
saveAsSequenceFile(path) 将数据集的元素,以 Sequencefile 的格式保存。
数据集元素必须是 Key/Value 类型,并都实现了 Writable 接口(或隐式转换为 Writable)
saveAsObjectFile(path) 使用 Java 序列化将数据集元素写入一个简单格式中,可以用 SparkContext 的 objectfile() 方法加载

RDD 控制操作

控制操作主要包括:故障恢复、数据持久化和移除数据。其中缓存操作 Cache/Persist 是惰性的,进行执行(Action)操作时才会执行;Unpersist 是即时的,会立即释放内存。Checkpoint 会直接将 RDD 持久化到磁盘或 HDFS,与 Cache/Persist 的区别是,Checkpoint 的 RDD 不会因为作业的结束而消除,可以被后续作业直接读取。

RDD 故障恢复

实现分布式数据集容错方法有两种:数据检查点和记录更新。

RDD 采用记录更新的方式,由于记录所有更新的成本很高,所以,RDD 只支持粗颗粒变换,只记录单个块(分区)上执行的单个操作,然后将每个 RDD 的变换序列(血统 lineage)存储下来,即每个 RDD 都包含了它是如何由其他 RDD 变换过来的以及如何重建某一块数据的信息。因此 RDD 的容错机制又称“血统”容错。

要实现这种“血统”容错机制,最大的难题就是如何表达父 RDD 和子 RDD 之间的依赖关系。上面介绍过,依赖关系可以分两种,窄依赖和宽依赖。

计算时,窄依赖可以在某个计算节点上直接通过计算父 RDD 的某块数据计算得到子 RDD 对应的某块数据;宽依赖则要等到父 RDD 所有数据都计算完成之后之后才能计算子RDD。

数据丢失时,对于窄依赖只需要重新计算丢失的那一块数据来恢复;对于宽依赖则要将祖先 RDD 中的所有数据块全部重新计算来恢复。

所以在“血统”链特别是有宽依赖的时候,需要在适当的时机设置数据检查点,减少数据恢复的工作量。

RDD 持久化

在不同转换操作之间,将过程数据数据缓存在内存中,实现快速重用或故障快速恢复。分为主动持久化和自动持久化。

主动持久化,目的是 RDD 重用,能够实现快速处理。可以使用 persist 方法标记一个持久化 RDD,一旦执行 Action 操作,持久化开始生效,在内存中(或其他介质,可以是磁盘)保存计算结果。

自动持久化,不需要用户主动调用方法,Spark 会自动保存一些 Shuffle 操作的中间结果(如 reduceByKey),为了避免 Shuffle 过程中一个节点崩溃导致所有输入重新计算。

存储等级

RDD 的缓存粒度为整个 RDD 对象,可是用不通的级别进行持久化,persist 方法支持传递 StorageLevel 对象选择存储等级,默认是 MEMORY_ONLY

StorageLevel 说明
MEMORY_ONLY 使用未序列化的Java对象格式,将数据保存在内存中。如果内存不够存放所有的数据,则数据可能就不会进行持久化,默认的持久化策略
MEMORY_AND_DISK 使用未序列化的Java对象格式,优先尝试将数据保存在内存中。如果内存不够存放所有的数据,会将数据写入磁盘文件中。不会立刻输出到磁盘
MEMORY_ONLY_SER RDD的每个partition会被序列化成一个字节数组,节省空间,读取时间更占CPU
MEMORY_AND_DISK_SER 序列化存储,超出部分写入磁盘文件中
DISK_ONLY 使用未序列化的Java对象格式,将数据全部写入磁盘文件中
MEMORY_ONLY_2 对于上述任意一种持久化策略,如果加上后缀_2,代表的是将每个持久化的数据,都复制一份副本,并将副本保存到其他节点上
OFF_HEAP RDD序列化存储在Tachyon

持久化时,一旦设置了就不能改变,必须先去持久化 unpersist()

如何选择存储级别?

如果默认能满足使用默认的
如果不能与 MEMORY_ONLY 很好的契合,建议使用 MEMORY_ONLY_SER
尽可能不要存储数据到磁盘上,除非数据集函数计算量特别大,或者它过滤了大量数据,否则从新计算一个分区的速度和从磁盘中读取差不多
如果想拥有快速故障恢复能力,可以使用复制存储级别(_2)
可以自定义存储级别(如复制因子为3)

RDD 与分布式共享内存(DSM)的区别

RDD 是一种分布式的内存抽象,DSM 是一种通用的内存数据抽象。

  • | RDD | DSM
    -- | --- | ----
    读 | 批量或细粒度读 | 细粒度读
    写 | 批量转换操作 | 细粒度转换
    一致性 |RDD 不可改 | 取决于应用程序
    容错性 | 细粒度、低开销使用 Lineage | 需要检查点和回滚
    落后任务的处理 | 任务备份,重新调度 | 无
    任务安排 | 基于数据存放位置自动实现 | 取决于应用程序

RDD 与 DSM 主要区别在于,不仅可以通过批量转换创建 RDD,还可以对任意内存位置读写。RDD 限制应用执行批量写操作,有利于实现有效的容错。由于 RDD 可以使用血统(Lineage)来恢复分区,基本没有检查点开销。失效时只需要统计丢失的 RDD 分区,在不同节点上并行恢复。

RDD 与 DSM 比较有两个优势:

  • 对于 RDD 的批量操作,根据数据存放位置调度数据,提高性能
  • 对于扫描类型操作,如果内存不足以缓存完整的 RDD,可以进行部分缓存,其余部分缓存在磁盘上

References:
《Spark 核心技术与高级应用》
《Spark大数据处理: 技术、应用与性能优化》

你可能感兴趣的:(Spark 编程模型 RDD)