你好,我是黄金。今天我们要来一起回顾复习的论文内容,是开源系统Spark的这篇引入了RDD概念的论文。

RDD介绍

RDD的全称是弹性分布式数据集,它允许开发人员在大规模集群上,以容错的方式执行内存计算。而Spark就是实现了RDD的分布式计算框架。

在Spark出现之前,并没有通用的分布式计算框架,可以高效地运行迭代算法。MapReduce是通用的分布式计算框架,但不管是Mapper任务还是Reducer任务,它们的执行结果都需要写入硬盘。这样一来,由多个MapReduce组合而成的迭代算法程序,运行起来就不够高效。因此,如何有效地利用分布式内存,就成为了研究的重点。

而在MapReduce之后,也出现了一些可以利用分布式内存的计算框架,它们把运算的中间结果保存在内存当中。这些计算框架确实提升了执行效率,但是不够通用,只能服务于特定的算法。

直到Spark的出现,才有了既高效又通用的分布式内存计算框架。

容错的分布式内存数据集

在设计RDD的时候,主要的挑战就是如何定义编程接口,才能让RDD具备有效的故障恢复能力。

我们先来看看MapReduce的结果集是如何容错的。Mapper任务把执行结果写入本地文件,服务器即使宕机,重启后依然可以读取结果。对于不能恢复的服务器,只需要把它负责的任务交给其他服务器,重新执行一遍即可。而Reducer任务是把执行结果写入HDFS,由HDFS提供容错支持。

那么,RDD保存在内存中,又要如何避免服务器宕机带来的影响呢?

它的方式是利用上游的RDD,重新执行一遍任务,来生成丢失的数据。RDD以分布式的方式存在,也就是由多个数据分片构成,一个数据分片损坏了,只需要重新生成这一个分片的数据就好。所以RDD在编程接口上支持的更新操作,是粗粒度的操作。

所谓的粗粒度,是指RDD需要在整个数据集上执行完更新才可见,更新到一半的数据集不可见。RDD是只读数据集,更新操作会让一个RDD变成另一个RDD,不存在中间状态,这就让恢复数据变得容易了。

RDD定义和Spark编程接口

我们再来看看RDD的定义,论文中讲RDD是只读的、已分区的记录集合,RDD只能通过明确的操作,以及通过两种数据创建:稳定存储系统中的数据;其他RDD。 这个明确的操作,是指map、filter和join这样的操作。

相信你现在应该就能明白,为什么RDD需要定义成只读的、经过明确操作来创建,其实这都是为了支持有效容错

Spark为RDD提供的编程接口有两种,分别是转换操作Transformation和行动操作Action。转换操作就是map、filter和join之类的操作,行动操作就是count、reduce和collect之类的操作。

Spark使用惰性求值,直到调用行动操作,它才开始执行之前定义的一系列转换操作和当前这个行动操作。这些操作共同构成了一个拓扑图,在论文中被称为Lineage graph。

窄依赖和宽依赖

那么,在这个拓扑图中,我们可以把RDD之间的依赖关系分成两种,一种是窄依赖,一种是宽依赖:

而区分这两种依赖到底有什么作用呢?

对于窄依赖而言,我们可以在RDD的数据分片所在的节点上,执行转换操作,让计算靠近存储,避免数据在网络上传输。而如果多个连续的转换操作构成窄依赖,那它们都可以在同一个节点,并且是数据分片所在的节点上执行。另外,窄依赖的下游RDD如果损坏了,只需要根据上游RDD重新计算,来恢复丢失的分区,由于它依赖的节点少,所以恢复的速度会很快。

对于宽依赖而言,我们需要从多个上游RDD获取数据。下游RDD损坏时,也可以通过多个上游RDD恢复。不过恢复宽依赖的下游RDD,消耗的网络带宽和计算资源,会比窄依赖的下游RDD大得多。这个时候,我们其实可以持久化下游RDD,当出现故障时就从磁盘中恢复,来加快恢复的速度。

性能表现

在论文的第6节Evaluation,作者还对比了逻辑回归、K-means两种迭代机器学习应用,在三种系统上运行的性能表现,这三种系统分别是:Hadoop、HadoopBinMem、Spark,其中HadoopBinMem是使用内存存储数据的Hadoop。

令人惊讶的是,Spark比使用内存存储数据的HadoopBinMem还要快20倍。这是什么原因呢?

首先是Hadoop软件栈的最小开销,开始时的初始化工作,结束后的清理工作,都要花不少时间;其次是处理数据时Hadoop的开销,处理每个Block都需要执行多次内存拷贝,计算校验和;最后是二进制和Java对象之间的转换开销,HadoopBinMem的内存数据使用二进制表示,Spark的内存数据直接用Java对象表示,这就省掉了转换的开销。

小结

最后,让我们回到RDD的全称,弹性分布式数据集,来理解下“弹性”的含义。徐老师说这个弹性体现在两个方面: