spark概论

一、概述
1.轻:(1)采用语言简洁的scala编写;(2)利用了hadoop和mesos的基础设施
 
2.快:spark的内存计算、数据本地性和传输优化、调度优化,使其在迭代机器学习,ad-hoc query、图计算等方面是hadoop的MapReduce、hive和Pregel无法比拟的
 
3.灵:
(1)实现层:完美演绎了Scala trait动态混入策略(如可更换的集群调度器、序列化库);
(2)原语层:允许款站新的数据算子(operator)、新的数据源、新的language bindings;
(3)范式层:支持内存计算、多迭代批处理、即席查询、流处理和图计算等
 
4.巧:与Hadoop无缝结合;数据仓库实现上借了Hive的势;图计算借用Pregel和PowerGrah的API以及PowerGraph的点分割思想;最主要借助了scala
 
5.缺陷:细粒度、异步的数据处理支持差
 
二、Spark的依赖
1.Hadoop的MapReduce模型
2.Scala的函数式编程
3.两种种分布式存储系统:HDFS和S3,应该算是目前最主流的两种了。
 
三、计算范式和抽象
1.Spark是一种粗粒度数据并行的计算范式。数据并行的范式决定了 Spark无法完美支持细粒度、异步更新的操作;
 
2.Spark的RDD,采用Scala集合类型的编程风格。一是闭包,二是RDD的不可修改性。
 
3.Spark的计算抽象是带有工作集的数据流。工作集的抽象很普遍,如多 迭代机器学习、交互式数据挖掘和图计算;流处理是一种数据流模型,MapReduce也是,区别在于MapReduce需要在多次迭代中维护工作集。为保证容错,MapReduce采用了稳定存储(如HDFS)来承载工作集,代价是速度慢。HaLoop采用循环 敏感的调度器,保证前次迭代的Reduce输出和本次迭代的Map输入数据集在同一台物理机上,这样可以减少网络开销,但无法避免磁盘I/O的瓶颈。
 
4.Spark的突破在于,在保证容错的前提下,用内存来承载工作集。关键是实现容错,传统上有两种方法:日 志和检查点。考虑到检查点有数据冗余和网络通信的开销,Spark采用日志数据更新。Spark记录的是粗粒度的RDD更新,这样开销可以忽略不计。鉴于Spark的函数式语义和幂等特性,通过重放日志更新来容错,也不会有副作用。
 
四、编程模型
Transformations和Actions

对于RDD,有两种类型的动作,一种是Transformation,一种是Action。它们本质区别是:

  • Transformation返回值还是一个RDD。它使用了链式调用的设计模式,对一个RDD进行计算后,变换成另外一个RDD,然后这个RDD又可以进行另外一次转换。这个过程是分布式的
  • Action返回值不是一个RDD。它要么是一个Scala的普通集合,要么是一个值,要么是空,最终或返回到Driver程序,或把RDD写入到文件系统中

关于这两个动作,在Spark开发指南中会有就进一步的详细介绍,它们是基于Spark开发的核心。这里将Spark的官方ppt中的一张图略作改造,阐明一下两种动作的区别。

 

          图1 两个空间的切换,四类不同的RDD算子
输入算子(橘色箭头)将Scala集合类型或存储中的数据吸入RDD空间,转为RDD(蓝色实线框)。输入算子的输入大致有两类:一类针对Scala集合类型,如parallelize;另一类针对存储数据,如上例中的textFile。输入算子的输出就是Spark空间的RDD。

一部分变换算子视RDD的元素为简单元素,分为如下几类:

  • 输入输出一对一(element-wise)的算子,且结果RDD的分区结构不变,主要是map、flatMap(map后展平为一维RDD);
  • 输入输出一对一,但结果RDD的分区结构发生了变化,如union(两个RDD合为一个)、coalesce(分区减少);
  • 从输入中选择部分元素的算子,如filter、distinct(去除冗余元素)、subtract(本RDD有、它RDD无的元素留下来)和sample(采样)。

另一部分变换算子针对Key-Value集合,又分为:

  • 对单个RDD做element-wise运算,如mapValues(保持源RDD的分区方式,这与map不同);
  • 对单个RDD重排,如sort、partitionBy(实现一致性的分区划分,这个对数据本地性优化很重要,后面会讲);
  • 对单个RDD基于key进行重组和reduce,如groupByKey、reduceByKey;
  • 对两个RDD基于key进行join和重组,如join、cogroup。

后三类操作都涉及重排,称为shuffle类操作。

从RDD到RDD的变换算子序列,一直在RDD空间发生。这里很重要的设计是lazy evaluation:计算并不实际发生,只是不断地记录到元数据。元数据的结构是DAG(有向无环图),其中每一个“顶点”是RDD(包括生产该RDD 的算子),从父RDD到子RDD有“边”,表示RDD间的依赖性。Spark给元数据DAG取了个很酷的名字,Lineage(世系)。这个 Lineage也是前面容错设计中所说的日志更新。

Lineage一直增长,直到遇上行动(action)算子(图1中的绿色箭头),这时 就要evaluate了,把刚才累积的所有算子一次性执行。行动算子的输入是RDD(以及该RDD在Lineage上依赖的所有RDD),输出是执行后生 成的原生数据,可能是Scala标量、集合类型的数据或存储。当一个算子的输出是上述类型时,该算子必然是行动算子,其效果则是从RDD空间返回原生数据 空间。

