详细阅读Spark论文

来源:互联网 发布:mac必备软件2017 知乎 编辑:程序博客网 时间:2024/06/09 22:20

为了尽可能完整地了解Spark的整个原理,并有一个层次性的认知,找了几篇最初Spark提出的论文来看
论文题目为:Cluster Computing with Working Sets。

一是希望 借此进一步加深对于Spark的理解进而指导之后的使用。
二是闲的 没事可干 那就虐虐自己吧。
首篇论文在这里可下

note: 并非完整翻译,这样也没什么意思,而基本流程是阅读内容–>理解内容–>总结内容–>提出想法吧。当然其中会添加进去一些自己理解相关知识点请的”外援“。

现在正式开始我们的边翻译边理解的进程吧:

摘要:
MapReduce和其一系列变型非常成功地实现商用集群所要求的 大规模 数据密集型应用。然而其中的很大一部分围绕着非循环数据流模型所建设,并不适用于其他一些热门的应用。这篇论文关注的是在多并行操作中对于数据的可复用性。 其中包括:包含迭代过程的机器学习算法和交互型的数据处理工具。 这里提出的名为Spark的新框架在保持和MapReduce同规模 同容错性基础上,很好地支持了上文中所述的两个应用。为了达到这个目的,Spark引入了抽象性弹性分布式数据集(resilient distributed datsets)即RDD。RDD是只读的,分区的记录集合,其对一组机器可见,同时当其中一个分区丢失后可以进行此分区的重建。在迭代的机器学习任务中Spark十倍优于Hadoop,同时其可在亚秒级别时间中交互式访问39GB数据集。
从摘要可以提出并解决这几个问题:
1. 什么是anayclic data flow model(非周期数据流模型)?
2. Spark提出的初衷是什么?
3. Spark产生这些优势的核心思想是什么?
4. Spark优势何在?
A1. 即为其对于数据的使用和处理无法周期性,数据从一个稳定的来源输入,进行加工处理后,输出到一个稳定的目的地,这是一个单向过程,无法形成数据的环向流动和周期性使用。
A2. 其初衷是为了弥补以MapReduce为主体的Hadoop在涉及迭代和循环这类对于数据有循环使用和处理应用上的不足。
A3. 一个抽象的系统,弹性分布式数据集,RDD。
A4. 不但保持了Hadoop所拥有的能力基础上,增加了1.对于含有迭代思想型应用的支持 2.对于实时交互的满足。

介绍:
集群计算变得普及,其基于自动提供局部性调度,容错,负载均衡的系统,利用并不那么优良的机器集群执行数据的并行计算。MapReduce是完成这类任务模型的首创,像Dryad和MapReduce一般化了对于数据流的支持。他们为使用者提供了一套可编程的模型以实现其可拓展和容错性能,使用者利用这个可编程模型创建非周期数据流图完成对输入数据的一系列操作。底层系统无需用户介入即可完成管理调度同时对错误做出相应反应,这使得使用者可将重心放在对数据的操作上。
从这段获悉这类已有模型有这样几个优势:其实现了在集群上针对数据的并行计算;提供了可编程的模型供使用者操作,并为使用者掩藏了底层繁杂的操作,使得可以专注于应用。
尽管上述这类数据流可编程模型对于很大一部分应用非常给力,但是存在一大类应用无法有效地用非周期数据流来表达。本文志在解决这类上述模型无法解决的问题上:即在多并行操作中要反复使用工作数据集。这包括了:(很多Hadoop使用者反馈Hadoop在这类问题上的低效性)
1. 迭代型工作:大多数的机器学习算法通过在同一数据集上反复应用一函数从而最优化函数参数(例如通过梯度下降)。尽管每一次迭代可以被表达成MapReduce/Dryad工作,然而每一次都需要从磁盘重新加载数据,多么蛋疼啊,这不把io累死。
2. 交互分析:Hadoop常通过像Pig和Hive这样的SQL接口在大数据集上跑ad-hoc搜索查询。理想化而言,用户可以通过大量机器将感兴趣数据加载进内存并反复查询。但是,像Hadoop,由于其作为一个独立的MapReduce来运行并需要从磁盘中加载文件,故每次查询都要承受显著的延迟(数百秒)。
*从上边可以很清楚地看到Hadoopde不足之处
至于为什么Hadoop会有这样的不足,研究下MapReduce的工作方式和原理。*
这篇paper提出了称为Spark的新集群计算框架,….
Spark中主要的抽象就是弹性分布式数据集,其通过一系列机器展现了一个只读的对象分区集合,如果分区丢失可以分分钟重建。用户可以通过机器显式地将一个RDD**缓存到内存**中,并在类MapReduce的并行操作中对其反复使用。RDD通过血统(lineage)的概念来实现容错:如果一个RDD的分区丢失,那么其余RDD拥有充足的信息显示其他RDD如何产生这个分区,进而可以利用未丢失部分对丢失部分进行恢复。
这里写图片描述
如图,太显然了,除非大规模的丢失,否则其前后均有相应的操作和生成关系,只要找到丢失分区所属的”血统”(lineage),那么再“生”一个太简单。

