本文参考了 http://shiyanjun.cn/archives/744.html 这篇译文,原文由UC Berkeley 的实验室发表。
RDD的概述
RDD是spark计算系统的核心和精华,所以下面针对RDD进行简单的探讨。
先用一个小例子来具象描述一下RDD的主要工作过程:首先你从HDFS中以K-V格式读入一个文件file1,得到r1;r1是我们的第一个RDD,它被载入到内存中。然后对r1操作,找出所有包含“error”的记录,得到r2;r2是我们的第二个RDD,它是从r1经过转换得来的,也被存放在内存中。最后统计r2中的记录数并返回。我们对RDD的权限是只读的,也就是说能查看它的类信息,或者操作它得到一个新的RDD,却不能改变它。而且,假设file1有100M,在HDFS上有两个块存放在不同节点上,那么r1是有两个分区的,分别位于file1的两个块节点上。
现有的大部分的集群计算系统都是数据流模型的,从稳定的物理存储中加载记录,然后传入由一组确定性操作组成的DAG进行执行,最后写回到稳定存储中。这样的数据流模型的计算系统不适用于迭代式算法和交互式数据挖掘(这类计算的特点是反复操作同一组数据,或者反复执行相同的操作)。RDD就是在这样的场景下产生的。它适用于需要多次重用工作集的并行操作,所以适用于迭代式算法,交互式数据挖掘这样的应用场景;同时它还拥有数据流模型的计算系统的优势,就是容错能力,可伸缩性和位置感知调度。
RDD,全称弹性分布式数据集,它最大的特点就是提供了一种高度受限的共享内存模型,也就是说他是只读的记录分区的集合。首先它是只读的,一个RDD只能来自于HDFS等物理存储或者从其他RDD转换而来,这体现出了RDD的高度受限性,但同时这些限制也保证了RDD具有较好的容错机制;其次它是共享内存的,RDD的信息是存放在内存中的,相比于MapReduce要多次读写物理存储来处理中间产出,RDD大大缩减了从物理存储读入和检索所花费的时间;最后,它是分布式的,一个RDD由多个分区组成,每个分区可能存在在不同的机器节点的内存中,从而具有很好的并行性。
RDD的定位
“我们的目标是为基于工作集的应用(即多个并行操作重用中间结果的这类应用)提供抽象,同时保持MapReduce及其相关模型的优势特性:即自动容错、位置感知性调度和可伸缩性。RDD比数据流模型更易于编程,同时基于工作集的计算也具有良好的描述能力。”spark的官方介绍。
说明RDD的定位是解决那些在并行操作中多次重用工作集的计算应用。主要包括两类。一类是迭代式算法,在机器学习和图应用中经常会用到;第二类是交互式数据挖掘工具,也就是用户反复查询一个数据子集。对于这类应用,把常用的数据缓存在内存中,能大大减小因为读取物理存储花费的时间。
同时RDD还要拥有一般的数据流模型的优点,其中最重要的就是自动容错能力。RDD通过lineage实现了较好的容错能力,下面会详细介绍。
RDD适合批处理操作,就是对大量记录执行相同的操作,因为执行转换和恢复数据都会很快;
RDD适合迭代计算,因为可以重用缓存在内存中的RDD,处理速度会很快;
RDD不适合异步细粒度更新,比如增量web爬虫;
RDD不适合大块读写,因为大量数据读入内存中会对内存造成较大负担。
RDD的描述
下面从RDD的内部结构,横向地描述RDD的基本结构;通过RDD的动作,以及RDD的依赖关系,纵向地展示RDD的变换和关系图。希望通过这三部分,能为大家勾勒出一幅RDD的基本轮廓图。
RDD的内部结构
RDD是以Java对象的结构存放在内存中。一个RDD包含:
(1) 一组RDD分区
(2) 元数据,也就是分区模式和存放位置的描述
(3) 对父RDD的一组依赖。这些依赖关系描述了RDD的lineage。
(4) 一个函数,也就是从父RDD执行何种计算得到它。
下面是spark中支持的RDD内部接口
操作 | 含义 |
partitions() | 返回一组Partition对象 |
preferredLocations(p) | 根据数据存放的位置,返回分区p在哪些节点访问更快 |
dependencies() | 返回一组依赖 |
iterator(p, parentIters) | 按照父分区的迭代器,逐个计算分区p的元素 |
partitioner() | 返回RDD是否hash/range分区的元数据信息 |
RDD的动作
RDD的动作有transformation和action两种。Transformation就是把一个RDD变成另一个RDD的转换。action是把RDD通过计算后得到输出结果并返回到driver的动作。所以,一个RDD的来源只可能有两种,一种是来自于外部的物理存储,比如HDFS读入;第二种是从一个RDD转换而来。
下面是spark中支持的RDD动作
转换 | map(f : T ) U) : RDD[T] ) RDD[U] |
动作 | count() : RDD[T] ) Long |
RDD的依赖
RDD的依赖包括宽依赖和窄依赖两种。宽依赖就是子RDD的分区依赖于父RDD的所有分区。窄依赖是子RDD的分区只依赖固定数量个父分区。比如,从一个RDD中找到所有包含“error”的记录,得到新的RDD,这个依赖是窄依赖;而MapReduce中map和reduce之间的shuffle是宽依赖。具体结构可以看下图:
RDD之间的窄依赖大有好处。首先,窄依赖的RDD之间支持pipeline计算。比如,记录通过一个RDD转换为第二个RDD后,就可以直接流水线地再转换成第三个RDD,逐条记录进行操作;而宽依赖关系下,只能等待一个RDD的数据全部计算完成,才能开始shuffle和计算子RDD。而且,窄依赖的容错机制较好,数据损坏只需要恢复父RDD的相应分区就行,而宽依赖的容错却需要对父RDD的所有分区都要恢复。
下面是RDD的两个主要特性的介绍,内存管理和容错机制。
内存管理
RDD的分区是以JAVA对象的方式存放在worker节点的内存中的,存放在内存中的RDD数据可供后续的多个task反复使用。用户可以自行配制是否需要对某个RDD进行缓存,也可以对RDD指定优先级。如果内存空间不足,RDD会根据LRU替换策略(最近最少使用)把一部分RDD数据存放到磁盘中,虽然这样会导致执行效率有一些下降,但确保了Job能继续正常执行。
Spark中进行Task调度时,会优先把一个任务指定给缓存了它的RDD的节点。
RDD的容错机制
RDD通过lineage实现容错机制。所有的RDD之间的依赖关系通过一个逻辑依赖关系图表示,这就是RDD的血统关系。当有某个RDD的分区数据损坏时,依据lineage关系找到其父RDD,重新执行从父RDD的转换得到这个RDD的数据。
注意,如果是窄依赖,那么只需要对损坏的RDD分区找到其父RDD的对应分区数据并执行转换就可以,如果父RDD的数据不存在,继续回溯找父RDD的父RDD,直到找到存在的数据为止,然后依次执行一系列转换进行数据恢复;如果是宽依赖,则需要得到父RDD的所有分区并进行shuffle,代价相对较大。
除了依据lineage的容错机制,RDD还支持检查点机制。因为当lineage链很长,或者是宽依赖关系的情况,采用lineage恢复数据耗时还是比较多的,这时候如果在某个合适的点处把RDD进行缓存,或者把宽依赖的父RDD进行本地物化存储,就能降低恢复的代价。检查点机制在RDD的介绍中并不是重点,不知道目前的使用情况如何,有待继续调研。