Spark论文总结——Lec15

一、Spark简介

1.本质上来讲,Spark是MapReduce的后继产物,可以将其认为是MapReduce一种演进版本

2.Spark被广泛运用在数据中心的计算任务中。

3.Spark做了一件令我们很感兴趣的事情, 那就是它对MapReduce中的两个阶段进行了总结,它将MapReduce总结为一个多步骤数据流图的概念。在应对优化以及处理故障方面,这让Spark系统有了更多解决的思路。

4.在遍历数据这块,Spark要比MapReduce来得更好,你可以将多个MapReduce应用程序整合在一起,让它们一个接一个地运行,但在Spark中我们可以更加方便地做到这一点。

二、RDD和DSM(分布式共享内存)

1.DSM : 每个网页的RANK可以放进共享内存里,每个WORKER可以去读和写。 但是DSM在容错时非常麻烦。
传统的解决方案是每小时对当前的内存状态写一个CHECK POINT。
一边计算一边写CHECK POINT 从共享内存去磁盘是十分昂贵的。还有必须REDO在这个CHECK POINT 之后的所有WORK。

2.SPARK同时解决了2个问题,第一个他能很好的容错,不像 DSM这么麻烦。第二,他也对迭代式的应用非常友好。
在SPARK里,数据被存储在一个叫RDD的结构里。 持久化这些RDD在内存中,那么下一次迭代可以基于内存里的RDD去做。
同时RDD也是SPARK里的核心概念。RDD参考另一篇博客RDD总结
在这里插入图片描述

三、Spark实现

我们已经用scala代码实现了Spark系统。
系统执行在Mesos集群(Mesos是一个在多个集群计算框架中共享集群资源的管理系统)管理之上,且能够与Hadoop、MPI以及其它应用进行资源共享(兼容)。每个Spark程序作为一个独立的Mesos应用执行,且都有自己的driver(master)和workers。应用之间的资源共享由Mesos来处理。
Spark能够通过Hadoop已经存在的插件接口读取来自不论什么Hadoop的输入资源。

1.Job Scheduling

调度器依据目标RDD的Lineage图创建一个由stage构成的有向无环图(DAG)例如以下图所看到的。
每一个stage内部尽可能多地包括一组具有窄依赖关系的流水线转换(transformations)。
stage边界的划分有两种情况:一是宽依赖上的Shuffle操作;二是已计算的分区。它能够缩短父RDD的计算过程。
在stage内须要启动一组任务用于计算缺失的分区,直到目标RDD计算完毕。
在这里插入图片描述

2.Interpreter Integration(解释器的集成)

(1)像Ruby和Python一样,Scala也有一个交互式shell。
Scala解释器通常依据用户输入的代码行。来对类进行编译,接着装载到JVM中,然后调用类中的一个函数进行处理。
这个类是一个包括输入行上的变量或函数单例对象,且会用一个初始化函数执行这行代码。
比如,假设用户输入代码var x = 5,接着又输入println(x),则解释器会定义一个包括x的Line1类。并将第2行编译为println(Line1.getInstance().x)。

(2)在Spark中我们对解释器做了两点修改:
a.类传输:解释器可以支持基于HTTP传输类字节码。这样worker节点就能获取输入每行代码相应的类的字节码。
b.改进的代码生成逻辑:通常在每行代码行上创建的单态对象通过相应类上的静态方法被访问。也就是说,假设要序列化一个闭包,它会引用前面代码行中变量。比方上面的样例Line1.x,Java不会依据对象关系传输包括x的Line1实例。
所以worker节点不会收到x。所以,我们将这样的代码生成逻辑改为直接引用各个行对象的实例

(3)下图说明了解释器怎样将用户输入的一组代码行解释为Java对象。
在这里插入图片描述

注:eval执行一个字符串表达式,并返回表达式结果。_在scala中可被用作参数占位符:①当匿名函数传递给方法或其他函数时,如果该匿名函数的参数在=>的右侧只出现一次,那么就可以省略=>,并将参数用下划线代替。这对一元函数和二元函数都适用。②当匿名函数的参数未被实际使用到时,可以不给它一个命名,而直接用下划线代替(上图情况)。

