Spark论文研究之-一篇文章彻底弄清RDD

2.2 RDD抽象

2.2.1 RDD说明

  • RDD是啥?
      形式上,RDD是一种只读的分区记录集合。是一种具有容错能力的并行数据结构。使用户可以显式地将数据存在磁盘上或是内存中,控制其分区,并使用丰富的运算符来操作数据。

  • RDD怎么产生?
      只能通过两种方式产生:确切的讲只能通过RDD 转换操作产生

    • 确定的操作作用于稳定存储的数据上
    • 确定的操作作用于其他RDD
  • RDD产生动机?
      在上一篇文章中已经提到,Spark研究者发现现有的处理系统其最大的共性问题是数据不能高效共享(利用外部存储来进行数据共享),有些系统即使在这方面做了优化,但也只支持特定的模式。

  • 一般的内存存储抽象是怎么容错的?
      跨节点复制数据或者通过日志来记录跨节点的更新,然而这种方式对于数据敏感性的应用来说是昂贵的,一方面这需要再集群网络上大量复制数据,而其带宽又远小于RAM,另一方面,这又占用了大量的存储空间。

  • RDD是怎么容错的?
      RDD提供了基于粗粒度的转换接口,能够将相同的运算操作应用到大量数据记录上。于是,只需要记录产生RDD的转换即可,也就是官方所说的lineage,而不需要记录实际数据。这样一来,每当有分区丢失的时候,RDD有记录足够的关于它是从哪些RDD计算得来的信息,然后据此重新计算丢失分区即可。

  • 其他
      RDD有足够的信息来说明它是如何从其他数据集(其谱系)派生的,以便从稳定存储中的数据计算其分区。这是一个强大的属性:实质上,程序不能引用在失败后无法重建的RDD。
      用户可以控制RDD的其他两个方面:持久化和分区。用户可根据将来的使用需要为RDD选择一种存储策略(eg:是存在内存还是存在磁盘),用户还可以依据每个记录中的key值对RDD的元素进行跨机器分区。

2.2.2 Spark编程接口

  Spark通过类似于DryadLINQ和FlumeJava的语言集成API来公开其RDD(用户可用),其中每个RDD都被当做一个对象,并利用这个对象的方法调用转换。
  Spark的方法分为两种,一种为transformations(eg: map 、filter),另外一种为actions(eg:count、collect、save)。用户通常可以通过transformations操作从稳定的存储数据中创建RDD,然后可以在action中使用这些数据集。跟DryadLINQ一样,Spark对RDDs执行懒计算,也就是说只有当第一次遇到要使用RDDs的情况下(action操作)Spark才开始执行RDDs计算。因此,Spark可以对所有transformations进行管道处理。
  在预料到某个RDD在将来会被再使用的情况下,Spark提供了persist接口来持久化该RDD,默认情况下Spark将持久化的RDD保存在内存中,在RAM不够的情况下会溢出到磁盘。spark提供了不同的持久化策略,用户可以选择是仅仅持久化到磁盘上,或者对持久化RDD进行跨机器复制。这些都通过persist的flag来完成。除此以外,用户还可以选择持久化优先级,以此来决定存储到内存的RDDs应该最先溢出到磁盘。
  
  下面来看看spark api是怎么使用的以及模型是怎么容错的

lines = spark.textFile("hdfs://...")
errors = lines.filter(_.startsWith("ERROR"))
errors.persist()

注意第三行,这一操作使errors被持久化到内存中,后续查询就可共享该数据。另外还需要注意的是直到这里,Spark还没有任何工作开始运行。通过执行一个action操作例如errors.count()后,用户便可以使用该RDD。通过将errors的分区保存在内存中,大大加速了后续基于此RDDs的计算。
RDD谱系图

图2.1 RDD谱系图(沿袭图)



  在这个例子中,Spark 调度器会把后两步转换进行一个组合成一个管道。然后发送一组任务到缓存有errors分区的节点来计算该转换。如果errors的一个分区丢失,spark会将filter(_.startsWith(“ERROR”))仅仅应用到相应的lines的分区上来重计算丢失分区。

2.2.3 RDD模型的优势

RDDs和分布式共享内存的比较
        图 3.1 RDDs和分布式共享内存的对比


