原文链接:http://nil.csail.mit.edu/6.824/2020/papers/zaharia-spark.pdf
摘要
Resilient Distributed Datasets (RDDs)把计算过程都在内存中进行,因此效率有很大的提升。同时,RDD可以更好地支持循环执行算法和交互式数据挖掘工具。RDD可以支持当前分布式大部分的算法范式,流入pregel等。
简介
在很多OLAP(Online Analytical Processing)过程中,数据都是可以复用的。很多机器学习或数据挖掘算法都会反复使用相同数据,例如PageRank(边连接关系)、K-means(各个点位置)、逻辑回归等。此外,用户在同一数据集上进行多次query查询也会复用数据。但是现有框架通常无法合理复用数据,例如Hadoop实现MR。
Spark最常见的对比对象就是hadoop。Hadoop实现MR的过程高度依赖文件系统,每一次MR处理都需要把中间结果存放在本机上,把最终结果放置到类似GFS的全局文件系统上。下一次MR处理相同数据得重新读取,很浪费时间(I/O问题是分布式里不可忽略的一个问题)。
RDD支持将多次复用的中间结果persist在内存里,另外通过粗粒度的转换方式(map、filter、join等)也方便在丢失后迅速重现。
RDD接口模型可以高效表达很多不同系统的编程模型,例如Haloop、Pregel、SQL等,应用范围极广。
Resilient Distributed Datasets(RDDs)
RDD概述
RDD是一个只读、分区记录的集合。RDD只可以通过指定方式在(1)data in stable storage or (2) other RDDs的基础上创建。
RDD无需时刻记录自身状态,它可以通过自身经历的lineage graph来快速恢复。
用户可以按照自己的意图控制RDDs的persist和partition信息。
Spark提供的接口可以分为两类:transformations和actions。Spark采用lazy compute的方式处理RDD,即每次transformation的时候都不会记录RDD的状态信息,直到action的时候才会一次性计算RDD的状态。
Spark通常把信息储存在内存中,但是内存不够的情况下Spark也会把信息存储在磁盘中。
RDD lineage图示例:
RDD的优势
1、RDD不需要通过checkpoint之类的机制保存快照等状态信息,可以通过lineage快速恢复。
2、大部分情况只有出现问题的partition才需要重新计算,已完成的其他内容不受影响。
3、与MapReduce一样,如果有执行过慢的节点,可以启动另一个back-up节点执行相同的任务,来加快任务的执行。
4、系统可以调节各个partition的位置,通过data locality来提升执行速率。
5、内存不够时可以把信息储存在磁盘中,与现有数据流系统一致,不会表现更差。
RDD适用范围
RDD本身还是OLAP模型,不适合作为商业应用的直接存储数据库(即用于OLTP)。
Spark编程接口
Spark在运行时,用户的driver程序启动多个worker,worker从分布式文件系统中读取数据块,并将计算后的RDD分区缓存在内存中。
Spark设计时用的语言是scala,可以通过反射实现闭包。
RDD的主要操作分为transformations和actions,具体分类如下所示:
Spark不鼓励使用checkpoint,但在必要时可以将计算结果缓存下来以方便进行恢复。
例如在多次循环的PageRank算法中,其lineage图如下所示:
Spark便可以将中间某次的ranks结果给缓存起来,以减少发生错误重新计算的时间花销。
另外,可以通过控制Partitioning的方式来优化计算。本例中,可以将ranks和links想对应的部分partition到一起,这样可以在同一个节点内部进行处理而无需进行特殊的通信。
Spark使用Pregel模型实现PageRank功能的示例代码:
# Pagerank in the Pregel model
from pyspark.sql.functions import coalesce, col, lit, sum, when, min
from graphframes.lib import Pregel
# Need to set up a directory for Pregel computation
sc.setCheckpointDir("checkpoint")
v = g.outDegrees
g = GraphFrame(v,e)
# withVertexCOlumn先定义一个初始值,然后用后面的coalesce顶替
ranks = g.pregel \
.setMaxIter(5) \
.sendMsgToDst(Pregel.src("rank") / Pregel.src("outDegree")) \
.aggMsgs(sum(Pregel.msg())) \
.withVertexColumn("rank", lit(1.0), \
coalesce(Pregel.msg(), lit(0.0)) * lit(0.85) + lit(0.15)) \
.run()
ranks.show()
# pyspark.sql.functions.coalesce(*cols): Returns the first column that is not null.
# Not to be confused with spark.sql.coalesce(numPartitions)
值得特别注意的是,一般Partition的数量要大于节点的数量,以便实现负载均衡(这样的话某个节点执行速度快可以快速接手剩余的内容)。
Representing RDDs
RDD可以通过以下五项表述自身全部信息:
值得特别一提的是depencies。我们可以将spark的依赖分为两种:窄依赖,即一个parent partition最多会被一个子partition使用;宽依赖,即一个parent partition可能会被多个子partitions使用。(这个定义需要注意,容易搞错理解为一个子partition依赖于多个parent partition)
与之相对,窄依赖恢复partition只需执行上面的那个partition即可;而宽依赖在恢复的时候可能需要重新执行全部的partition运行流程,时间开销很大。
另外实践中,关于宽依赖和窄依赖的优化执行逻辑也不一样。
一个指令通常对应于特定的依赖方式,示意图如下所示:
Implementation
Spark的scheduler会考虑哪些RDD在memory中。当用户执行某个action时,spark会计算出包含stage的有向无环图(DAG)。每个stage包含尽可能多的narrow transformation,不同stage间以宽依赖进行区分。
最终spark也会以stage为子单位进行子任务重新运行。
scheduler在执行tasks的会延后执行并考虑数据locality。如果一个任务用到的partition知道在某一个node上,会将任务分发给该node,有倾向性描述也会运送过去。
值得特别注意的是,我们会把每一个stage(进行宽依赖转换)前的中间结果保存起来以便于错误恢复(就像MapReduce保存map outputs到内存上一样)。
当一个任务失败时,只要其parent stage没问题,就分发到另一个节点上重新执行。目前并不支持调度器失败,尽管这处理起来很简单。
内存管理
Spark为缓存RDD提供了3个选择:
1、在内存中反序列化为Java对象。这种方式性能最快,因为Java VM可以在本地访问每一个RDD元素。
2、在内存中反序列化为数据。这种方式允许用户在空间受限时,可以选择一种比Java对象更有效的内存策略。
3、存储在磁盘上。这种方式对于太长的以致不能放在RAM中的RDD比较有用,但在使用时计算代价较高。
为了管理有限的内存空间,提出了RDD级别上的LRU策略(最近最少使用)。当计算一个新的RDD时所需空间不足,便将最近最少使用的RDD替换出去,除非如它与具有新分区的RDD是同一个RDD。这种情况下,在内存中记录旧分区,以防止同一个RDD循环的进来、出去。
另外,用户可以为每个RDD指定“缓存优先级”