Spark解释器便于跟踪处理大量对象关系引用,而且便利了HDFS数据集的研究。
我们计划以Spark解释器交互式地执行高级数据分析语言。比方类似SQL。

3.Memory Management

Spark为持久化存储RDD提供三种选择:
(1)内存存储反序列化的java对象;
(2)内存存储序列化的数据。
(3)磁盘存储方式。

4.Support for Checkpointing

(1)虽然RDD中的Lineage信息能够用来故障恢复,但对于那些Lineage链较长的RDD来说,这样的恢复可能非常耗时。如果其中一个worker发生了故障,这可能就得从头开始计算所有东西。
一般来说,Lineage链较长、宽依赖的RDD需要采用检查点机制。这样的情况下,集群中一个节点故障(导致父RDD所在的磁盘数据丢失)可能导致每一个父RDD的数据块丢失。因此需要所有节点进行又一次计算(因为宽依赖失败节点上有多个父RDD的数据)。相反,假设在窄依赖之上使用检查点的操作是没有价值的。假设一个节点失败了,能够通过“血统”进行又一次计算。这样带来的开销也不过复制RDD的几分之中的一个而已。
Spark当前已经提供了checkpoint的接口,可是哪些数据需要进行checkpoint操作的选择权留给了用户。

(2)例子
有一些transformation操作,它们依赖上一步所有的transformation操作的结果。接着,又是一些窄依赖。
(下图:窄依赖-宽依赖-窄依赖)
在这里插入图片描述
如果有一个worker出现了故障,我们需要去重新执行这个出现故障的worker所负责的任务。但一般来说中间数据不会保存,除非通过调用cache来告诉Spark这些links数据要进行持久化。(这里持久化的意思是,在该worker机器未出故障并且内存未满时,中间结果会保存在内存中,而不像其他未调用cache的中间结果一样执行完即丢弃)
如果我们去执行这个wide transformation操作时,我们就会遇上一个问题,它不仅需要同一个worker所处理的分区上的数据,它也需要其他分区中的数据(这些worker已经执行transformation并丢弃了结果)。这意味着,为了重建这个出现故障的worker(下图中间的机器)所负责的计算任务,我们还得重新执行所有其他worker的这一部分并且也要重新执行这个发生故障的worker上的整个lineage graph中的操作(下图中红实线框)。
在这里插入图片描述
出于以上原因,Spark允许我们定期创建特定transformation操作的checkpoint。在这个scala程序中会去调用persist,当计算完这个transformation的输出结果时(下图中双蓝色框的地方,进入宽依赖的节点),将它们的输出结果保存到HDFS中。宽依赖出故障时,只需从HDFS中读取这些数据即可,无需从头开始重新对所有分区中的数据进行计算。
在这里插入图片描述
ranks数据保存到HDFS中的checkpoint执行的时间间隔也需要在成本和性能之间进行取舍。
问题:调用cache时,是否扮演了checkpoint的角色?
我们可以在下图第8行和17行调用cache。cache通常的用途是为了将数据保存在内存中以便之后复用这些数据,但在例子中它的效果是将这个阶段的输出结果放在这些worker的内存中,而不是HDFS中。这些cache请求只是建议Spark对数据进行缓存,如果这些worker中的内存满了,那么这些请求就会被丢弃。这意味着,调用cache并不能确保这些数据真的可用。
在这里插入图片描述
(3)以上就是它的编程模型,执行模型以及故障恢复策略。
那些用于在大型集群上运行的最新设计实际上在很大程度上都在使用故障恢复策略。例如,Spark坚持认为这些transformation操作的结果是确定性的以及这些RDD都是不可变的,对此有很多解释。因为这使得它通过对一个分区进行重新计算就可以从故障中恢复过来,而不是从头开始执行整个计算过程。

四、PageRank代码