1、可以看到,RDD和DST之间主要的不同在于,RDD只能通过粗粒度的转换方式来创建。而DST允许在内存的任意位置读写。这将RDD限制为执行批量写入的程序,但这也允许更高效的容错。RDD利用谱系图来进行故障恢复,不会带来检查点开销。而且,只有丢失的分区需要被重新计算,重计算任务也可以多节点并行执行,无需回滚程序。
2、RDD的第二个益处在于它的不可变特性,这种特性使得可以运行处理缓慢节点的任务的备份来缓解节点任务执行落后的状况。
3、相对于DSM,RDD还有另外两方面的好处。首先,在RDD的批量操作中,在运行时可以根据数据位置来调度任务以提高性能。然后,在RDD用于扫描操作的时候,RDD可以优雅的降级,也就是在内存不够的情况下,会溢出到磁盘。

2.2.4 不适合RDDs的应用


  首先需要明确的是,RDD的目标是批处理应用,也就是将同一个操作应用到一个数据集的每个元素上。在这样的情况下,RDD才能将每一个转换操作作为执行谱系图中的一个步骤有效的记录下来。以便于在恢复丢失分区时不需要记录庞大的数据信息。所以,RDD并不是适合进行异步细粒度更新的应用程序,比如,web应用的存储系统或者是网络爬虫程序。


扫描二维码关注公众号,回复: 6516584 查看本文章

2.3 Spark编程接口


1、为什么选择scala来编写spark?

  官方解释:因为scala是简洁性(体现在交互上的便利性)和高效性(静态类型)的结合。
2、怎么使用?
  开发者使用driver(驱动程序)连接到集群workers(工作程序),driver定义RDD并且在其上调用action。driver上的spark代码也追踪RDD沿袭。workers是长期存在的进程,可以跨操作在RAM中存储分区。
  用户给RDD操作如map提供参数,通过传入闭包。Scala将每个闭包视为一个java Object,这些对象可以被序列化并可以被加载到另一个节点上以便于把闭包在网络间传输。Scala也将跟闭包绑定的任何变量当做域保存在java Object中。
  RDD是由其元素类型参数化的静态类型对象。eg:RDD[int]表示integer类型的RDD,但是,由于Scala的类型推断特性,一般情况下我们都将类型省略。
  尽管用scala把RDD公开的方法的概念是简单的,但是挑战是通过反射的方式处理Scala闭包对象所带来的问题。而且,让Scala解释器可以使用spark也需要做很多工作。

2.3.1 Spark中的RDD操作

spark中的rdd操作

图5.1 Spark中的一些RDD可用操作



注意:transformations是懒执行运算。
  需要注意的是,有些操作算子例如join仅仅对键值对的RDD可用。函数名称也与Scala和其他的函数式语言相匹配。例如,map是一对一的映射,flatMap将每一个输入映射为一个或者多个输出。
  另外,用户可以使用persist算子将RDD持久化,此外,用户还可以获得一个用Partitioner类表示的RDD的分区顺序,并可以用这个Partitioner类对另一个数据集分区。诸如groupBykey、reduceBykey、sort等均会产生按hash或者范围分区的RDD。即自动重分区。

2.3.2 应用示例

  • 逻辑回归
      这是一个通用分类算法,用来搜索一个能够将两组点分离开的最佳超平面。该算法使用梯度下降:w以一个随机值开始,在每一个迭代过程中,对数据的w函数值进行求和,以便往改善w的方向移动w。
val points = spark.textFile(...).map(parsePoint).persist()
var w = // random initial vector
for (i <- 1 to ITERATIONS) {
val gradient = points.map { p =>
p.x * (1 / (1 + exp(-p.y * (w dot p.x))) - 1) * p.y
}.reduce((a,b) => a + b)
w -= gradient
}

  通过map 转换操作,作用于一个text文本将每一行解析成一个Point对象,并将其持久化,得到一个持久化的RDD。然后对points在每一次迭代中重复执行map和reduce来计算梯度(通过对当前w的函数值求和),通过将points持久化到内存中使得速度得到20倍的提升。

  • 网页排名
    网页排名中涉及稍微复杂点的数据共享模式。该算法通过将链接到该篇文档的贡献值相加来迭代更新一篇文档的rank,在每一次迭代过程中,每一篇文档发送一个r/n的贡献值给他的邻居文档。其中r是文档排名rank,n是其邻居数。然后文档将其rank更新为a/N+(1-a)*sum(Ci),求和部分表示文档接收到的contribution求和。N是文档总数。a是可调参数。
