Spark RDD简介
我相信时至今日,应该没有人没有听说过Spark这个系统的大名。在众多的互联网企业中,或多或少都一定程度的部署或者使用了这套系统,应用相当广泛。
那么,为什么Spark会如此成功?相对于其他之前的系统,Spark解决了什么问题?其设计又能给我们什么样的启发?为了了解这些,除了阅读Spark社区的一些公开文献外,我也非常建议读一下Spark RDD核心提出者和设计者Matei的博士毕业论文,看看在系统设计初期,作者是如何考虑的。
在Spark出来之前,是各种MapReduce及其衍生系统的天下。有人说Spark的设计目标是解决MR表达缺陷的问题,对于这点,我觉得并不正确。在MR中,用户可以用Map来表达单一记录的处理逻辑、Shuffle/GroupByKey及Reduce来表达多条记录之间的关联计算逻辑,其表达能力是完备的。但,这并不意味着MR是个完美的系统,其在计算效率上存在一定的问题。Matei经过定位发现,在大多数大数据计算中,存在许多共享数据,这些数据被反复的进行网络传输或者引发落盘IO。比如PageRank和各种机器学习迭代型算法,往往一轮迭代的产出是下一轮迭代的输入;Map计算的结果,需要传输到Reduce所在的server进行汇聚计算等。在大多数数据中心,网络和磁盘IO的资源往往是瓶颈,值得在上面进行优化。
那么,如何解决这些问题?Matei提出了RDD。我们可以将RDD理解为一个大的分布式Dataset,被系统管理并分散放置众多物理机上(尽量cache在内存中)。用户编写的数据处理代码,也以RDD对象为核心进行操作(参考下图)。Spark记录并组织出对应的computation graph,并计算出每个RDD需要经历的计算过程(可以参考FlumeJava的Defer-Evaluation加Fussion过程)得到一个lineage graph,然后将相关的处理代码,序列化后发送到RDD所在的物理机器上执行。如此一来,许多计算只需要在本地、基于内存进行计算即可。大多数情况只有在进行GroupByKey/Shuffle时,才需要将数据走网络或者分布式存储发送给其他Worker节点。
这种模式可以显著的减少数据的传输和磁盘IO,主要表现在两个方面。首先,之前众多系统采用的是算子固定部署在Worker上,然后让数据”流“过这些算子的方式进行计算;而Spark反其道而行之,将数据固定、把序列化后的计算代码发送并apply到这些数据集上。这显然减少了数据的传输规模;其次,由于大数据处理场景,大量的计算是SIMD(Single Instruction Multipile Data)模式,即将一条指令并行的应用到众多的数据上。因此,分发计算code的代价远远低于分发数据。此外,RDD计算往往具有局部性,这就意味着系统很容易实现Cache策略,让数据尽量的停留在内存中,进一步提升了计算的效率。
RDD在概念上,是不可改写的。也就是说,如果一个RDD的partition出现了缺失,系统可以根据这个RDD partition的computation graph,重新从源头开始重新发起计算,进而实现容灾。为了防止容灾时需要重算的内容过多,Spark也会根据一定策略,将RDD的checkpoint持久化存储下来。在之前许多系统中,如果一个Worker节点发生故障,那么系统会寻找另外一个健康的Worker替代、发起重算,如果需要重算的内容过多,就可能将整个计算block在这个恢复节点上。在Spark系统中,恢复RDD的计算是分散到众多的Worker节点上的,也就是说系统是并发的进行恢复,这种模式能够显著的降低recovery对计算作业延时的影响。
在RDD这个核心抽象的基础上,除了实现类MR的batch型计算,社区还发展出Graph、SQL等各种不同的计算引擎,形成了整个Spark生态。由于其中的RDD抽象是共通的,这就使得这些框架是可以交换数据的。这个在现实生产环境中是个非常有用的特性,我可以根据数据计算的不同特点,将整个数据处理分解成不同的阶段、每个阶段采用最方便表达的引擎来完成,最大程度的减少跨计算引擎所引发的成本(比如encoding/decoding数据,数据重分发等)。我相信这也是Spark在众多应用中能够脱颖而出的重要原因。
Spark系统经过近10年的发展,已经成为了大数据处理系统的“标配”,被广泛使用。如果有志于从事大数据分析或机器学习领域的工作,Spark几乎是一个必须掌握的系统。其中RDD的设计思想,是该系统最核心的突破,十分优雅、简洁,值得每个技术人员学习。