Spark 经典论文笔记
Resilient Distributed Datasets : A Fault-Tolerant Abstraction for In-Memory Cluster Computing
为什么要设计spark
现在的计算框架如Map/Reduce在大数据分析中被广泛采用,为什么还要设计新的spark?
- Map/Reduce提供了高级接口可以方便快捷的调取计算资源,但是缺少对分布式内存有影响的抽象。这就造成了计算过程中需要在机器间使用中间数据,那么只能依靠中间存储来保存中间结果,然后再读取中间结果,造成了时延与IO性能的降低。
- 虽然有些框架针对数据重用提出了相应的解决办法,比如Pregel针对迭代图运算设计出将中间结果保存在内存中,HaLoop提供了迭代Map/Reduce的接口,但是这些都是 针对特定的功能设计的不具备通用性。
针对以上问题,Spark提出了一种新的数据抽象模式称为RDD(弹性分布式数据集),RDD是容错的并行的数据结构,并且可以让用户显式的将数据保存在内存中,并且可以控制他们的分区来优化数据替代以及提供了一系列高级的操作接口。
RDD数据结构的容错机制
设计RDD的主要挑战在与如何设计高效的容错机制。现有的集群的内存的抽象都在可变状态(啥是可变状态)提供了一种细粒度(fine-grained)更新。在这种接口条件下,容错的唯一方法就是在不同的机器间复制内存,或者使用log日志记录更新,但是这两种方法对于数据密集(data-intensive大数据)来说都太昂贵了,因为数据的复制及时传输需要大量的带宽,同时还带来了存储的大量开销。
与上面的系统不同,RDD提供了一种粗粒度(coarse-grained)的变换(比如说map,filter,join),这些变换对数据项应用相同的操作。这样使用日志记录数据集是如何产生的而不是去记录数据本身,使得容错变得高效。一旦某个parition丢失的话,通过日志可以找出该partition是如何通过其他的RDD经过什么操作得到的,因此可以快速的重新再计算来修正错误,同时也不需要数据的复制。
RDD
通常来讲,RDD是一种只读的,分区(partitioned)式的数据集合。RDD创建的方式只用两种:要不从现有的存储上读取数据,要么从别的RDD转化(transformations)而来。RDD并不不要每时每刻都具体化(理解为计算出结果???),一个RDD包含了它是如何计算出来如果分区的,这种特性带来一个好处:程序不能引用经过失败不能重组的RDD(这是啥意思??)。
对于RDD,用户可以设置的参数为persistence
和partitioning
。用户可以决定哪些RDD是需要的重用的然后选择一种保存策略对该RDD进行保存(肯定有IN-MEMORY,IN-DISK,常驻内存和常驻磁盘)。用户也可以设定RDD的元素是如何基于每一个Record中的key来进行分区的。
RDD的好处
- 一旦某个partition丢失,只需要按照transformations的世袭(lineage)进行重新计算就可以恢复数据,程序不用做任何回滚操作。
- A second benefit of RDDs is that their immutable nature lets a system mitigate slow nodes(stragglers) by running backup copies of slow tasks as in MapReduce(没看懂)
RDD不适宜的场景
数据需要异步更新,或者增量更新的情况下,RDD就不太合适了,因为RDD每次都是都数据做相同的操作,在异步更新中,每个或者每批数据的更新可能都是不相同的。但是在流处理中不就是相当于是增量更新??
RDD的表示
RDD的组成分为以下5个部分:
- 分区集合,数据的原子片,就是数据的不同存放分区
- 父RDD的依赖信息
- 函数,用以从父RDD计算当前的RDD
- 分区数据存放的数据帧格式的元数据
- 数据分布RDD接口表如下表所示
Operation | Meaning |
partitions() | 返回一个 Partition 对象的列表 |
preferredLocations(p) | 根据数据列出分区p可以更快的被读取 |
dependencies() | 返回一系列的依赖 |
iterator(p,parentIters) | 给定父分区的迭代器后计算当前分区的元素的值 |
partitioner() | 返回元数据,指定RDD是否为散列/范围分区 |
依赖的种类:
窄依赖:一个父RDD最多被一个子RDD依赖,比如说map操作
宽依赖:一个父RDD可能被多个子RDD所依赖,比如说join操作
为什么要做这样的依赖区分?
窄依赖可以对某个RDD在某个单独的节点上顺序执行一系列的操作,比如在map之后使用filter等一连串的操作。而宽依赖则需要某节点需要对所有的父节点都可用,可能需要在节点之间对数据进行重新洗牌操作(shuffled)。其二,一旦错误发生,窄依赖的错误恢复要简单的多,因为只有一个分区需要被重新计算,其他的不受错误影响。相比之下,在具有广泛依赖性的谱系图中,单个故障节点可能会导致RDD的所有祖先丢失一些分区,从而需要完全重新执行。
我们在下面描绘一些RDD实现。
- HDFS.使用DHFS创建RDD时,partition的生成是根据HDFS上的文件块进行生成的,每一个文件块都生成一个partition。
patitions
函数为为每一个文件返回一个分区, 文件块的偏移量都记录在Partition对象中。preferredLocations
给出了每个文件块所在的位置,iterator
可以读取该文件块。 - map.使用map生成RDD时,其partition信息和其父RDD的相同
- union.使用union操作后分区也是两个父RDDpartition的联合,子分区通过一个窄依赖计算两个父节点的结果然后合并到一起
- sample. 和map类似,但是RDD内部保存了一个随机数生成种子用以随机挑选父RDD的records。
- join. join可能时窄依赖也可能时宽依赖也有可能时混合类型的。
Spark的实现
实现Spark总共14000行scala代码。以下介绍spark的作业调度、内存管理以及检查点支持(checkpoints)
作业调度
stage划分准则,每个stage中尽量包含更多的窄依赖。每当用户在RDD上运行actions(例如,计数或保存)时,调度程序将检查RDD的谱系图以构建执行阶段的DAG。每个阶段包含尽可能多的具有窄依赖关系的流水线转换。阶段的边界是广泛依赖性所需的shuffle操作,或任何已经计算出的可能使父RDD计算短路的分区。然后,调度程序启动任务以计算每个阶段的丢失分区,直到计算出目标RDD为止。
解释器的设计
此部分暂时忽略
内存管理
spark针对RDD的保存提供了三种方式:作为序列化的对象保存在内存中,作为序列化的数据保存在内存中,直接保存在硬盘中。
- 内存中的序列化对象:表现最好,主要在于JVM可以直接读取每一个RDD元素
- 内存中的序列化数据:表现次之,但是在内存有限的情况写很好,
- 硬盘保存:性能最差,但是对于数据量特别大的情况较为适用
在内存有限的情况下,我们使用LRU策略进行管理RDD,当得到一个最新的RDD但是空间不足时就从最近一直没有使用的RDD中弹出一个位置。但是如果新旧相同那么直接保存旧的,新的抛弃避免相同的RDD进行频繁的入和出。
checkpoint支持
虽然RDD可以通过世袭进行重新计算,但是如果世袭的族谱比较长的话,计算起来就会很耗费时间,所以引入checkpoint的方式,将某个时间点的一些RDD保存在硬盘上,一旦出了问题就直接从检查点进行恢复,避免重复计算。
Evaluation
与之前的技术进行比较,这里不做过多介绍,有兴趣的可以找出该论文进行查看