// Load graph as an RDD of (URL, outlinks) pairs
val links = spark.textFile(...).map(...).persist()
var ranks = // RDD of (URL, rank) pairs
for (i <- 1 to ITERATIONS) {
// Build an RDD of (targetURL, float) pairs with contributions sent by each page
val contribs = links.join(ranks).flatMap {
case (url, (links, rank)) =>
links.map(dest => (dest, rank/links.size))
}
// Sum contributions by URL and get new ranks
ranks = contribs.reduceByKey((x,y) => x+y).mapValues(sum => a/N + (1-a)*sum)
}

再看看网页排名的谱系沿袭图
pagerank

图6.1 PageRank谱系图



  在每一次的迭代中,都基于上一次迭代的ranks、contribs结果及静态数据links数据集生成了一个新的ranks数据集。可以看出这个图谱会随着迭代次数的增加而变长。因此为了减少故障恢复时间,迭代过程中的部分版本的ranks可靠地保存起来是必要的。用户可以调用persist操作并传入RELIABLE标识来进行这个保存操作。但是需要注意的是,links数据集不需要复制操作。因为在输入数据集上进行一个map操作就可以将links分区有效重建。links数据集比ranks要大的多,因为每个文档都有很多link,但只有一个rank。因此利用lineage的方式恢复它要比使用检查点记录程序的完全内存状态要节省很多时间。
  最后,可以通过控制RDD的分区来优化PageRank中的通信。我们可以将links和ranks以相同的方式分区,使得links和ranks之间的join不需要通信。也可以编写一个自定义的Partitioner类来将相互链接的页面进行分组。eg:根据URLs的域名将其分组。只需要使用partitionBy即可:

links = spark.textFile(...).map(...).partitionBy(myPartFunc).persist()

  在进行这一操作之后,links和ranks之间的join操作将自动每个URL的贡献值聚合到其链接列表所在的计算机上,并重新计算其排名,并和其链接链接起来。这种跨迭代的一致分区是Pregel等专用框架的主要优化之一。RDD能让用户直接表达这一目标。

表示RDD的接口

图6.2 Spark中用于表示RDD的接口


2.4 RDD的表示

  提供RDD作为抽象的挑战之一是为他们选择一种可以追踪各种转换操作的谱系的表示。同时,一个使用RDD的系统应该尽可能的提供多的转换算子,并且可以让用户任意组合使用他们。我们使用一种简单的基于图的RDD表示来促进这一目标。在spark中使用了这种表示方法,用来提供丰富的转换操作而不需要为调度器添加特殊的逻辑,这大大简化了系统设计。

 Spark使用一种公共接口表示RDD,这个公共接口公开五条信息:

  • 一组分区,这是数据集的原子部分
  • 对父RDD的一组依赖关系
  • 基于父RDD来计算该数据集的函数
  • 有关其分区方案的元数据
  • 数据位置

 例如,一个表示HDFS上文件的RDD具有每个块的分区,并且知道每个块都在哪个机器上。此RDD上的map结果具有相同的分区,但是在计算元素的时候讲map应用于父级数据。
将依赖关系划分为两类已经足够并且有用:

窄依赖:父RDD的每一个分区最多被一个子RDD的一个分区使用
宽依赖:父RDD的每一个分区被多个子RDD的分区使用

窄、宽依赖

图7.1 窄依赖、宽依赖,每个方框代表RDD,阴影矩形代表分区

例如:map操作是窄依赖的,而join操作是宽依赖的(除非父RDD是hash分区的)

  这一特性在两方面很有用,首先,窄依赖操作可以在一个集群节点上以管道的方式执行,能计算所有的父分区。例如可以逐个元素地应用map然后再应用filter。相反,宽依赖要求所有父分区的数据可用,并且使用类似MapReduce操作在节点间进行洗牌操作。其次,在节点故障恢复时,窄依赖操作的恢复效率更高,由于仅仅是丢失的父分区需要重新计算,同时可以多节点并行计算。相反,在一个宽依赖的谱系图中,单节点失败可能导致RDD的所有的祖先丢失某些分区,从而需要完全重新执行。
  RDD的这个通用接口使得用少于20行的代码中实现Spark中的大多数转换成为可能。使用这些接口实现转换并不需要知道调度器的调度细节。
  HDFS 文件:例子中的输入RDD是HDFS文件,文件的每一个block就是一个partition(每个block的偏移都保存在partition中)。preferredLocations给出块所在的节点,然后迭代器读取块。
  map: 在任何RDD上调用map都返回MappedRDD对象,这个对象和其父RDD具有相同的分区和首选位置。但将传递给map的函数应用于在其迭代器方法中的父RDD记录。
  union: 在两个RDD上调用union,返回一个RDD,结果分区是这两个父分区的union,每个子分区通过对一个父分区的窄依赖来计算。
  sample: 采样类似于map,除了RDD为每个分区保存一个随机数生成器种子用以确定性的对父记录采样。
  join: 对两个RDD进行join会出现要么两个窄依赖(两个RDD都采用同一个哈希/范围分区器进行分区的)、两个宽依赖、或者一个混合的(一个有partitioner而另一个没有)。任意一种情况下,输出都有一个partitioner(或者是从父RDD继承的或者是默认的哈希分区器)。