note:图中大写的ABC..等每个代表一个RDD,其中的小颜色矩形代表RDD中的分区。本来在这篇文章实现的Spark中不包括groupBy,join这类操作的,这是作者针对实现"shuffle"的下一步操作。

尽管RDD并非一般性的共享内存的抽象,其在表达力这方面和可拓展性以及可靠性方面找到了最佳的平衡,可以看到其适用于大量的应用。
不得不提的是Spark使用Scala实现…(开始吹了 略)。总之Spark很好地契合了Scala,可以按照编程思维来使用并操作集群。Spark第一个允许使用高效,通用的编程语言来在集群上以交互的方式处理数据。
<这里是吹Spark,其针对Hadoop的优势上文中提到了几百次了>

通过介绍部分,可以明确这么几个问题:
1. 使用MapReduce模式的Hadoop模型其优势和不足在哪里以及为什么会有这样的不足?
2. 1中模型在迭代型任务和交互分析这样的应用中的无力点体现在哪里?
3. Spark针对他们的不足做了怎样的工作?
4. Spark的核心是什么以及Spark怎样合理地解决了那些问题?怎么理解Spark的核心?
5. Spark基于怎样的编程语言,以及基于此提供了怎样的便利?
6. 作者写这个论文吹逼的特点在哪里?
A1.
优势:完美地实现了集群化并行计算;包括提供底层的调度,容错和负载均衡;提供了可编程模型,并为用户封装了底层实现,大大满足了用户关注任务而非集群操作的诉求;可拓展性 容错性;
不足:先天的不足,只针对非周期性数据流;数据需要从磁盘反复加载,导致其在像机器学习这种需要反复同一块数据集的任务上,浪费了太多资源在数据的重新加载上;同样的问题体现在交互分析上。
A2.
这类的问题无一例外牵扯到对同一块数据的反复访问,但是Hadoop这样的就是需要先将数据放到磁盘上,其每次访问是作为一个新的MapReduce任务,这样访问就重新加载的模式真是显得…
A3.
文中明确提及,Spark框架提供和MapReduce相似的可拓展和容错性。
但是其Spark建立在统一抽象的RDD之上,使得它可以以基本一致的方式应对不同的大数据处理场景,同时RDD拥有非常优良的特性,足以支持Spark对于更多问题的解决。
A4.
Spark的核心是RDD,即弹性分布式数据集,其可以看做是一个容错的、并行的数据结构,可以让用户显式地将数据存储到磁盘和内存中,并能控制数据的分区。其本质上是一个只读的分区记录集合。一个RDD可以包含多个分区,每个分区就是一个dataset片段,同时随着操作的进行,会产生更多的分区,而这些分区之间拥有逻辑关系(即所谓“血统”),这样形成了逻辑网络(如上图),这样的逻辑性为容错和回复提供足够多的信息。
A5.
Spark基于Scala实现,并与Scala编译器以及思想很好地整合在一起,使得使用Scala这样一个通用编程语言可以高效地基于集群进行一系列操作和完成并行计算的任务。
A6.
吹自己,扁Hadoop。

