2012-NSDI-RDD

本篇博客是本人原创整理,其中参考了网上相关资料,最后没有列全参考资料,深感抱歉。

主要基于RDD论文进行翻译,参考了网上的中文翻译版本。并增加了Spark总体概述内容,此部分主要参考厦大林子雨老师的课件。

此报告作为本人的课程学习报告,如需转载,请告知本人。


Title

Resilient Distributed Datasets: A Fault-TolerantAbstraction for In-Memory Cluster Computing

Writer

Matei Zaharia, Mosharaf Chowdhury, Tathagata Das,Ankur Dave, Justin Ma, Murphy McCauley, Michael J. Franklin, ScottShenker, Ion Stoica

University of California, Berkeley

NSDI 2012. Best Paper Award and Honorable Mention for Community Award.

1Spark概述

1.1Spark简介

Spark最初由美国加州伯克利大学的AMP实验室于2009年开发,是基于内存计算的大数据并行计算框架,可用于构建大型的、低延迟的数据分析应用程序。2013年Spark加入Apache孵化器项目后发展迅猛,如今已成为Apache软件基金会最重要的三大分布式计算系统开源项目之一(Hadoop、Spark、Storm)。Spark在2014年打破了Hadoop保持的基准排序纪录,用十分之一的计算资源,获得了比Hadoop快3倍的速度。

Spark主要有4个特点:

  1. 运行速度快:使用DAG执行引擎以支持循环数据流与内存计算。
  2. 容易使用:支持使用Scala、Java、Python和R语言进行编程,也可以通过Spark Shell进行交互式编程。
  3. 通用性强:Spark提供了完整而强大的技术栈,包括SQL查询、流式计算、机器学习和图算法组件。
  4. 运行模式多样::可运行于独立的集群模式中,或者运行于Hadoop中,也可运行于Amazon EC2等云环境中,并且可以访问HDFS、Cassandra、HBase、Hive等多种数据源。

1.2Spark与Hadoop比较

Hadoop存在如下一些缺点:

  1. 表达能力有限:计算都要转换成Map和Reduce操作,难以描述复杂的数据处理过程。
  2. 磁盘IO开销大:每次执行都需要从磁盘读数据,计算过程中,需要将中间结果写入磁盘。
  3. 延迟高:任务之间的衔接涉及IO开销,前一个任务执行完成之前,其他任务无法开始,难以胜任复杂、多阶段的计算任务。

Spark在借鉴Hadoop MapReduce优点的同时,很好地解决了MapReduce所面临的问题。相比于MapReduce,Spark主要具有如下优点:

  1. Spark的计算模式也属于MapReduce,但不局限于此,还提供了多种数据集操作类型,编程模型比MapReduce更灵活。
  2. Spark提供了内存计算,可将中间结果放到内存中,对于迭代运算效率更高。
  3. Spark基于DAG的任务调度执行机制,要优于MapReduce的迭代执行机制。

1.3Spark生态圈

在实际应用中,大数据处理主要包括以下三个类型:

  1. 复杂的批量数据处理:通常时间跨度在数十分钟到数小时之间。
  2. 基于历史数据的交互式查询:通常时间跨度在数十秒到数分钟之间。
  3. 基于实时数据流的数据处理:通常时间跨度在数百毫秒到数秒之间。

当同时存在以上三种场景时,就需要同时部署三种不同的软件,比如MapReduce、Impala、Storm。这样做会带来一些问题:

  1. 不同场景之间输入输出数据无法做到无缝共享,通常需要进行数据格式的转换。
  2. 不同的软件需要不同的开发和维护团队,带来了较高的使用成本。
  3. 比较难以对同一个集群中的各个系统进行统一的资源协调和分配。

Spark的设计遵循“一个软件栈满足不同应用场景”的理念,逐渐形成了一套完整的生态系统——BDAS(Berkeley DataAnalytic Stack)。

以Spark为核心的BDAS既能够提供内存计算框架,也可以支持SQL即席查询、实时流式计算、机器学习和图计算等。

Spark可以部署在资源管理器YARN之上,提供一站式的大数据解决方案。因此,Spark所提供的生态系统足以应对上述三种场景,即同时支持批处理、交互式查询和流数据处理。

图1展示了BDAS架构图。


图1:BDAS架构

2RDD运行原理

2.1RDD设计背景