2.5 实现

  Spark 用了大概34000行scala语言实现。可以在多种集群管理器上运行,包括Apache Mesos、Hadoop Yarn、Amazon EC2或者其内置集群管理器。每个Spark程序都作为一个独立应用在集群上运行,有它自己的driver(master)和workers。应用之间的集群资源共享问题由集群管理器来解决。
Spark可以使用Hadoop现有的插件API从任何Hadoop数据源读取数据,并在未经修改的Scala版本上运行。

2.5.1 任务调度

  Spark调度器也用了RDD表示。
  Spark调度器与Dryad的调度器相似,只不过Spark调度器考虑了内存中RDD的哪个分区可用。当用户对一个RDD执行一个action操作的时候(eg:count或者save),调度器便检查RDD的谱系图来构建一个面向stage阶段的DAG(有向无环图)来执行,每个stage包含以管道方式组织的尽可能多的窄依赖转换操作,直到出现需要洗牌的宽依赖操作、或者已经计算好的分区可以使父RDD的计算短路时,便出现一个stage分界,这个操作便被划分到下一个stage中。然后调度器就开始启动任务来计算每个stage中丢失的分区,直到目标RDD被计算完成。
  Spark计算任务stage示例
  

图2.5 Spark 计算任务Stage的示例

  图中,实线方框表示RDD,阴影矩形表示分区,如果分区已经在内存中则用黑色表示。为了在RDD G上运行一个action,以宽依赖为分界,并在每一个stage中管道化其转换操作,以此构建DAG stage。在这个案例中,stage 1的结果已经在内存中,所以,直接运行stage2 和 stage3。
  
  Spark调度器使用延迟调度策略并依据数据本地性来将任务发送到RDD所在的机器上。如果任务需要处理一个分区,这个分区在某个节点的内存中,我们就将该任务发送到那个节点。如果任务需要处理一个RDD提供了首选位置的分区(eg:HDFS 文件),Spark调度器会将任务发送到这些首选位置进行计算。
  对于宽依赖,目前是将中间结果物化到持有父分区的节点上,以此来简化故障恢复。正如MapReduce物化Map的输出一样。
  如果一个任务失败,调度器会在另一个节点上重新运行它,只要该stage的父stage还可用,如果有stage变得不可用(eg:比如shuffle操作的map输出丢失),调度器重新调度任务以并行方式重计算丢失的分区。我们未对调度器失败进行容错,尽管复制RDD的沿袭图会很简单。
  如果任务运行缓慢(即落后者),调度器会其它节点启动一个推测性备份副本,如MapReduce那样,谁先完成便使用谁的输出。
  尽管Spark中的计算都是为响应driver程序中调用的action操作而运行的,我们正在尝试让在集群上运行的任务调用lookup操作,这一操作提供对按键进行hash分区的RDD的随机访问,task需要在失败时告诉调度器计算所需分区。

