SPARK学习笔记--RDD

学习书籍《图解Spark核心技术与案例实战》–郭景瞻 编著

RDD概述

RDD简介

RDD提供一种基于粗粒度变换的接口,该接口会将相同的操作应用到多个数据集上,这就使得它们可以记录创建数据集的“血统”,而不需要存储真正的数据,从而达到高效的容错性。
当某个RDD分区丢失的时候,RDD记录有足够的信息来重新计算,而且只需要计算该分区,这样丢失的数据可以很快的恢复,不需要昂贵的复制代价。

基于RDD机制的多类模型计算:

  • 迭代计算
    目前最常见的工作方式,比如应用于图处理、数值优化及机器学习中的算法。
  • 交互式SQL查询
  • MapReduceRDD
  • 流式数据处理

RDD特性

RDD是一个弹性的分布式的数据集,是Spark的基本抽象,RDD是不可变的,并且它由多个partition构成(可能分布在多台机器上,可以存memory上,也可以存disk里等等),可以进行并行操作
弹性:分布式计算时可容错
不可变:一旦产生就不能被改变

RDD类型

Spark编程中需要编写一个驱动程序来连接到工作进程。驱动程序定义一个或多个RDD以及相关行动操作,驱动程序同时记录RDD的继承关系,即“血统”。而工作进程是一直运行的进程,它将经过一系列操作后的RDD分区数据保存在内存中。

Spark中的操作大致可以分为四类操作:

  • 创建操作
    创建RDD,共有两种方式。
    • 来自于内存集合和外部存储系统
    • 通过转换操作生成
  • 转换操作
    将RDD通过一定操作转换成新的RDD
  • 控制操作
    进行RDD持久化,可以让RDD按不同的存储策略保存在磁盘或者内存中,比如cahe接口默认将RDD缓存在内存中
  • 行为操作
    能够触发Spark运行的操作,例如,对RDD进行collect。Spark中行动操作分为两类
    • 操作结果变成Scala集合或者变量
    • 将RDD保存到外部文件系统或者数据库中

RDD的实现

作业调度

对RDD执行转换操作时,调度器会根据RDD的“血统”来构建由若干调度阶段(Stage)组成的有向无环图(DAG),每个调度阶段包含尽可能多的连续窄依赖转换。调度器按照有向无环图顺序进行计算,并最终得到目标RDD

就近原则,调度器向各个节点分配任务采用延迟调度机制并根据数据存储位置(数据本地性)来确定。若一个任务需要处理的某个分区刚好存储在某个节点的内存中,则该任务会分配给该节点;如果不在,则找到较佳位置,并分配给所在节点。

对应宽依赖的操作,在Spark将中间结果物化到父分区的节点上,这和MR物化map的输出蕾丝,可以简化数据的故障恢复过程。

对于执行失败的任务,只要它对应调度阶段父类信息仍然可用,该任务会分散到其他节点重新执行。
如果某些调度阶段不可用,则重新提交相应的任务,并以并行方式计算丢失的分区。
在作业中如果某个任务执行缓慢(即Straggler),系统则会在其他节点上执行该任务的副本,并取最先得到的结果为最终结果。

内存管理

Spark提供了3中国持久化RDD的存储策略

  • 未序列化Java对象存在内存中,性能最优,因为可以直接访问在Java虚拟机内存里的RDD对象
  • 序列化的数据存于内存中,在空间有限的情况下,可以让用户采用比Java对象更有效的内存组织方式,但代价是降低了性能
  • 存储在磁盘中,使用与RDD太大的情形,每次重新计算该RDD会带来额外的资源开销(如I/O等)。

对于内存使用LRU回收算法来进行管理,当计算得到一个新的RDD分区,但没有足够空间来存储时,系统会从最近最少使用的RDD回收其一个分区的空间。除非该RDD是新分区对应的RDD,这种情况下Spark会将旧的分区继续保留在内存中,防止同一个RDD的分区被循环调入/调出。

检查点支持

虽然“血统”可以用于错误后RDD的恢复,但是如果“血统”过长,则耗时较长,因此需要通过检查点操作(Checkpoint)保存到外部存储中。对于包含宽依赖长的“血统”的RDD设置检查点操作是非常有用的。窄依赖就不是必须的