行动算子有如下几类:生成标量,如count(返回RDD中元素的个数)、reduce、fold/aggregate(见 Scala同名算子文档);返回几个标量,如take(返回前几个元素);生成Scala集合类型,如collect(把RDD中的所有元素倒入 Scala集合类型)、lookup(查找对应key的所有值);写入存储,如与前文textFile对应的saveAsText-File。还有一个检 查点算子checkpoint。当Lineage特别长时(这在图计算中时常发生),出错时重新执行整个序列要很长时间,可以主动调用 checkpoint把当前数据写入稳定存储,作为检查点。

这里有两个设计要点。首先是lazy evaluation。熟悉编译的都知道,编译器能看到的scope越大,优化的机会就越多。Spark虽然没有编译,但调度器实际上对DAG做了线性复 杂度的优化。尤其是当Spark上面有多种计算范式混合时,调度器可以打破不同范式代码的边界进行全局调度和优化。下面的例子中把Shark的SQL代码 和Spark的机器学习代码混在了一起。各部分代码翻译到底层RDD后,融合成一个大的DAG,这样可以获得更多的全局优化机会。

另一个要点是一旦行动算子产生原生数据,就必须退出RDD空间。因为目前Spark只能够跟踪RDD的计算,原生数据的计算对它来说是不可见的(除非以后 Spark会提供原生数据类型操作的重载、wrapper或implicit conversion)。这部分不可见的代码可能引入前后RDD之间的依赖,如下面的代码:

第三行filter对errors.count()的依赖是由(cnt-1)这个原生数据运算产生的,但调度器看不到这个运算,那就会出问题了。

由于Spark并不提供控制流,在计算逻辑需要条件分支时,也必须回退到Scala的空间。由于Scala语言对自定义控制流的支持很强,不排除未来Spark也会支持。

Spark 还有两个很实用的功能。一个是广播(broadcast)变量。有些数据,如lookup表,可能会在多个作业间反复用到;这些数据比RDD要小得多,不 宜像RDD那样在节点之间划分。解决之道是提供一个新的语言结构——广播变量,来修饰此类数据。Spark运行时把广播变量修饰的内容发到各个节点,并保 存下来,未来再用时无需再送。相比Hadoop的distributed cache,广播内容可以跨作业共享。Spark提交者Mosharaf师从P2P的老法师Ion Stoica,采用了BitTorrent(没错,就是下载电影的那个BT)的简化实现。有兴趣的读者可以参考SIGCOMM'11的论文 Orchestra。另一个功能是Accumulator(源于MapReduce的counter):允许Spark代码中加入一些全局变量做 bookkeeping,如记录当前的运行指标。

五、运行和调度

Spark会将RDD和MapReduce函数,进行一次转换,变成标准的Job和一系列的Task。提交给SparkScheduler,SparkScheduler会把Task提交给Master,由Master分配给不同的Slave,最终由Slave中的Spark Executor,将分配到的Task一一执行,并且返回,组成新的RDD,或者直接写入到分布式文件系统。

1.由master完成的工作:
(1)记录变换算子序列,增量构建DAG图(有向无环图)
(2)行动算子触发,DAGSchedule将DAG图转化为作业和任务集
(3)由cluster manager将划分好分区的任务集发送到集群的节点上
 
2.由worker完成的工作:
(1)任务线程(task thread)真正运行DAGScheduler生成的任务;
(2)块管理器(block manager)负责与master上的block manager master通信(完美使用了Scala的Actor模式),为任务线程提供数据块。
 
3.DAGSchedule如何分区以及如何分配给集群上的节点
如何分区以及分区该放在哪个节点,涉及到了RDD的另外两个域,分别是分区划分器和首选位置
(1)分区划分器:Spark提供两种分区划分器:HashPartitioner和RangePartitioner。同时允许用户自定义ArrayHashPartitioner。
划分器的工作:它决定了该操作的父RDD和子RDD之间的依赖类型,这对于shuffle类操作很关键。同一个join算子,如果协同划分的话,两个父 RDD之间、父RDD与子RDD之间能形成一致的分区安排,即同一个key保证被映射到同一个分区,这样就能形成窄依赖。反之,如果没有协同划分,导致宽依赖。所谓协同划分,就是指定分区划分器以产生前后一致的分区安排。
 
(2)分区放置的节点,这关乎数据本地性:本地性好,网络通信就少。有些RDD产生时就有首选位置,如HadoopRDD分区的首选位置就是HDFS块所在的节点。有些RDD或分区被缓存了,那计算就应该送到缓存分区所在的节点进行。再不然,就回溯RDD的lineage一直找到具有首选位置属性的父RDD,并据此决定子RDD的放置。
 
RDD的数据结构里很重要的一个域是对父RDD的依赖。有两类依赖:窄(Narrow)依赖和宽(Wide)依赖。
注:这个概念我还没能够完全理解,以免误导,不做过多解释
宽/窄依赖的概念不止用在调度中,对容错也很有用。如果一个节点宕机了,而且运算是窄依赖,那只要把丢失的父RDD分区重算即可,跟其他节点没有依赖。而宽依赖需要父RDD的所有分区都存在, 重算就很昂贵了。所以如果使用checkpoint算子来做检查点,不仅要考虑lineage是否足够长,也要考虑是否有宽依赖,对宽依赖加检查点是最物 有所值的。
posted @ 2013-08-23 18:46  vincent_hv  阅读(974)  评论(0编辑  收藏  举报