2.5.2 多租户

  RDD模型将计算拆分成独立的、细粒度的任务,因此它允许集群多租户资源共享。在执行期间,每个RDD应用可以动态扩展和收缩,应用可以轮流访问每台机器,也可被高优先级的应用抢占。Spark任务大多都在50ms到几秒之间,从而实现高度响应的共享。

  • 在每个Spark应用中,允许多线程并发提交job,资源分配方式类似于Hadoop 公平调度的分层公平调度方式。这一特性主要用于在相同的内存数据上构建多用户应用,例如Spark SQL的服务引擎模式,多用户可以同时运行查询。公平调度使得job之间相互隔离,同时短任务能够很快的返回,即使长任务占据了整个集群资源。
  • Spark公平调度器也使用延迟调度来保证数据本地性同时保持公平性,通过让任务轮流访问每台机器上的数据来实现。Spark支持多种情况的数据本地性,包括内存、磁盘、机架,以此掌控整个集群中数据访问的不同成本。
  • 由于tasks是相互独立的,调度器支持取消job来为高级别的job腾出空间。
  • 在spark应用之间,通过mesos中的资源分派概念,Spark依然支持细粒度的资源共享,这使得不同的应用可以用相同的API来在集群上提交细粒度任务。这允许Spark 应用之间以及Spark应用与其他计算框架如Hadoop进行动态资源共享。提供数据本地性的延迟调度工作在资源分派模型中依然可以正常进行。
  • Spark已经扩展为使用Sparrow system执行分布式调度,这一系统允许多Spark应用程序以去中心化的方式在同一集群上对工作进行入队,同时提供数据本地性、低延迟、以及公平性。分布式调度在多应用并发提交job时通过避免一个中心化的Master的方式极大地提高了可扩展性。

  由于大多数的集群都是多租户的,并且运行其上的工作负载变得越来越具有交互性。这些功能使Spark比传统的集群静态分区具有显著的性能优势。

2.5.3 集成解释器

  Scala包含一个和ruby和python类似的可交互的shell,通过内存数据实现低延迟,Spark希望通过解释器为用户查询大数据集提供可交互性。
Spark interpreter

图2.6 Spark 解释器将用户输入行转化成Java 对象示例

  Scala解释器把用户键入的每一行编译成一个类,加载到JVM中,然后在其上调用一个函数。这个类包含一个含有该行中的变量或者函数的单例对象,并在一个初始化方法中运行该行的代码。举个例子,如果一个用户键入var x=5 仅跟着一行println(x),解释器定义一个叫Line1的类,包含变量x,然后第二行就编译为println(Line1.getInstance().x)。

  在Spark中做了两方面的改动:
  1、发送class:为了能使worker节点拉取在每行上创建的类字节码,spark使用HTTP为这些类提供服务。
  2、修改字节码生成:正常情况下,为每一行代码创建的单例对象是通过其相应类上的静态方法来访问的。这意味着当我们序列化一个引用了在前一行定义的变量的闭包的时候,java不会跟踪对象图来发送包装x的Line1实例,因此,工作节点不会受到x。Spark中修改了字节码生成逻辑,直接引用每行对象的实例。

2.5.4 内存管理

  Spark为持久化RDD提供了三种选择:内存中的原始的Java 对象、内存中的序列化的数据、磁盘存储。第一种选择具有最快性能,因为JVM可以本机直接访问每个RDD元素。在空间有限的情况下,第二种选择提供更节省内存的表示相比于Java 对象图,访问性能稍差一些。在RDD特别大而且RAM放不下同时在需要用时重计算代价较大时使用第三种选择。
  为了管理有限的内存资源,我们在RDD层面上实施LRU赶出策略,当一个新的RDD被计算出来而又没有足够内存存储它的时候,我们就从内存移出最近最少使用的RDD的一个partition,除非这与具有新分区的RDD相同。在这种情况下我们将旧分区保留在内存中,以防相同RDD的分区循环进出。因为大多数操作都是在整个RDD上运行任务,所以,已经存在于内存中的分区在将来某个时刻被需要是非常可能的。目前这种默认策略在所有应用中工作良好,但Spark仍然通过“持久化优先级”为用户对每一个RDD提供进一步控制。
  目前,集群上的每个Spark实例都有其各自独立的内存空间,在将来的工作中,我们计划通过统一内存管理起来在Spark实例之间共享RDD。

2.5.5 支持检查点

  尽管沿袭图总是可以被用于失败之后恢复RDD,但是在沿袭图链条很长的情况下,恢复是耗时的。所以,必要时将某些RDD设置成检查点存储到稳定存储中会有帮助。
  通常,检查点对具有长沿袭图并包含宽依赖的RDD是很有用的,比如之前PageRank示例中的rank数据集。在这个情况下,集群中的某个节点失败,可能会导致来自每个父RDD的某些分片的丢失,这时必须进行整个重计算。相反,对于在稳定存储中的在数据上是窄依赖关系的RDD,如前逻辑回归例子中的points和PageRank中的links,检查点没有价值。集群上的某个节点失败,从这些RDD上丢失的分区可以在其他节点进行并行重计算,而复制整个RDD的成本只是一小部分。
  Spark提供设置检查点的API(通过一个持久化REPLICATE flag),而哪些数据需要设置检查点由用户自己决定。然而,我们也在研究如何自动实施检查点。因为Spark调度器知道数据集的大小和第一次计算该数据集所花的时间。它应该能为检查点选择一组最佳RDD,用以大大缩短系统恢复时间。
  RDD的只读特性使得对其设置检查点比进行内存共享要简单,因为不需要考虑一致性,RDD可以后台写出而不需要程序暂停或者进行分布式快照方案。