1.PageRank算法简介

PageRank是谷歌使用的用来计算不同网页搜索结果重要性的一个非常著名的算法,PageRank被广泛应用于那些MapReduce无法处理的情况。PageRank涉及了许多不同的步骤,它涉及了遍历和循环。
PageRank这个版本中的输入元素是一个巨大的集合,每一行包含了2个URL,该网页的URL中包含了一个链接,这个链接代表的URL又指向了一个page。
PageRank所试着做的事情是,它会去估算每个网页的重要性,这意味着,它是根据其他重要页面是否具有指向给定页面的链接来估计重要性的,这种模型可以估算用户点击链接到达每个给定页面的可能性。
它有一个用户模型,用户有85%的可能性会从当前页面随机选一个链接跳转到该链接对应的页面,用户还有15%的可能性会切换到另一个页面,哪怕当前页面上没有指向该页面的链接,比如你直接在浏览器上输入一个URL链接。
这里的思路是,PageRank算法会反复模拟用户查看网页并按照链接跳转的行为,以此提高目标页面的重要性,然后再次运行它。像Spark上的PageRank一样,它会有针对性地并行地对所有页面运行此模拟,在模拟随机用户对其点击时,对该算法会去跟踪每个页面或者每个URL的pagerank值,该值进行更新,这些rank值最终会收敛于某个真实的值
因为它是通过遍历来做的,虽然也可以在MapReduce中编写这样的代码,但是我们不可能通过单个MapReduce程序做到这些事情,这需要对MapReduce应用程序进行多次调用,每次调用都好似遍历时的其中一步,性能很差。因为在MapReduce中,它思考的只是一个Map操作和一个Reduce操作,它总是从磁盘上的GFS文件系统中读取它的输入元素,并写出它的输出元素,即更新后的每个PageRank值,在每个阶段,它会将更新后的每个PageRank值写入到GFS的文件中,如果使用多个MapReduce程序来做的话,那就会产生大量的文件I/O

2.应用程序代码

(1)Scala语法

Scala 与 Java 的最大区别是:Scala 语句末尾的分号 ; 是可选的。Scala 程序是对象的集合,通过调用彼此的方法来实现消息传递。
Scala中的=>符号可以看做是创建函数实例的语法糖。例如:A => T,A,B => T表示一个函数的输入参数类型是“A”,“A,B”,返回值类型是T。请看下面这个实例:

scala> val f: Int => String = myInt => "The value of myInt is: " + myInt.toString()
f: Int => String = <function1>
 
scala> println(f(3))
The value of myInt is: 3

上面例子定义函数f:输入参数是整数类型,返回值是字符串。
另外,() => T表示函数输入参数为空,而A => Unit则表示函数没有返回值。

(2)PageRank的代码

示例是一段PageRank的代码,由Spark源码改动而来。
输入元素如下所示,每一行上有两个URL,u1是一个网页的URL,u3是该页面中所指向的另一个页面所代表的URL链接(下面in中第一行u1 u3)。(下面用123代指u1u2u3)
in:
u1 u3
u1 u1
u2 u3
u2 u2
u3 u1
这个输入文件所表示的webgraph中只有3个网页,这些链接来自123这三个页面,1有一个指向它自己的链接,2上面有一个指向3的链接,这里有一个指向2自身的链接,3这里有一个指向1的链接,这是一个非常简单的图结构。
SparkPageRank.scala:

val lines = spark.read.testFile("in").rdd
val lines1 = lines.map ( s =>
    val parts = s.split("\\s+")
    (parts[0], parts[1])
)
val links2 = links1.distinct()
val links3 = links2.groupByKey()
val links4 = links3.cache()
var ranks = links4.mapValues(v => 1.0)

for(i <- 1 to 10){
    val jj = links4.join[ranks]
    val contribs = jj.values.flatMap{
        case (urls, rank) =>
            urls.map(url => (url, rank / urls.size))    
    }
    ranks = contribs.reduceByKey(_ + _).mapValues(0.15 + 0.85 + _)
}