现在的计算框架如MapReduce在大数据分析中被广泛采用,为什么还要设计新的Spark?有如下几点原因:

  1. 在实际应用中,存在许多迭代式算法(比如迭代式机器学习、图算法等)和交互式数据挖掘工具(比如用户在同一数据子集上运行多个Ad-hoc查询),这些应用场景的共同之处是:不同计算阶段之间会重用中间结果,即一个阶段的输出结果会作为下一个阶段的输入。
  2. 目前的大部分框架(比如MapReduce)对计算之间的数据复用的处理方式都是把中间结果写入到一个稳定的外部存储系统中(比如HDFS),这样会带来大量的数据复制、磁盘I/O和序列化开销。
  3. 虽然有些框架对数据重用提出了相应的解决方法,比如类似Pregel等图计算框架将中间结果保存在内存中,HaLoop提供了迭代MapReduce接口,但是这些都是针对特定功能设计的框架,并对用户屏蔽了数据共享的方式,不具备通用的抽象模式,例如,允许一个用户可以加载几个数据集到内存中并进行一些跨数据集的即时查询。

针对以上问题,Spark提出了一种新的数据抽象模式RDD(弹性分布式数据集),令用户可以直接控制数据的共享。用户不用担心底层数据的分布式特性,只需将具体的应用逻辑表达为一系列转换处理,不同RDD之间的转换操作形成依赖关系,可以实现管道化,从而避免了中间结果的存储,大大降低了数据复制、磁盘I/O和序列化开销。

2.2RDD概念

  1. 一个RDD就是一个分布式对象集合,本质上是一个只读的分区记录集合,每个RDD可以分成多个分区,每个分区就是一个数据集片段,并且一个RDD的不同分区可以被保存到集群中不同的节点上,从而可以在集群中的不同节点上进行并行计算。
  2. 由于RDD提供了一种高度受限的共享内存模型,不能直接修改,因此RDD的来源只有两种:基于稳定的物理存储中的数据集来创建RDD,即从外部系统输入,比如HDFS、Hive、Cassandra、Hbase等;从父RDD上执行确定的Transformation操作得到,比如map、filter、join、groupBy等。
  1. RDDs在任何时候都不需要被“物化”(进行实际的变换并最终写入稳定的存储器上)。实际上,一个RDD有足够的信息知道自己是从哪个数据集计算而来(就是Lineage)。它有一个强大的特性:从本质上说,若RDD失效且不能重建,程序将不能引用该RDD。
  2. 用户可以控制RDDs的其他两个方面:持久化和分区。对于复用数据,可以选择内存缓存,也可以基于一个元素的key值来为RDD所有元素在机器节点间进行数据分区。这对位置优化来说是有用的,比如可用来保证两个要join的数据集使用了相同的哈希分区方式。

RDD典型的执行过程如下图2所示:


图2:RDD执行过程的一个实例。首先从外部数据源创建RDDs,然后经过一系列的Transformations操作(如map、filter),每一次都会产生新的RDD,最后一个RDD经过Action操作(如count、collect、save)进行计算得到相应的结果值,并输出到外部数据源。这一系列处理被称为一个Lineage(血缘关系),即DAG(有向无环图)拓扑排序的结果。可以看到,RDDs只有触发了Action操作才会真正计算,这是RDDs的Lazy特性,因此可以先对Transformations进行组装一系列的Pipelines,然后再通过Actions计算。

此外,编程人员可以通过调用RDDs的persist方法来缓存复用数据。Spark默认将缓存数据放在内存中,但是如果内存不足则会写入磁盘。用户可以通过persist的参数来调整缓存策略,比如只将数据存储在磁盘中或者复制备份到多台机器上。最后,用户也可以为每一个RDDs的缓存设置优先级,明确哪个在内存中的RDDs应该最先写入磁盘。

举例说明:监控日志数据挖掘

假设一个web服务遇到错误,运维人员想从存储在HDFS中的几TB的日志中找出错误的原因。运维人员可以通过Spark将日志中的错误信息加载到分布式的内存中,然后对这些内存中的数据进行查询。以下是Scala代码:

 

在对errors第一次做Action操作之后,Spark会将errors的所有分区的数据存储在内存中,这样对后面的errors的计算速度会有很大提升。需要注意的是,像lines这样的基础数据的RDD不会存储在内存中,包含错误信息的数据可能只是整个日志数据的一小部分,所以将过滤后的数据放在内存中较为合理。

下图3展示了代码中第三个查询的Linage Graph。


图3:例子中第三个查询的Lineage Graph,其中方框表示RDDs,箭头表示转换。

2.3RDD操作