2.6 评估

本节主要是对Spark性能表现的一些展示及官方总结。

  • 在进行迭代式机器学习和图计算的时候,Spark要比Hadoop快最高80倍,主要原因在于,Spark通过内存存储Java Object避免了I/O以及反序列化对象带来的开销。
  • 特别地,用Spark来进行数据分析通常会比在Hadoop上运行快40倍。
  • 在节点失败时,Spark能够通过仅计算丢失分区很快修复失败。
  • Spark能被用于1TB的数据交互,延迟仅仅在5-7s。

2.6.1 迭代式机器学习应用

evaluation

图2.7 在100个节点的集群上通过Spark、Hadoop、HadoopBinMem对100G数据执行逻辑回归和k-邻近算法的时间对比

HadoopBinMem:这是Hadoop的一个部署,在第一次迭代时将输入数据转化成二进制格式以消除在后续迭代过程中的文本解析操作,并将其存储在HDFS上。
注:k-邻近算法是计算密集型的,而逻辑回归算法不是,因此逻辑回归算法对I/O和反序列化时间更敏感。

  • 第一次迭代:都要以从HDFS上读取text文件开始。Spark比Hadoop快,但差距不是巨大,这一差距主要是由于Hadoop有主从节点进行心跳通信的信令开销。HadoopBinMem最慢,因为它要运行一个额外的MapReduce来将数据转换为二进制,并且跨集群节点进行复制存储(HDFS存储)。
  • 后续迭代:从图中可以看出,对于逻辑回归,Spark分别比Hadoop和HadoopBinMem快85和70倍,对于k-邻近算法,Spark分别比Hadoop和HadoopBinMem快26和21倍,可以看出Spark RDD模型在其他系统对I/O,序列化涉及较多的情况下,优势十分明显。
  • 在这几种计算框架中Spark快的原因:
    –Hadoop软件堆栈的最小开销
    –提供数据服务时HDFS开销
    –将二进制记录转换成为可用的Java Object内存对象的成本
    官方测试,对于一个无任何计算操作的Hadoop job,从job建立、开始到清理,最少需要25s。对于HDFS开销,在提供每个块服务的时候,HDFS要执行多个内存拷贝并生成一个校验和。在逻辑回归算法中,执行二进制反序列化的步骤耗费的时间比耗费在计算上的时间还长,这样解释了为什么HadoopBinMem最慢。

2.6.2 PageRank

  分别用Hadoop和Spark对54GB的维基百科数据进行网页排序。对PageRank算法进行十次迭代,处理近400万文章的link。
Hadoop和Spark处理网页排名对比

图2.8 Hadoop和Spark处理网页排名算法的性能对比

  在30个节点时,单独的内存存储使Spark处理速度比Hadoop提升了2.4倍。控制RDD的分区以使其在迭代中保持一致,将加速提升到了7.4倍。当扩大集群数量时,处理速度也得到了近线性提升。

2.6.3 故障恢复

故障恢复

图2.9 在发生失败的情况下k-邻近算法的迭代时间,在第六次迭代开始的时候一个节点被kill,引发用沿袭图对部分分区重建

  这一评估是模拟了k-邻近算法的迭代过程中节点失败时根据沿袭图重建RDD分区的开销。75个集群节点对k-邻近算法进行10次迭代,在没有失败的情况下,每次迭代由400个任务组成并处理100GB数据。

  在节点被kill之后,该节点上的tasks停止,存储在该节点的分区丢失。这个时候,Spark会在其他节点以并行方式重新运行这些任务,通过沿袭图重新读取相应输入数据并重建丢失的RDD分区。(这里要注意,Spark在选择节点重新执行的时候是会考虑数据本地性)。
  注意,如果设置基于检查点的错误恢复机制,错误恢复至少要执行几次迭代过程,这取决于检查点设置的频度。系统需要跨网络复制100G的数据。用Spark时,需要消耗两倍的内存来将检查点数据复制到内存中,或者是必须等待时间使数据复制到磁盘上。相反,在本例中RDD的沿袭图的大小都小于10KB。(可见,RDD沿袭图在故障恢复时优势十分明显,一减小了存储空间要求,二减小了重计算量)。