编程模式
开发者可以写一个驱动程序以实现针对应用中流的高层控制,以及并行发射多样的操作。Spark为并行化编程提供了两种主要的抽象:弹性分布式数据集在这些数据集上的并行操作(通过在数据集上调用相应函数来激活)。同时其提供了两种限制性的参数共享模式,以完成在集群上的函数式运行。
弹性分布式数据集:前已述,不啰嗦。由前述,我们知道RDD是一个只读的分区记录集合,每个分区对应一个数据集片段。这里需要明白的是RDD中的单元并非相应存储在真实的物理内存中;相反,我们只需要指向RDD的一个柄(handle),这个柄中含有从存储于可靠位置数据中计算出相应RDD的足够信息。这一措施也保证了结点崩溃后的RDD重建(显然因为其并未真是存在于其中,而是维护一个可重现的逻辑关系)。
在Spark中,每个RDD都是Scala的对象,通过程序可以4中方式来构建RDDs:
1. 通过共享文件系统中的文件,例如:HDFS。(这个都不知道速去自杀)。
2. 通过在驱动程序中将Scala自带的集合(如array)并行化(parallelizing),这意味着将数组(以此为例)分成大量的切片并送达多结点。
3. 通过对已有RDD的转化。使用名为flapMap的操作可以将含有类型A的数据集转化为含有类型B的数据集,这个转化原则是将原先数据集中的每个元素A经过用户提供函数操作后成为B:A–>List[B]。其他操作包括map(流程同上述)和filter(对原先数据集中每个元素进行过滤操作,即判断是否符合用户要求)—> 学过Python的同学想必很清楚。
4. 通过将已有的RDD持久化。这里有一个背景:RDD默认是“懒惰”和临时性的。“懒惰”体现在:数据集分区只有在真正被并行化操作所用时才会物化(就是只能用真实数据来满足操作的情况)。临时性体现在:其用过即从内存抛弃。但是为了满足用户一些要求,Spark为RDD的持久化提供了两个途径:cache–>保留数据集“懒惰”的特性,但是会将其保存在内存里以便于复用(这里也体现了Spark怎么来解决Hadoop针对特定应用频繁加载数据而产生的低效性,很简单:放在内存);save–>评估数据集并将其写入一个类似HDFS那样的分布式文件系统中。
注意:当集群中没有足够的内存将数据集的所有分区全部存储时,我们的cache操作就仅仅作为一个“暗示”,Spark只有在使用时才重新计算这些分区。采用这样的方式也是考虑到在结点失效或者数据集过大时,系统的持续运行性。这个思想和虚拟内存有点像哦…
作者的宏大思想是:让用户自己在 存储RDD的开销,访问其的速度,丢失部分RDD的可能性,重新计算RDD的开销中做出平衡。(更灵活)
同样可以提出几个问题:
1. 那些措施保证了RDD丢失之后的重建,那些措施和特性保证了系统在出现问题时的持续运行性?
2. 其哪种构建RDD的方式在你看来,提供了对于Hadoop所无力的那些问题的解决方案?
3. 总结下RDD的一些特性?
<由于这部分文章写的很清晰,就不再回答了>

并行操作
针对RDD的并行操作有:
1. reduce:在驱动程序中使用联结函数将两个数据集相结合产生结果。
2. collect:将数据集所有元素传入驱动程序。
3. foreach:将每个元素通过用户定义的函数。
注意:此时的(文章发布年代)还不支持像MapReduce中那样的群体性的reduce操作(参见上文图中相邻RDD间那些密密麻麻的线条);此时的reduce结果仅仅被收集在一个进程中。作者说在之后的工作中会使用在分布式数据集上“shuffle”来支持群reductions。–>现在回味这个话,作者所言不虚啊。