表1:Spark中RDD常用的Transformations和Actions操作。Seq[T]表示元素类型为T的一个列表。表中,一些操作比如join只适合key-value类型的RDDs。函数名需要和编程语言函数名一致。比如, map 是一个 one-to-one 的映射操作, 而 flatMap 的每一个输入值会对应一个或者更多的输出值(有点像MapReduce 中的 map)。除了这些操作,用于可以通过persist操作请求缓存RDD。另外,用户可以拿到被Partitioner 分区后的分区数以及根据 Partitioner 对另一个 dataset 进行分区。 像 groupByKey、reduceByKey 以及 sort 等操作都是经过了hash 或者 range分区后的 RDD。

可以将Transformation操作理解成一种惰性操作,它只是定义了一个新的RDD,而不是立即计算它,而Action操作这时立即计算,并返回结果给程序,或者将结果写入到外存储中。

举例说明:逻辑回归

逻辑回归算法是一个用来找到最佳区别两种点集(比如垃圾邮件和非垃圾邮件)的超平面w的常用分类算法,它用到了梯度下降方法:一个随机的值作为w的初始值,每次迭代都会将含有w的方法应用到每一个数据点然后累加得到梯度值,w会往改善结果的方向移动。其Scala代码如下所示。


一开始,定义一个叫points的RDD,这个RDD从一个text文件中经过map将每一行转换为Point对象得到。然后我们重复对points进行map和reduce操作计算出每一步的梯度值。在迭代之间我们将points存放在内存中可以使得性能提高20倍。

举例说明:PageRank

在PageRank中,如果一个文档引用另一个文档,那被引用的文档的Rank需要加上引用文档发送过来的贡献值。每次迭代中,每一个文档都会发送r/n的贡献值给它的邻居,其中r表示这个文档的排名值,n表示这个文档的邻居数量。然后更新文档的排名值为a/N+(1-a)*sum ci, 这个表达式值表示这个文档收到的贡献值, N 表示所有的文档的数量。其Scala代码如下所示:

2.4RDDs的表达

抽象RDDs的一个挑战是如何经过一系列的Transformation操作后追踪其继承关系。主要有以下5个接口可以表达RDDs:

  1. 分区列表:每一个分区就是RDD的一部分数据;
  2. 分区位置列表:指明分区优先存放的结点位置;
  3. 依赖列表:存储父RDD的依赖列表;
  4. 计算函数:利用父分区计算RDD各分区的值;
  5. 分区器:指明RDD的分区方式(hash/range)。

在设计接口的过程中,最有趣的问题在于如何表示RDDs之间的依赖关系。R

DDs之间的依赖有两种类型:

  1. 窄依赖:表现为一个父RDD的分区对应于一个子RDD的分区,或多个父RDD的分区对应于一个子RDD的分区,比如map,filter,union等都是窄依赖。
  2. 宽依赖:表现为存在一个父RDD的一个分区对应于一个子RDD的多个分区,比如groupByKey、sortByKey等是宽依赖。

对于join操作可以分为两种情况:

  1. 对输入进行co-partitioned(协同划分),属于窄依赖(图左所示)。协同划分是指多个父RDD的某一个分区的所有“键(key)”,落在子RDD的同一个分区内,不会产生同一个父RDD的某一分区,落在子RDD的两个分区的情况。
  2. 对输入做非协同划分,属于宽依赖(图右所示)。


图4:窄依赖与宽依赖的区别。每一个方框代表一个RDD,带有颜色的矩形表示分区。

 以下两个原因使得区分宽窄依赖很有用:

  1. 窄依赖可以以流水线的方式计算所有父亲的分区数据,不会造成网络之间的数据混合。对于宽依赖的RDD,则通常伴随着Shuffle操作,即首先需要计算好所有父分区的数据,然后在节点之间进行Shuffle(类似于MapReduce中的Shuffle操作)。
  2. 窄依赖从一个失败节点中恢复是非常高效的。因为,RDD数据集通过Lineage记住了它是如何从其他RDD中演变过来的,血缘关系记录的是粗粒度的转换操作行为,当这个RDD的部分分区数据丢失时,它可以通过血缘关系获取足够的信息来重新运算和恢复丢失的数据分区(不需要重新计算所有分区),而且可以并行地在不同节点进行重新计算。对于宽依赖,单个节点失效通常意味着重新计算过程会涉及多个父RDD分区,开销较大。

2.5Stage的划分

Spark通过分析各个RDD的依赖关系生成了DAG,再通过分析各个RDD中的分区之间的依赖关系来决定如何划分阶段,具体划分方法如下:

  1. 在DAG中进行反向解析,遇到宽依赖就断开;
  2. 遇到窄依赖就把当前的RDD加入到当前的阶段中;
  3. 将窄依赖尽量划分在同一个阶段中,可以实现流水线计算。