2.6.4内存不足时的表现

在这里插入图片描述

图2.10 不同比例数据集在内存中时25台机器用逻辑回归算法处理100GB数据的表现

2.6.5 交互式数据挖掘

在这里插入图片描述

图2.11 在100台机器上用Spark交互式查询急剧增长的输入数据的响应时间

  分析1TB的维基百科页面查看日志,集群配置100台机器,每台8核 68GB内存,每次的查询都扫描整个输入数据。图2.11显示了,即使是1TB的数据查询,Spark的响应时间也只在5-7s。Exact Match 查询跟输入跟标题完全匹配的页面,Substring Match 查询输入跟标题部分匹配的页面,Total View 查询所有页面。这比使用磁盘数据快一个数量级,例如,从磁盘查询1TB的数据需要170s。这说明RDD使Spark成为交互式数据挖掘的强大工具。


2.7 讨论

  本节主要讨论RDD可以表示哪些编程模型,为什么可以广泛适用。从官方介绍当中,我们确实可以发现RDD能表示现今大多数集群计算框架模型,而且重要的是,RDD使得用户可以将不同的模型组合在一个程序中(例如先运行一个Mapreduce操作构建一个图,然后在其上运行Pregel),并在这些模型中共享数据。

2.7.1 表示现存的编程模型

效率从哪里来?
  1)将特定数据持久化到内存中
  2)将数据分区来减少访问开销
  3)发生失败时只需重计算丢失分区
可用RDD表示的模型包含:
  MapReduce:可用Spark中的flatMap、GroupByKey、或者是reduceByKey来表示。
  DryadLINQ:这一系统提供了比MapReduce更大范围的操作,但这些操作都是批操作,Spark中的RDD 转换操作可直接对标使用。
  SQL:数据集上的并行操作,RDD转换操作均可完成。
  Pregel:谷歌的一个用于图计算应用的专业模型。程序以一系列的superstep执行,每个superstep中,每个顶点都执行同一个用户函数来改变该顶点相关的状态、改变图的拓扑结构、并发送信息给其他顶点用于下一次的superstep。这一模型可以表示很多算法,比如最短路径、二分匹配、网页排名等。
由于迭代过程会将同一函数应用于每个顶点,因此,这便是可用RDD表示这一模型的关键。可将顶点状态保存在RDD中、然后使用批转换函数(flatMap)应用用户函数并产生消息RDD,然后再将顶点状态和消息RDD进行join来进行消息交换。更重要的是,RDD允许我们将顶点的状态和Pregel一样保存在内存中,可控制其分区以减少访问开销,并支持失败时小比例执行恢复数据。
  Iterative MapReduce:例如HaLoop和Twister,用户给以一系列MapReduce job并形成一个环。系统在迭代计算中保持数据分区一致性,Twister还可以将其保存在内存中。这些优化均是RDD的一部分,故均可用RDD来表示。

2.7.2 RDD表达性的解释

  • 尽管RDD只能通过批转换操作得来,这也不影响其对大多数编程模型的表示,因为很多并行计算都是将同一操作应用到大量的数据上,于是RDD便很容易表示。
  • RDD的不可变性,也不是其障碍,因为可以创建多个RDD来表示数据集的不同版本,而且,现有的运行于文件系统上的MapReduce应用也不运行对文件进行更改,例如HDFS。
    那RDD这么神,难道其他专业系统设计时都没想到吗?Spark开发者给出的解释:专业系统在设计时针对的是应用的特定问题,而不是考虑其更普遍的原因。这里所指的普遍原因是作业间的数据共享。

2.7.3 利用RDD进行调试

  通过记录job RDD的沿袭图,可以后续重建RDD并可以进行交互式的查询。通过重新计算它所依赖的分区,在单进程调试器中重新运行作业的任何任务。不像传统分布式系统的重现调试器,必须捕获或者推断出多节点事件的顺序,通过只记录RDD沿袭图的方式,使调试器做记录的开销近零。