val output = ranks.collect()
output.foreach(tup => println(s"$(tup._1) has rank: ${tup._2} ."))

(3)执行PageRank代码

在这里插入图片描述
执行结束,输出了一些垃圾信息后得到u1u2u3三个页面对应的rank值,可见页面u1的rank值最高。
在这里插入图片描述

(4)逐行执行PageRank代码

为了理解Spark的编程模型,将程序逐行传入Spark解释器中。
启动Spark shell,直接在里面输入代码。准备一个特殊版本的MapReduce程序,可以一次执行一行代码。

a).val lines = spark.read.testFile(“in”).rdd
在这里插入图片描述
这行代码的意思是,Spark去读取输入文件,这个输入文件里面包含了3个页面。当Spark读取一个文件时,它实际所做的事情就是从GFS/HDFS这样的分布式文件系统中读取一个文件(HDFS文件系统和GFS非常相似),所以如果有一个大型文件,这个文件里面放着所有的URL、链接和网页,HDFS会将大型文件拆分成很多个分区,这里就会有很多个worker机器,每个worker机器会负责读取该输入文件的多个分区,这其实和MapReduce中的Map操作非常相似。
第一行代码是要求系统去读取一个文件,但它这行代码并不会去对输入文件进行处理,而是去构建一个lineage graph。只有当调用action的时候,才会真正开始进行计算,如collect函数。所以,lines中实际保存的其实是lineage graph中的一部分,而不是结果
在这里插入图片描述
我们所期望看到的结果就是该文件的内容,lineage graph通过这个Transformation操作一次得到多行数据结果。它里面包含了该输入数据文件中每一行的数据,所以这就是该程序中第一行代码所做的事情。

问题:通过collect是不是就是对该调用链执行JIT编译?(即时编译,也称为动态翻译或运行时编译,是一种执行计算机代码的方法,这种方法设计在程序执行过程中(在执行期)而不是在执行之前进行编译。)
是的。如果你调用collect,它会让Spark对这个lineage graph进行处理并生成Java字节码,用来描述所有不同的Transformation。
当调用collect时,通过查看HDFS,Spark会去找到你想要找的数据,它会去让一组worker来处理该输入数据的不同分区。它会将lineage graph中的每个
transformation
编译为Java字节码,它会将这些字节码发送给Spark所选择的所有worker机器上,这些worker机器就会去执行这些字节码,它会告诉每个worker去读取它所负责的那个分区输入数据,接着,我们从这些worker中取回所有处理完的数据。所以直到你去调用某个action算子时,它们才会去执行这些操作。但是上面我们现在早早地调用了collect,这里是因为想去看下输出结果是什么,以便理解这些transformation操作做了什么。

b) 第二行代码:
val lines1 = lines.map ( s =>
val parts = s.split(“\s+”)
(parts[0], parts[1])
)
这里的lines指代的是第一个transformation的输出结果,这组字符串对应着输入文件中每一行。
map所做的事情就是,它会调用一个函数来对输入数据中的每个元素进行处理。
在这个例子中,每个元素指的就是输入文件中的每一行,map里面调用了一个split函数来对每一行数据进行处理,往split中传入一个以空格来切分字符串的正则表达式,然后得到一个字符串数组,最后一部分就是直接去拿这个数组的parts(0)和parts(1)中的数据。
这一行打印语句所表示的意思是,这个transformation操作的输出结果由两部分组成第一部分是它所处理的这一行数据中的第一个字符串,第二部分则是该行的第二个字符串,这里我们只是做了些小转换以便能更容易地处理这些字符串。
调用collect,可以看到,lines中保存的是一个个字符串行,links1中保存的是由两个URL组成的Stringpairs数组,即From URL和ToURL。
在这里插入图片描述
当执行这个map操作的时候,每个worker可以独立处理该输入文件它所负责的那个分区,因为它认为每一行都是独立的,不同行或者分区间不存在任何交互。如果针对每个输入记录的这些map操作是一个纯粹的本地操作,那么所有的worker就可以中它们所负责的那块分区做到并行处理