图5:根据RDD分区的依赖关系划分Stage。其中实线的方框代表RDDs,蓝色的方框代表分区。假设从HDFS中读入数据生成3个不同的RDD(即A、C、E),通过一系列转换操作后再将计算结果保存回HDFS。对DAG进行解析时,在依赖图中进行反向解析,由于从RDD A到RDD B的转换以及从RDD F到RDD G的转换,都属于宽依赖。因此,在宽依赖处断开后可以得到3个阶段,即Stage 1,Stage 2和Stage 3。可以看出,在阶段2中,从map到union都是窄依赖,可以形成一个流水线操作。比如,分区7通过map操作生成的分区9,可以不用等待分区8到分区10这个转换操作的计算结束,而是继续进行union操作,转换得到分区13,这样流水线执行大大提高计算的效率。

由上述论述可知 ,把一个DAG图划分成多个Stage以后,每个阶段都代表了一组关联的、相互之间没有Shuffle依赖关系的任务组成任务集合。每个任务集合会被任务调度器(TaskScheduler)进行处理,由任务调度器将任务分发给Executor运行。

2.6RDD运行过程

RDD在Spark中的运行过程如下图6所示:

  1. 创建RDD对象;
  2. SparkContext负责计算RDD之间的依赖关系,构建DAG;
  3. DAGScheduler负责把DAG图划分为多个Stage,每个Stage中包含了多个Task;
  4. 每个Task会被TaskScheduler分发给各个Worker Node上的Executor去执行。


图6:RDD在Spark中的运行过程。

2.7RDD特性

为了更好理解RDD的好处,下表中用RDDs和分布式共享内存系统进行了对比。在DSM系统中,应用程序读取和写入全局地址空间的任意位置。这里的DSM不仅包括传统的共享内存系统,还包括其他能让应用程序执行细粒度“写”共享状态的系统,例如提供DHT的Picclo和分布式数据库。DSM是一个很普遍的抽象,但是这个普遍性使得它在商用集群中实现高效且容错的系统比较困难。


表2:RDD与分布式共享内存系统比较

RDDs和DSM之间的区别如下:

  1. RDDs只能通过粗粒度转换创建,而DSM允许对每个存储单元读取和写入。这使得RDD在批量写入主导的应用上受到限制,但增强了其容错方面的效率。
  2. RDDs可以使用Lineage恢复数据,不需要检查点的开销。此外,当出现失败时,RDDs的分区中只有丢失的那部分需要重新计算,而且该计算可在多个节点上并发完成,不必回滚整个程序。而DSM需要检查点和程序开销。
  3. RDDs的不可变性让系统像MapReduce那样用后备任务代替运行缓慢的任务来减少缓慢节点(stragglers)的影响。因为在DSM中任务的两个副本会访问相同的存储器位置和受彼此更新的干扰,这样后备任务在DSM中很难实现。
  4. RDDs还具备了DSM的两个优点。首先,在RDDs的批量操作过程中,任务的执行可以根据数据的所处的位置来进行优化,从而提高性能。其次,只要所进行的操作时只基于扫描的,当内存不足时,RDD的性能下降也是平稳的。不能载入内存的分区可以存储在磁盘上,其性能也会与当前其他数据并行系统相当。

Spark采用RDD以后能够实现高效计算,原因主要在于:

  1. 高效的容错性。
  1. 已经存在的分布式内存抽象系统,比如distributed shared memory、key-value stores、databases以及Piccolo,为了实现容错,必须在集群节点之间进行数据复制或者记录日志,也就是在节点之间会发生大量的数据传输,这对于数据密集型应用而言会带来很大开销。
  2. RDD是一种天生具有容错机制的特殊集合,不需要数据冗余的方式(比如检查点)实现容错,而只需通过RDD父子依赖(血缘)关系重新计算丢失的分区来实现容错,无需回滚系统,这样避免了数据复制的高开销,而且重算过程可以在不同节点之间并行,实现高效的容错。
  3. 此外,RDD提供的转换操作都是一些粗粒度的操作(比如map、filter、join),RDD依赖关系只记录这种粗粒度的转换操作,而不需要记录具体的数据和各种细粒度操作的日志(比如对哪个数据项进行了修改),这句大大降低了数据密集型应用中的容错开销。
  1. 中间结果持久化到内存中。数据在内存中的多个RDD操作之间进行传递,不需要“落地”到磁盘,避免了不必要的读写磁盘开销。
  2. 存放的数据可以是Java对象,避免了不必要的对象序列化和反序列化开销。   

