《Spark: Cluster Computing with Working Sets》读书报告
介绍
大数据和人工智能的诞生给在集群计算机上进行并行计算提出了需求。
Apache Spark 是专为大规模数据处理而设计的快速通用的计算引擎。Spark是UC Berkeley AMP lab (加州大学伯克利分校的AMP实验室)所设计的,类似Hadoop MapReduce的通用并行框架。Spark保持了MapReduce的可扩展性和容错性,但不同于MapReduce适合用于非循环数据流的是,spark比较适合处理复用的数据,像现在的机器学习算法基本上对数据都要进行迭代运算,一个数据集的数据要处理多遍。Spark主要抽象了RDD这种弹性分布式数据集,使得处理能力比hadoop好很多。
每个task中间输出结果可以保存在内存中,从而不再需要读写HDFS,因此Spark能更好地适用于数据挖掘与机器学习等需要迭代的MapReduce的算法。
Spark 是一种与 Hadoop 相似的开源集群计算环境,但是两者之间还存在一些不同之处,Spark 启用了内存分布数据集,除了能够提供交互式查询外,它还可以优化迭代工作负载。
Spark 是在 Scala 语言中实现的,它将 Scala 用作其应用程序框架。与 Hadoop 不同,Spark 和 Scala 能够紧密集成,其中的 Scala 可以像操作本地集合对象一样轻松地操作分布式数据集。
尽管创建 Spark 是为了支持分布式数据集上的迭代作业,但是实际上它是对 Hadoop 的补充,可以在 Hadoop 文件系统中并行运行。通过名为 Mesos 的第三方集群框架可以支持此行为。----百度百科
基本概念:
假设我们现在有一个集群,我们设有一个master节点,两个worker节点,并且驱动程序(本文中使用tasker名称)运行在master节点上。
在master节点提交应用以后,在master节点中启动driver进程,driver进程向集群管理者申请资源(executor),集群管理者在不同的worker上启动了executor进程,相当于分配资源。
driver进程会将我们编写的spark应用代码拆分成多个stage,每个stage执行一部分代码片段,并为每个stage创建一批tasks,然后将这些tasks分配到各个executor中执行。
两种算子
RDD有两种算子:
1.Transformation(转换):属于延迟Lazy计算,当一个RDD转换成另一个RDD时并没有立即进行转换,仅仅是记住数据集的逻辑操作;
2.Action(执行):触发Spark作业运行,真正触发转换算子的计算;
共享变量
1、广播变量
Spark提供的Broadcast Variable,是只读的。并且在每个节点上只会有一份副本,而不会为每个worker都拷贝一份副本。因此其最大作用,就是减少变量到各个节点的网络传输消耗,以及在各个节点上的内存消耗。此外,spark自己内部也使用了高效的广播算法来减少网络消耗。
当用户创建一个值为v的广播变量b,v就会被存入共享文件系统中的文件里。b的序列化格式就是指向这个文件的路径。当worker结点查询b的值时,Spark首先检查v是否位于本地缓存,否则从文件系统中将其读入。使用的是新开发的一个更加高效的流广播系统。
2、累加器变量
Spark提供的Accumulator,主要用于多个节点对一个变量进行共享性的操作。Accumulator只提供了累加的功能,允许做全局累加操作,accumulator变量广泛使用在应用中记录当前的运行指标的情景。但是worker只能对Accumulator进行累加操作,不能读取它的值。只有Driver程序可以读取Accumulator的值。
累加器的实现是使用到了一个不同的序列化手法。在累加器创建时,其就被赋予一个独一无二的id;当被存储时,累加器的序列化形式包含其id和对应其类型的“0”值。在工作结点,为每个使用本线程变量来运行任务的线程创建一份累加器的拷贝,并在任务启动时将其重置为“0”。在每个任务都运行之后,工作结点向驱动程序发送一段信息,信息中包含其对于变量累加器所做的更新。当然在任务因失败而重新执行情况下,驱动确保只一次使用从每个操作的每个分区中传来的更新以防重复计算。
示例及说明:
文中给出了几个代码示例说明了spark与一般运算平台的不同。
1、
val file = spark.textFile("hdfs://...")
val errs = file.filter(_.contains("ERROR"))
val ones = errs.map(_ => 1)
val count = ones.reduce(_+_)
这段代码统计了hdfs中一个log文档的出现ERROR的行数。加上val cachedErrs = errs.cache()这句以后,errs这个中间计算节点就可以缓存在内存中,以后再使用就会很快速。rdd流如下
上边的操作会生成一系列数据集(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寻找转换分区的本地缓存副本;同时每个分区最佳位置开始就等同于此分区在其父辈中的最优位置,这个性质会保存直到这个分区被缓存到其他结点,之后会进行该分区位置的更新(否则会一直和其父辈同位置)以使得之前结点可挪作他用。这样的设计使得错误的处理变得容易:若一个结点失效,分区将从其父辈数据集中重新读取并最终缓存到其他结点。
最后,分发tasker的时候,scala的闭包(java对象)也需要使用java序列化发送给各个tasker,
2、
// Read points from a text file and cache them
val points = spark.textFile(...)
.map(parsePoint).cache()
// Initialize w to random D-dimensional vector
var w = Vector.random(D)
// Run multiple iterations to update w
for (i <- 1 to ITERATIONS) {
val grad = spark.accumulator(new Vector(D))
for (p <- points) { // Runs in parallel
val s = (1/(1+exp(-p.y*(w dot p.x)))-1)*p.y
grad += s * p.x
}
w -= grad.value
}
文中给出的逻辑回归算法的spark代码,这段代码使用了累加器常量grad来做梯度下降。代码第二个for循环是并行运算的。
代码第56行原文中有误,应该改为
val s=(1/(p.y*(1+exp(-w dot p.x))-1))*p.y
spark逻辑回归算法和hadoop比较
发现对于迭代运算,spark优势明显
3、
val Rb = spark.broadcast(R)
for (i <- 1 to ITERATIONS) {
U = spark.parallelize(0 until u)
.map(j => updateUser(j, Rb, M))
.collect()
M = spark.parallelize(0 until m)
.map(j => updateUser(j, Rb, U))
.collect()
}
的矩阵,U是的矩阵,,R代表各观众-各电影的合适程度,已知历史R矩阵,计算得到U和M矩阵之后就可以用某行U矩阵乘某列M矩阵获得某观众对某电影的好感度。
算法如下:1、随机初始化M;2、固定M,优化U减小与R的误差;3、固定U,优化M减小与R的误差;4、循环2、3
##spark整合编译器
如何将Spark整合进Scala的编译器中的呢
Scala是通过将使用者码进去的每一行编译成一个类来运行,这个类包含单对象,此对象中又含有本行里的变量或者函数,最终在其结构中跑本行的代码。如:
var x = 5;
println(x)
// 编译器定义一个类,假设是Line1,其中包含变量x。
// 将第二行编译成 println(Line1.getInstance().x)
// 这些类被加载进JVM以完成对于每一行的运行。
基于Scala的以上代码编译运行方式,为了使得其编译器可用于Spark,做了两处改变:
- 使得编译器将其定义的那些类输出到共享文件系统中,从而在工作结点里使用正常的Java类加载器就能加载这些类。
- 改变生成的代码,从而每行的单对象可以直接引用之前行的单个类,而非使用静态方法 getInstance()。这使得无论何时闭包被序列化传送到worker结点中时,可以捕获其引用的单个类的状态。如果不这样做,那么对于单对象的更新(如前设置x = 7)将不会被广播到工作结点中。
感想
spark是一个使用了hadoop,建立在scala框架上的集群并行运算计算引擎。通过阅读这篇论文,对spark的框架和实现、使用有了大致的了解,以后还需要在使用中学习。