c) 程序中的下一行代码是去调用distinct:val links2 = links1.distinct()
作用是去掉重复的链接。在一个给定的页面上,如果存在着多个链接指向另一个相同页面,我们只会记录其中一个用于PageRank计算。
因为在输入数据中那些data item是以随机顺序进行排列的,distinct要将每个重复的输入元素替换为一个单个输入元素,distinct需要通过某种方式将完全一样的item放在一起,这就需要网络通信了。所有这些数据是分散在所有的worker上的,我们会对数据进行shuffle处理,这样的话,任何两个相同的item就会放在同一个worker上了。
我们会对item通过hash的方式来进行shuffle,我们通过对item进行hash来选出用于处理的worker,接着,通过网络将这些item发送过去,或者你也可以让系统对这些item进行排序,接着将排序好的输入元素拆分到所有的worker上。
但在这个例子中,distict()基本没有发生什么事情,因为并没有什么重复项存在。除了顺序以外,links2(即该distinct操作的处理结果)和links1中的数据是完全相同的,用于该Transformation操作的输入数据还是那些数据。这里顺序变了,因为这里使用了hash或者排序之类的处理。
在这里插入图片描述
d) 下一个transformation操作是groupByKey:val links3 = links2.groupByKey()
此处我们要做的是收集所有的链接。
groupByKey会根据URLpair中的fromURL来对所有记录进行分组,它会将来自同一page的所有link归纳到一个link集合中,该集合内包含了当前page的URL加上该page上的所有link。
这需要通信,Spark很智能,它足以去优化这一点,因为distinct操作已经将所有拥有相同fromURL的记录交由同一个worker进行处理,groupBykey就可以很容易地对它们进行处理,并且根本就不需要进行网络通信了,因为它可以观察到这些数据已经根据fromURL这个key分好组了。
在这里插入图片描述
在这里插入图片描述
调用collect进行计算看下links3的结果:可以看到一个tuple数组,每个tuple的第一部分是由from page这个URL所构成,第二部分则是该page上的所有link所组成的列表。u2有两个链接u2和u3,u3只有一个链接u1,u1有两个链接u1和u3。

e) val links4 = links3.cache()
遍历是从这几行代码处开始的,它会去反复使用这些数据。每次循环遍历的时候,它都会去使用links3中的信息来模拟这些用户点击链接的行为。每当调用collect时,Spark会重新读取输入文件,重新执行map操作以及distinct操作,我们并不想每次循环遍历的时候对这数TB大小的links数据反复做这种处理。
所以,为了告诉Spark去查看我们想反复使用这些数据,用paper中的话讲,程序员需要显式持久化这些数据。在Spark中,如果你想将数据保存在内存中,你需要去要调用的函数叫做cache,而不是persist。
links4的内容和links3的内容是完全相同的,我们想让Spark将这些数据保存在内存中,因为我们会反复使用这些数据。
在这里插入图片描述
f) var ranks = links4.mapValues(v => 1.0)
在循环开始前,需要为每一个由sourceURL所代表的page来设置一组Page Rank,即初始化每个page的rank值(代表概率)为1。当每次循环遍历的时候,它会生成一个新版本的rank值,通过该算法可以去更新每个URL所指向page的PageRank值。打印下rank:
在这里插入图片描述
这里面保存了一种映射关系,即当前page的from URL(该page的URL)和它所对应的PageRank值。

g) val jj = links4.join(ranks)
现在要开始执行这个循环了。在这一行代码中执行了join操作,这是第一次循环遍历中的第一条语句
这里join所做的就是将links和ranks连接起来,这里的links指的是每个URL所指向的page,这里的rank指的是每个URL所指向page当前PageRank值
它可能需要将URL作为key来对数据进行shuffle操作,Spark应该能够注意到这些links和ranks已经根据key以相同的方式分好区了。假设它创建ranks的时候使用了和它创建links时相同的hash方案,那么相同key所对应的分区已经交由同一个worker进行处理了,Spark会注意到它不需要去移动任何数据了。