用户可自行决定需要为哪些数据设置检查点操作。由于RDD的只读性,使得不需要关心数据一致性问题,比常用的共享内存更容易做检查点

编程接口

Spark中提供了通用接口来抽象每个RDD。

  • 分区信息
    他们是数据集的最小分片
  • 依赖关系
    指向其父RDD
  • 函数
    基于父RDD的计算方法
  • 划分策略和数据位置的元数据
    例,一个HDFS文件的RDD将文件的每个文件块表示为一个分区,并且知道每个文件块的位置信息,当对RDD进行map操作后分区将具有相同的划分。
操作 含义
Partitions() 返回分片对象列表
PerferredLocations(p) 根据数据的本地特性,列出分片p的首选位置
Dependencies() 返回依赖列表
Iterator(p,parentIters) 给定p的父分片的迭代器,计算分片p的元素
Partitioner() 返回说明RDD是否是Hash或者是范围分片的元数据

RDD分区(Partitions)

RDD划分成很多的分区分布到集群的节点中,分区的多少涉及对这个RDD进行并行计算的粒度。
分区是一个逻辑概念,变换前后的新旧分区在物理上可能是同一块内存或存储,这种优化防止函数式不变性导致的内存需求无限扩张。
在RDD操作中用户可以使用Partitions方法获取RDD划分的分区数,用户也可以设定分区数目。
如果没哟鱼制定将使用默认值,而默认熟知是该程序分配到的CPU核数,如果是从HDFS文件创建,默认为文件的数据块数。

RDD首选位置(PreferredLocations)

在Spark形成任务有向无环图(DAG)时,会尽可能地把计算分配到靠近数据的位置,减少数据网络传输。
当RDD产生的时候存在首选位置,如HadoopRDD分区的首选位置就是HDFS块所在的节点;
当RDD分区被缓存,则计算应该发送到缓存分区所在的节点进行,再不然回溯RDD的“血统”一直找到具有首选位置属性的父RDD,并据此决定子RDD的位置。

RDD依赖关系(Dependencies)

在RDD中将依赖划分成了两种类型:窄依赖和宽依赖。
窄依赖是指每个父RDD的分区都至多被一个子RDD的分区使用,而宽依赖是多个子RDD的分区依赖一个父RDD的分区。例,map操作是一种窄依赖,而join操作是一种宽依赖(除非父RDD已经基于Hash策略被划分过了)。
SPARK学习笔记--RDD
这两种依赖的区别从两个方面来说比较有用。

  • 窄依赖允许在单个集群节点上流水线式执行,这个节点可以计算所有父级分区。相反,宽依赖需要所有的父RDD数据可用,并且数据已经通过类MR的操作shuffle完成
  • 在窄依赖中,节点失败后的恢复更加高效。因为只有丢失的父级分区需要重新计算,并且这些丢失的父级分区可以并行地在不同的节点上重新计算。相反,在宽依赖做的继承关系中,单个失败的节点可能导致一个RDD的所有先祖RDD中的一些分区丢失,导致计算的重新执行

RDD分区计算(Iterator)

Spark中RDD计算是以分区为单位的,而且计算函数都是在迭代器复合,不需要保存每次计算的结果。
分区计算一般使用mapPartitions等操作进行,mapPartitions的输入函数是应用于每个分区,也就是把每个分区中的内容作为整体来处理,以分区为单位。

RDD分区函数(Partitioner)

分区划分对于Shuffle类操作很关键,它决定了该操作的父RDD和子RDD之间的依赖类型。
例如Join操作,如果协同划分的话,两个父RDD之间、父RDD与子RDD之间能形成一致的分区安排,即同一个Key保证被映射到同一个分区,这样就能形成窄依赖。反之,如果没有协同划分,导致宽依赖。
这里所说的协同划分是指定分区划分器以产生前后一致的分区安排。

在Spark默认提供两种划分器:HashPartitioner和RangePartitioner,且Partitioner只存在于(K,V)类型的RDD中,对于非(K,V)类型的Partitioner值为None。