RDD简述

  (简单介绍一下DPark的特点、优缺点,适用范围,和其他一些计算框架比较等) DPark是Spark的Python实现版本,所以其具有与Spark相同的优缺点以及适用范围,这里只做简单介绍,详细可以参考Spark官方网站。

  Spark最核心的概念是RDD,近年来,有关集群运算的编程框架和模型例如MapReduce, Dryad等正在被大量运用于处理不断增长的数据量,这些系统具有容错、平衡负载等优点,使得大部分用户都可以使用这些系统进行大数据的处理。但是几乎所有的现代集群计算系统都是基于非循环式的数据流模型,意味着每一次的计算过程都必然包含着从存储中读取数据然后计算完成之后写入结果的过程,这样的模型使得那些需要重复使用一个特定的数据集的迭代算法无法很高效的运行,RDD和Spark正是为了解决这一类问题而诞生的。

  RDD的设计理念是在保留例如MapReduce等数据流模型的框架的优点的同时(自动容错、本地优化分配(locality-aware scheduling)和可拓展性),使得用户可以明确地将一部分数据集缓存在内存中,以大大加速对这部分数据之后的查询和计算过程。

  RDD可以被认为是提供了一种高度限制(只读、只能由别的RDD变换而来)的共享内存,但是这些限制可以使得自动容错的开支变得很低。RDD使用了一种称之为“血统”的容错机制,即每一个RDD都包含关于它是如何从其他RDD变换过来的以及如何重建某一块数据的信息,这个在后面会进行一下详细的介绍。

  在RDD之前,也有一些模型被创造出来解决数据流模型中存在的缺点,例如Google的Pregel(迭代图计算框架)、Twister和HaLoop(迭代MapReduce框架)等,但是这些系统只能被应用于一些非常有限和特殊的场景当中。而RDD提供了一种更为通用的迭代并行计算框架,使得用户能够显式的控制计算的中间结果,然后将其自由的运用于之后的计算中。

  RDD的发明者已经在Spark的基础上实现了Pregel(100行Scala代码)以及迭代MapReduce框架(200行Scala代码),并在那些其他框架都无法很好适用的一些场景之中取得了一些很好的成果,例如交互式的大数据查询等。一些实际的实践和研究表明,Spark在那些迭代式计算中比Hadoop快20倍,能够在5-7s的时间内交互式的查询1TB的数据(比Hadoop快40倍)。

RDD的容错机制

  通常而言,有两种实现分布式数据集容错机制的方法:数据检查点或者记录更新。就RDD设计的使用场景(大数据计算分析)而言,设置数据的检查点的花费会很高,因为它意味着在各个机器之间复制大的数据集,然而考虑到网络带宽相比机器内的带宽要小很多,这种拷贝操作会变得相当缓慢,而且这种拷贝会消耗大量的存储资源。

  因此我们选择使用后一种机制,即记录更新。然而,记录所有更新的成本也会很高,所以RDD仅支持粗颗粒度变换(coarse-grained transformation),即仅记录在单个块上执行的单个操作,然后创建某个RDD的变换序列存储下来,当数据丢失时,我们可以用变换序列(血统)来重新计算,恢复丢失的数据,以达到容错的目的。

  当然,当变换序列变得很长的时候,重新计算以恢复丢失的数据所需的时间会变得很长,所以我们建议用户在RDD的变换序列变得很长的时候,建立一些数据检查点以加快容错的速度。DPark目前并没有提供自动判断是否需要设置数据检查点的功能,但是我们考虑在未来的升级版本中加入这一功能以实现更快更好的容错机制,目前,用户可以通过saveAsTextFile方法来手动设置数据检查点。

RDD内部的设计

  在DPark中,为了使得RDD能够在未来支持更多的变换而且不需要每次都去改变任务调度机制,同时为了“血统”容错机制的实现,我们为RDD设计了几个共通的接口。简单来说就是,
  
1. 每个RDD都需要包含源数据分割之后的数据块,源代码中的splits变量。
2. 关于“血统”的信息,即这个RDD所依赖的父亲RDD以及两者之间的关系,源代码中的dependencies变量
3. 一个计算函数,即这个RDD如何通过父亲RDD计算得到,源代码中的iterator(split)和compute函数
4. 一些关于如何分块和数据所放位置的元信息,例如源代码中的partitioner和preferredLocations

  举例来说,一个从分布式文件系统中的文件得到的RDD具有的数据块是通过分割各个文件之后得到的,而它没有父RDD,它的计算函数只是读取文件中的每一行并作为一个元素返回给子RDD,而对于一个通过map函数得到的RDD,它会具有和父RDD相同的数据块,它的计算函数是对每个父RDD中的元素调用了一个函数。

  在上述的四个接口中,如何表达父亲和儿子之间的依赖关系是Spark以及DPark在实现的时候考虑最多的问题,最终我们发现可以将依赖关系分为两类:窄依赖和宽依赖。所谓的窄依赖是说子RDD中的每一个数据块只依赖于父RDD中的对应的有限个固定的数据块,当然这个依赖的数量要和父RDD总的数据块规模相差比较大;而宽依赖就是指子RDD中的一个数据块可以依赖于父RDD中的所有数据块。举例来说,对于map变换来说,子RDD中的数据块就只依赖于父RDD中对应的一个数据块,也就是说它是个窄依赖;而对于groupByKey变换来说,子RDD中的数据块会依赖于所有父RDD中的数据块,因为一个key可能存在于父RDD中的任何一个数据块中。

  这样的分类方式有两个很重要的特性,也是这两个特性要求了我们对于这两种不同的依赖需要采用不同的任务调度机制和容错恢复机制。第一,窄依赖意味着可以在某一个计算节点上直接通过父RDD的某几块数据(通常是一块)计算得到子RDD某一块的数据;而相对的,宽依赖意味着子RDD某一块数据的计算必须等到它的父RDD所有数据都计算完成之后才可以进行,而且需要对父RDD的计算结果进行hash并传递到对应的节点之上。第二,当某一个计算节点出错的时候,窄依赖的错误恢复会比宽依赖的错误恢复要快很多,因为对于窄依赖来说,只有丢失的那一块数据需要被重新计算,而宽依赖意味着所有的祖先RDD中所有的数据块都需要被重新计算一遍,这也是我们建议在长“血统”链条特别是有宽依赖的时候,需要在适当的时机设置一个数据检查点以避免过长的容错恢复。