注:如果这其中发生了些错误导致links和ranks中的数据以不同的方式进行分区,为了将2个RDD中对应的key join到一起,那么此时Spark就得对数据进行移动。

jj是一个数组,里面放着每个page的URL,以及该page上的links列表,还有我们所选择的该page的当前PageRank值,即1.0。在每一条单独的记录中,它都包含了我们所需page对应的所有信息。
在这里插入图片描述
h) val contribs = jj.values.flatMap{ case (urls, rank) => urls.map(url => (url, rank/urls.size)) }
下一步要去得到每一个page的当前PageRank值,通过rank/urls.size这个公式来计算出该URL的PageRank值。
我们对URL所指向的每个page都进行了map操作,对于URL所指向的每个page来说,我们通过frompage的当前PageRank值除以它所指向的page数量来计算出这个数字,这里我们将page和和它对应的新PageRank值建立起联系。
这里生成的是由URL和该URL所对应的PageRank值为一个元素所组成的集合,这里每条URL中都有多条记录,任意一个给定的page,对于每个指向该page的link来说,这里都会有一条记录,该记录表示的是该fromURL和它所对应的更新后的PageRank值。
在这里插入图片描述
现在要将每个page在contribs中该page相关的PageRank contribution值进行相加。这里我们需要去进行一次shuffle操作,这就是一个使用了wide input的wide transformation,因为我们需要将每个page所对应的contribs中的所有元素放到同一个分区中,并交由同一个worker进行处理,这样我们就可以对它们进行求和。

i) ranks = contribs.reduceByKey(_ + ).mapValue(0.15 + 0.85 *)
通过调用reduceByKey来完成对PageRank的计算,reduceByKey做的事情是,首先它会将具有相同key的所有记录都放在一起,接着将给定key所对应的每条记录中的第二个元素全部加起来,然后,生成输出结果,该输出结果由两部分组成,它的key是一个URL,它的value是该page所对应的更新后的PageRank值(即0.15+0.85 x rank)。
实际上这里有2个transformation操作,第一步是reduceByKey(contribs.reduceByKey(_ + _)),第二步是mapValues(mapValue(0.15 + 0.85 * _))。这里假设用户有15%的可能性会去访问一个随机页面,85%的可能性会去点击访问当前页面上的一个链接。虽然这里已经对ranks进行了赋值,这里最终要做的就是创建一个全新的transformation操作,它不会去立刻修改已经计算出来的值。
一开始的时候,ranks中包含了一堆(URL,PageRank) pair,此处,依然是一堆(URL,PageRank) pair。不同的是,我们通过一个步骤对这些pair中的PageRank进行了更新。这些PageRank在一开始时候的值是1,但此处的结果更接近于我们所看到的最终输出结果。
在这里插入图片描述
总结计算PageRank的过程:根据输入文件统计页面跳转情况,对于join后的每一个记录,记录跳向的页面为key,和对应的跳转概率(rank/urls.size其他url跳到当前页面的概率)。将相同url的跳转概率按照0.15+0.85*rank的格式累加起来,格式的意思是,0.15表示留在当前页面的概率,0.85表示从其他页面跳转过来的概率,得到的结果就是用户访问该页面的总概率。

j) val output = ranks.collect()
以上是该算法的一次遍历。当循环回到开始,算法会执行相同的join操作,flatmap操作,以及reduceByKey操作。循环实际每次做的事情是生成这个lineage graph,它不会对循环中所涉及变量的值进行更新。本质上,它是往它所构建的lineage graph中创建并追加了新的transformation节点,但只会在循环结束后执行这个lineagegraph(代码中调用collect操作这个点)。
接着,在宽依赖(wide dependencies)处进行shuffle操作。最后,在运行该程序的机器上将输出结果收集到一起。paper将这运行该程序的机器叫做driver,我们在这个driver机器上运行了这个scala程序,它用来执行Spark中的计算。接着,程序会拿到这个output变量并打印出它的值。
在这里插入图片描述
这段代码所做的工作是由很多独立的MapReduce来实现的。这段代码一共21行,使用MapReduce来进行这种处理可能会来得更为简单。
直到最后的collect操作前,这段代码所做的就是生成lineage graph,而不是对数据进行处理。