参数共享
通过给Spark发送闭包(函数),程序调用相应的map,filter和reduce操作。显然,就像函数式编程那样,这些闭包(函数)牵扯到一些变量问题。一般情况下,当Spark在工作集群中运行闭包(函数)时,这些变量会被复制进工作结点。但是,Spark允许编程者创造两种受限的参数共享模式来支持这两简单但不简约的使用模式。
- 广播变量:如果在多并行操作中用到了一个大的只读数据(如查找表),那么更偏向于一次性将其分发给工作结点而非将其打包进每一个闭包–>Spark允许编程者创建一个含数值“广播参数”对象并确保其仅被复制进每个工作结点一次。
- 累加器:有些变量,工作结点使用相关操作仅可对其”加“,而驱动只可读。那么这可被用于实现类似MapReduce中那样的计算器(counters)并为并行的求和提供一个必不可少的语法。累加器可定义为任何形式的“add”运算和“zero”值,同时由于其只读特性,其很容易被做成容错的。

几个简单的例子
1. 文件搜索

// 从hdfs中加载数据文件// 注意: 加载数据文件时 必须要在前边写上从哪里加载//  hdfs or file(本地)等 别管什么默认情况// 你就记着写上就是了val file = spark.textFile("hdfs://...")// 逐行传入 判断是否满足函数 "ERROR" 字符串// 这是一个transform操作获得一个新的RDDval errs = file.filter(_.contains("ERROR"))// 将含有指定字符串的行均设置为1val ones = errs.map(_ => 1)// 求和相加 取得文件中含有"ERROR"行的行数// NOTE: errs 和 ones 均为"懒惰"RDD,即不会物化。//  当调用reduce时,每个工作结点以流方式浏览输入的区块//  来估计ones,并将其家中得到本地的reduce,最终将其本地//  计算结果提交到驱动中。//  这种"懒惰"的数据集 最大程度上模拟了MapReduce// 但是Spark不同于其他框架的点在于其通过操作保留了一些中间数据集。// 例如用: val cachedErrs = errs.cache()// 这种保留中间值的优势可以大大提高之后的运行速度。val count = ones.reduce(_+_)
  1. 逻辑回归
    这是机器学习算法,利用在同一块数据上的反复运行来找到区分数据的最优化参数。迭代!!!
// 从hsfd中读取数据集,并将其缓存val points = spark.textFile(...).map(parsePoint).cache()// 初始化参数 为随机的D维向量val w = Vector.random(n)// 运行多迭代来更新wfor (i <- i to ITERATIONS){    val grad = spark.accumulator(new Vector(D))    // 尽管我们创建了RDD形式的 points, 这里还是使用了 for 结构    // for 关键字是Scala语法和Spark想联结的! 为了码农方便开发    // 其实这里的意思是 以循环体作为闭包(函数)为集合中调用    // foreach... 即 for (p <-- points) {body} 等同于    // points.foreach(p => {body})。这两种方式无疑都是    // 可行的,结果都是启动了Spark的并行化模式。    for (p <- points){ // 并行运行    val s= (1/....)..    // 为实现加总梯度gradient,我们使用了一个累加器变量叫    // grad(如前声明),并使用了显示的重载 += 运算,这种    // 将累加器和for结合使得Spark程序看起来更加"舒服"    grad += s * p.x    }    w -= grad.value}

实现
Spark基于Mesos,其作为一个“集群操作系统”使得多并行应用以很好的精细度共享一个集群,并为应用程序在集群上发射一个应用提供相应的API,Spark也可伴随运行于已有的集群计算框架,并与其共享数据。同时,建立在Meoso上大大简化了编程工作量。
Spark的核心是对于弹性分布式数据集的实现。
我们回过头来看前边已经讲过的那个例子:

val file = spark.textFile("hdfs://...")val errs = file.filter(_.contains("ERROR"))val ones = errs.map(_ => 1)val count = ones.reduce(_+_)

这里写图片描述
如图:
我们上边的操作会生成一系列数据集(RDD),如图所示,这些数据集会以对象链条形式保存以捕获每一个RDD的”血统“(也就是逻辑关系和生成流程关系);每个数据集对象均保留有一个指针指向其父辈并存留有其父辈如何转换生成它的信息。
在内部,每个RDD对象实现相同的简易接口,包含三个操作:
getPartitions:返回一组分区id号
getIterator(partition),遍历一个分区。
getPreferredLocations(partition),用于任务调度以由本地获取数据。
那么当在一个数据集上调用一个并行化操作,Spark创建为数据集的每个分区创建对应的task,并将这些tasks送达工作结点。这里我们尝试使用名为延迟调度的技术将每个task送达其最佳位置其中之一。一旦在工作结点上发射,每个task调用getIterator开始读其分区。
不同类型RDD的相异处仅在于其如何实现RDD接口。例如,对于hdfs-textFIle,分区(partition)是在HDFS中的区块id号,其最优位置就是区块的位置,同时getIterator打开一个流来读这个块。
在MappedDataset中,分区和最优位置与其父辈相同,但是迭代器将map函数用于父辈的元素。最后,在CachedDataset中,getIterator寻找转换分区的本地缓存副本;同时每个分区最佳位置开始就等同于此分区在其父辈中的最优位置,这个性质会保存直到这个分区被缓存到其他结点,之后会进行盖分区位置的更新(否则会一直和其父辈同位置)以使得之前结点可挪作他用。这样的设计使得错误的处理变得容易:若一个结点失效,分区将从其父辈数据集中重新读取并最终缓存到其他结点。

共享变量
在Spark中有两种共享变量的方式,分别为:广播变量和累加器,并使用类自定义的序列化格式来实现。
当用户创建一个值为v的广播变量b,v就会被存入共享文件系统中的文件里。b的序列化格式就是指向这个文件的路径。当工作结点查询b的值时,Spark首先检查v是否位于本地缓存,否则从文件系统中将其读入。我们最初使用HDFS来广播变量,但是我们开发了一个更加高效的流广播系统。
累加器的实现是使用到了一个不同的序列化手法。在累加器创建时,其就被赋予一个独一无二的id;当被存储时,累加器的序列化形式包含其id和对应其类型的“0”值。在工作结点,为每个使用本线程变量来运行任务的线程创建一份累加器的拷贝,并在任务启动时将其重置为“0”。在每个任务都运行之后,工作结点向驱动程序发送一段信息,信息中包含其对于变量累加器所做的更新。当然在任务因失败而重新执行情况下,驱动确保只一次使用从每个操作的每个分区中传来的更新以防重复计算。

整合编译器
这里简要描述如何将Spark整合进Scala的编译器中。
Scala通过将使用者码进去的每一行编译成一个类来运行,这个类包含单对象,此对象中又含有本行里的变量或者函数,最终在其结构中跑本行的代码。如:

var x = 5;println(x)// 编译器定义一个类,假设是Line1,其中包含变量x。// 将第二行编译成 println(Line1.getInstance().x)// 这些类被加载进JVM以完成对于每一行的运行。

基于Scala的以上代码编译运行方式,为了使得其编译器可用于Spark,我们做了两处改变:
- 使得编译器将其定义的那些类输出到共享文件系统中,从而在工作结点里使用正常的Java类加载器就能加载这些类。
- 改变生成的代码,从而每行的单对象可以直接引用之前行的单个类,而非使用静态方法 getInstance()。这使得无论何时闭包被序列化传送到工作结点中时,可以捕获其引用的单个类的状态。如果不这样做,那么对于单对象的更新(如前设置x = 7)将不会被广播到工作结点中。