2.8 相关工作

  • 集群编程模型
      首先,像MapReduce、Dryad、CIEL的数据流模型提供了丰富的算子来处理数据,但是他们稳定存储来提供数据共享。RDD提供了一个更有效的共享抽象因为它避免了数据复制、I/O、序列化开销。
      其次,像DryadLINQ、FlumeJava这样的具有高级别的编程接口的数据流系统,用户可以通过其集成语言API提供的算子如map、join来操作“并行数据集”。但是这一并行数据集仅仅代表磁盘文件或者是表示查询计划的短暂数据集。尽管系统可以在同一个查询中管道化组织多个算子(eg:一个map接着另一个map),它也不能在多个查询中有效地共享数据。我们将Spark API构建在并行数据集模型的基础上,是因为其便利性,也不是因为集成语言的新奇性,而仅仅是为接口提供RDD作为一种存储抽象。我们使之能够支持广泛的应用。
      第三,为需要数据共享的并且提供高级别接口便是为特定应用类设计的系统。例如Pregel支持迭代式图计算,而Twister和HaLoop是运行时迭代的MapReduce。然而问题是这些系统只为他们支持的计算模式提供数据共享。而并没有提供通用抽象来使用户在其使用的操作间共享其选择的数据。例如,用户无法使用Pregel和Twister将数据加载到内存中,然后选择在其上运行哪一个查询。RDD显式提供分布式存储抽象故而支持很多这些专业系统不支持的应用。例如交互式数据挖掘。
      最后,一些系统公开共享的可变状态来使用户可执行内存计算。例如Piccolo允许用户运行并行函数来读取和更新一个分布式哈希表中的单元。分布式内存共享(DSM)系统和key-value形式存储的RAMCloud也提供相似的模型。 RDD在两方面与这些系统不同,首先,RDD提供基于map, sort 和 join这种操作算子的高级别的编程接口,而Piccolo和DSM仅仅读取和更新数据表单元。其次,Piccolo和DSM通过检查点和回滚来提供容错恢复。这在很多应用中都是比基于RDD 沿袭图策略进行容错恢复要昂贵的。另外,RDD在落后者缓解方面也是相对于DSM的优势。
  • 缓存系统
      Nectar能够通过程序分析,识别常见的子表达式,从而在DryadLINQ作业中重用中间结果。添加到基于RDD的系统将具有强大的功能。然而,Nectar并不支持将数据缓存在内存中(其将数据放在分布式文件系统上),也不支持用户显式控制将哪些数据持久化并决定怎样对数据分区。CIEL和FlumeJava同样可以缓存任务结果,但是也并不支持内存缓存并显式控制缓存哪些数据。
      Ananthanarayanan等建议在分布式文件系统中添加内存缓存,来利用数据访问的时间和空间本地性。这种方式加快了已在文件系统中的数据的访问速度。但是这种方式还是没有通过RDD在应用中共享中间结果的方式有效。因为这种方式在不同stage之间还是需要应用将的结果写到文件系统中去。
  • 沿袭
      捕获数据的谱系和起源信息一直以来都是科学计算和数据库研究课题,用于解释结果、用于再生产、用于在工作流中发现bug或者丢失某些数据集时进行重计算。在获取细粒度的谱系图比较昂贵的情况中,RDD提供一个并行编程模型,于是RDD可被用于失败恢复。
    基于谱系图的恢复机制和MapReduce和Dryad作业恢复机制相似,他们追踪DAG任务的依赖关系。然而在这些系统中,当作业结束时他们的沿袭信息就不存在了,需要使用复制的存储系统来在计算之间共享数据。相反,RDD使用沿袭来有效地跨计算保存内存数据,而无需复制和磁盘I/O成本。
  • 关系数据库:
      RDD在概念上类似于数据库视图。而持久RDD类似于物化视图。然后,对于DSM系统和数据库,允许细粒度的读写所有记录,需要对操作和数据做记录进行容错和维持数据一致性的额外开销。对于粗粒度转换模型-RDD,这些开销都不需要。

2.9 总结

  文章中我们主要呈现了弹性分布式数据集RDD,一个用于集群数据共享的高效的、通用的、容错的抽象。RDD可以表示广泛的应用,包括目前被用于进行迭代式计算的专业系统和这些专业系统未能胜任的其他应用。不像现存的需要对数据进行复制进行容错的集群存储抽象,RDD使用粗粒度的转换API使用户可以从沿袭图中高效地恢复数据。RDD已在Spark系统中实现,其执行迭代计算速度比Hadoop快最高80倍,并可用于交互查询上百GB数据。

注:文章内容是笔者根据对Spark论文的理解以及对Spark的使用经验进行地Spark原文的简洁提炼,旨在追本溯源,了解Spark的来龙去脉,文章图片均来自于原始论文。

猜你喜欢

转载自blog.csdn.net/weixin_43878293/article/details/90256290
今日推荐