2.8RDD应用场景

  1. RDDs非常适合将相同操作应用在整个数据集的所有的元素上的批处理应用。在这些场景下,RDDs可以利用Lineage Graph来高效的记住每一个Transformation的步骤,并且不需要记录大量的数据就可以恢复丢失的分区数据。
  2. RDDs不太适合用于需要异步且细粒度的更新共享状态的应用,比如一个web应用或者数据递增的web爬虫应用的存储系统。对于这些应用,使用传统的记录更新日志以及对数据进行checkpoint会更加高效,比如使用数据库、RAMCloud、Percolator以及Piccolo。

3 实现

3.1作业调度

  1. Spark的调度器类似于Dryad,但是增加了对持久化RDD分区是否在内存里的考虑
  2. 调度器在分配tasks时采用延迟调度来达到数据本地性的目的(即数据在哪里,计算就在哪里)
  3. 对于宽依赖,将中间数据写入到节点的磁盘中以利于从错误中恢复。
  4. 对于执行失败的任务,只要对应stage的父类信息可用,便会在其他节点上重新执行。


图7:怎么计算 spark job stage 的例子。实现的方框表示 RDDs ,带有颜色的方形表示分区, 黑色的是表示这个分区的数据存储在内存中。在这个场景中, stage 1 的输出结果已经在内存中, 所以我们开始运行 stage 2 , 然后是 stage 3。

3.2解释器集成

  1. 让用户通过解释器来交互性的运行Spark,从而达到查询大数据集的目的。
  2. Scala解释器通常将用户输入的每一行代码编译成一个类。
  3. Spark中做出两个改变:
  1. 类传输:为了让工作节点能够从各行生成的类中获取到字节码,解释器通过HTTP来为类提供服务。
  2. 代码生成器的改动:让各行对象的实例可以被直接引用。

3.3内存管理

Spark在持久化RDDs时提供三种内存管理方式:

  1. Java对象的内存存储:提供最快的性能。
  2. 序列化数据的内存存储:用户内存空间有限时,比java对象图存储更加有效。
  3. 磁盘存储:对大到无法放进内存但每次重新计算又很耗时的RDD非常有用。

此外,当有新的RDD分区被计算出来而内存空间又不足时,Spark使用LRU策略将老分区移除到磁盘上。

3.4检查点支持

尽管RDD的Lineage可以用来还原数据,但这通常会非常耗时,因此将某些RDDs持久化到磁盘上会非常有用。对于Spark来说,因为RDD都是不可变的,不用考虑数据的一致性,所以完全可以在后台持久化RDD,而无需暂停整个系统。

4 评估

通过在Amazon EC2上进行一系列实验和用户应用程序的基准测试,对Spark和RDDs进行了性能评估。测试结果如下:

  1. 在迭代机器学习和图计算中,Spark性能要比Hadoop模型好20倍,这些性能提升来自于将数据以java对象存入内存从而减少系统IO和反序列化的开销。
  2. 用户应用程序同样有很好的性能和扩展性。使用Spark来运行一个原本运行在Hadoop上的分析报告的应用,相较于Hadoop性能提升了40倍。
  3. 当出现节点故障时,Spark可以通过重新计算,快速地恢复那些丢失的RDD分区。
  4. Spark可以在5-7秒内交互式地查询1TB的数据。

5 总结

RDD是Spark的核心,也是整个Spark的架构基础。本篇论文重点总结如下:

  1. RDDs提出动机是为了解决迭代式算法场景和交互式数据挖掘场景中数据共享问题。
  2. RDDs提供了一种受限制的共享内存的方式,这种方式是基于粗粒度的转换共享状态而非细粒度的更新共享状态。
  3. RDD是一个只读的,被分区的数据集。
  4. RDD具有高效的容错性,可以通过Linage重新计算丢失分区。

6 参考资料

1.Spark

2.SparkCN

3.Scala教程

4.Spark简介_厦门大学数据库实验室

5.Spark及其生态圈简介

6.Spark踩坑记:从RDD看集群调度

7.Spark容错机制

8.SparkShuffle详解及作业

9.Spark原理介绍

10.Hadoop&Spark MapReduce对比 & 框架设计与理解

11.SparkRDD深入了解

12.逻辑回归

13.梯度下降(GradientDescent)小结

14.PageRank算法--从原理到实现


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章