针对实现部分,可以提几个问题:
1. 实际中Spark如何实现了RDD,其怎样根据具体操作合理并高效地创建,访问,使用,存储RDD以实现分布式和用户操作的统一?
2. 简述应用在Spark上的工作流程,并分析代码背后框架所做的一系列事情。
3. Spark怎样实现了两种变量共享方式?
4. 如何将Spark和Scala开发相合并。
A1.
框架根据用户每一个操作(map,filter..)分别实现不同类型的RDD,并依据操作的相互关系生成RDD之间的逻辑联系(即“血统”),最终形成了一个RDD的依赖图。同时,每个RDD中包含有多种操作接口以使其功能得到完善(而不同RDD间的区别也只在于如何实现各自的这些接口)。(这些接口涉及:识别,访问,数据片的存储位置)。即这些RDD根据各自对应的不同目的,实现相同的为用户所看到的接口(即使方式等完全不同),但是抽象出来的能力是一样滴。从这里也可以看到RDD的所谓抽象化,从始至终没有真正动数据,而是拿着几个相互关联的“信息包”瞎咋呼。
A2.
首先请万分注意,RDD真实地代表了你需要操作的数据集,但是为了分布式处理的要求,这个数据集在RDD中以分区形式存在,所以RDD里会有一个信息是数据分区的集合。
推荐一个文章,很nice(可以直接拖动到后边看流程,详细!!)。
A3.
广播变量:肯定不会傻傻地把带着数值的变量在集群里发来发去。而是将值放在一个共享文件系统,集群里交互的是这个变量的序列化形式也即变量对应数值的存放路径。
累加器:其基本方法是工作结点干完活后,将干活结果发给老大,老大整理所有结点之后得到结果。这样有三个问题:老大怎么知道是哪个累加器的处理被发上来了;工作结点在累加器什么形式上进行工作;老大怎么解决因意外产生的重复计算问题。
< 回答了这几个问题也就清楚了>
A4.
明确Scala编译器工作原理:其将每一行编译成一个单独的对象,下一行对上边的行访问其实是通过那个行编译生成的单对象的。说明重点在于编译器编译每一行生成的针对那一行的对象,因为信息都在里边。
那么Spark针对此:将编译器编译的每一行产生的类放到共享文件系统里,这样所有工作结点都可以获得相应信息。
还要知道,为了获得那一行的信息使用了getInstance的静态方法;那么在Spark里,屏蔽这个静态方法,使得针对行单对象的访问变得直接化。

在文章的最后作者简要讲了Spark和目前现有的模型思想比有怎样的特性:
对比分布式共享存储。
对比集群计算框架。 要明白Spark和Hadoop有很多共性,甚至作者一再强调Spark最大程度上维持Hadoop的容错性 稳定性等。
Spark将“血统”思想很好地整合进了框架。
Spark能和很多语言相整合,并提供交互式操作,不过这个交互式主要是针对数据的密集型计算。

这篇文章讲到的是Spark的最基础思想,甚至有些简陋,但是不得不说这正是Spark的根基所在。
在文章最后,作者提出了之后的规划(我们可以对比现在来看看):
1. 挖掘RDD特性和Spark里其余抽象的最大潜力,并拓展其适用范围。
2. 增强RDD的抽象性 使得通过编程就体现存储消耗和重建损失间的均衡。
3. 为转换(transform)RDD提供更多的操作,诸如:shuffle,从而可以通过key将一个RDD重新划分。这样针对RDD的更细致化操作就能搞起来了:可以进行group-by和joins的操作。可以避免对整个数据集的扫描,而是只关注RDD中的某部分。
4. 在Spark接口中提供更高层次交互接口,如SQL和R的shell.

ccdeveloper邀请您访问桃隐社区
http://forwardtous.taoy.info/?fromuid=254589
All Over…

0 0
原创粉丝点击