3、lineage graph

(1)paper中的Figure3如图所示,在最后调用collect之前,程序所做的就是生成这个lineagegraph。我们从文件中读取数据并生成links,然后下图右侧有一连串处理阶段。
在这里插入图片描述
(2)这里我们通过单独的一步来对rank值进行初始化。接着,这里会进行重复的join操作,以及reduceBykey操作,每次循环遍历都会生成一次join操作和reduceByKey操作所需的参数(下图中中括号处)。这个循环会往这个lineagegraph中添加了一个又一个的节点,这里它生成的并不是一个环形图。另一个要注意的事情是,你不会在MapReduce中看到这种情况,即我们这里所缓存的数据会在每次循环遍历的时候被反复使用。Spark会将这些数据保存在内存中,并且它会多次使用这些数据。
在这里插入图片描述
(3)执行过程
①假设在HDFS中,输入数据一开始就分好了区。假设输入文件已经拆分成了很多个64MB大小之类的数据块,并保存在HDFS上。当你调用collect开始进行计算的时候,Spark知道这些输入数据已经被分好区放在了HDFS中,它会试着以某种方式来将任务拆分并分配给对应的worker来做,它可能会试着在保存着HDFS分区数据的同一台机器上执行计算,或者,它可能会让一堆worker去读取每个HDFS分区中的数据,这里更可能出现的情况是,每个worker会负责读取多个分区中的数据。首先做的事情是,每个worker会去读取该输入文件中的一部分数据,这一步是读取文件数据。
(总结:HDFS分区,Spark的worker读取本地文件处理
在这里插入图片描述
②在每条链中第一个节点中,worker所干的事情就是去拿到它所负责的HDFS数据块,该链中剩下的节点就是lineage graph中的节点。自前为止,这条执行链中的一切事情都是在一个单独的worker上发生的。下图左侧红虚线框是worker1,右侧红虚线框是其他的worker。每个worker都是各自处理各自的任务,假设这些worker所在的机器与存储着对应HDFS中的分片数据所在的机器是同一台。
但这里如果不是上面假设的情况,从HDFS中拿到数据,并将数据传给对应的worker,这个过程中可能存在着网络通信。但在此之后,它所作的就是某种程度超快的本地操作了。这就是这里所发生的事情,人们将这个称之为Narrow dependencies,所谓的窄依赖,就是Transformations所处理的数据record和其他record是彼此独立不相干的,这条处理chain无须担心会与其他处理chain产生彼此依赖。
这其实已经要比MapReduce来得更为高效,这是因为,如果我们有多个map阶段,它们会将数据都保存在内存中,如果你运行了多个MapReduce程序,如果它们是只有Map操作的MapReduce应用程序。在每个阶段,程序都会从GFS中读取输入数据,计算,然后将它的输出结果写回GFS,然后,下一个阶段还是读取,计算,写入。所以,此处我们已经消除了读取和写入这两步操作,在效率方面大大提升。
在这里插入图片描述
③然而,并不是所有的transformation操作都是Narrow的,在逐条读取数据记录的时候,这些记录(record)彼此间并不一定是独立的(非独立是指,两条处理链下所涉及的两条record可能是相同的key,一旦涉及到分组,去重操作就要有关联了)。
distinct(去重)需要知道所有实例上某个特定key相关的所有记录。类似地,groupByKey需要了解拥有同一个key的所有实例的相关信息。join也是如此,它要对数据进行移动。所以,这里就会涉及一堆非本地的transformation操作,paper中将它们称为wide transformation。因为它们可能得去查看该输入数据的所有分区,这很像MapReduce中的Reduce部分。
拿distict来说,我们会在所有worker上对所有的分区执行distinct操作,这些分区是根据key来划分的。不同的key所对应的数据会交由与之对应的worker来处理,但这些key所对应的数据可能分散在所有那些执行上一步transformation操作的worker上。
一般来讲,执行map和执行distinct操作的是同一批worker,但为了将这些具有相同key所对应的数据放在一起,这些数据需要在这两个transformation操作中进行移动。所以Spark实际做的事情是,它会拿到这个map操作的输出结果,然后根据每条记录的key来对每条记录进行hash,拿到key,然后根据worker的数量对其进行取模,以此来选择哪个worker可以看到这部分数据。这个实现和MapReduce很相似,在Narrow stage中最后发生的事情是,输出结果会被拆分到不同的bucket(下图map行下方的小格子)并交由不同的worker来进行下一步transformation操作。当这些操作全部结束后,就可以在这些worker上执行distinct操作,因为具有相同给定key的数据都放在了同一个worker上,它们就可以开始生成输出结果了。
narrow transformation的效率超级高,因为我们在本地执行这一系列函数处理操作。当我们将map函数的海量输出结果通过网络推送到distinct函数时,要进行shuffle操作。所以这些wide transformation的使用成本都很高, 这里面涉及了大量的网络通信,这里也存在着某种计算阻碍,因为在我们进行下一步操作之前,我们得等待narrow stage中的所有处理完成才行,这就是wide transformation。在这里插入图片描述
④为了优化,因为在开始对数据进行处理之前,Spark会去创建一个完整的lineage graph
在原始程序中,有两个wide transformation操作,distinct需要进行一次shuffle操作(下图第6行代码),groupByKey(下图第7行代码)也是如此。这两个都是wide transformation操作会根据key来进行分组。当进行distinct操作的时候做一次shuffle操作后,Spark可以意识到,它已经按照适合groupByKey的方式进行了shuffle操作,无须再进行另一次shuffle操作了。Spark很可能在没有使用网络通信的情况下,就实现了groupByKey的wide transformation操作,因为这些数据已经根据key分好了区。
所以,在这个例子中,在无须对数据进行shuffle操作的情况下,我们就可以进行groupByKey操作。只有在Spark生成出完整的lineage graph,然后在执行计算时,它才能这样做。
在这里插入图片描述
在这里插入图片描述

五、Spark的适用场景

Spark很适合用来对海量数据进行批处理。Spark可能有助于离线分析客户的购买习惯,但它并不适合在线处理。paper中有提到,Spark并不适用于对数据流进行处理。Spark所假定的使用情况是,所有的输入数据都是已经可用的情况。(现在Spark有一个变种,它叫做SparkStreaming,它更适合用来处理数据流。当数据到达的时候,它会将这些数据流拆分成一些更小的批次进行处理,Spark每次会对一个批次的数据进行处理)

六、总结

Spark视作一种升级版的MapReduce,它解决了MapReduce中可能存在的一些表达和性能方面的问题。Spark经常做的事情是让数据流图变得明确,他们想让你以Figure3中的lineagegraph这种风格来思考计算过程,以及在这些阶段之间进行数据移动,同时,Spark会对这个lineagegraph做相应的优化,故障恢复这块我们也得根据这个lineagegraph来思考。所以这其实是在大数据处理中通过数据流图来思考描述计算过程的一种方式。
在这里插入图片描述
这有很多好处,Spark可以通过将transformation操作间产生的中间数据放入内存来提高性能,而不是先将这些数据写入GFS,然后在下一个transformation操作执行开始的时候,从GFS中读取这些数据——这些是在使用MapReduce的时候不得不做的事情。
另外一点就是,它能够去定义这些数据集(即RDD),并告诉Spark,将这些RDD放在内存中,因为我要对它进行复用。在后续阶段中,如果能复用这些结果的话,这样付出的代价要比重新计算它们来得低,这种事情在Spark中处理起来很容易,但在MapReduce中处理起来就很困难。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值