Spark源码系列(一) RDD详解
文章目录
Spark理论基石-RDD
概述
RDD叫做弹性分布式数据集(Resilient Distributed Datasets),它是一种分布式的内存抽象,表示一个只读的记录分区的集合,它只能通过其他RDD转换而创建,为此,RDD支持丰富的转换操作(map、join、filter、groupBy等),通过这种转换操作,新的RDD则包含了如何从其他RDDs衍生所必需的信息,所以说RDDs之间是有依赖关系的。基于RDDs之间的依赖,RDDs会形成一个有向无环图DAG,该DAG描述了整个流式计算的流程,实际执行的时候,RDD是通过血缘关系(Lineage)一气呵成的,即使出现数据分区丢失,也可以通过血缘关系重建分区。总结一下,基于RDD的流式计算任务可描述为:从稳定的物理存储(如分布式文件系统HDFS)中加载记录,记录被传入由一组确定性操作构成的DAG,然后最终写回稳定存储。RDD还可以将数据集缓存到内存中,使得在多个操作之间可以重用数据集,基于这个特点可以很方便地构建迭代型应用或者交互式数据分析应用。而MR的痛点就是每一个步骤都要落盘,使得不必要的开销很高。
对于分布式系统,容错支持是必不可少的。为了支持容错,RDD只支持粗粒度的变换,即输入数据集是immutable的,每次运算都会产生新的输出。不支持对一个数据集中细粒度的更新操作。这种约束,大大简化了容错支持,并且能满足很大一类的计算需求。初次接触RDD的概念的时候,不大能够理解为什么要以数据集为中心做抽象。后来在接触Spark越来越多的情况下,对数据集的一致性抽象正是计算流水线(pipeline)得以存在和优化的精髓所在。在定义了数据集的基本属性(不可变、分区、依赖关系、存放位置等)后,就可以在此基础上施加各种高阶算子,以构建DAG执行引擎,并做适当优化。从这个角度来说,RDD确实是一种精妙的设计。
在RDD论文中,主要的关键设计点在于:
- 显式抽象。将运算中的数据集进行显式抽象,定义了其接口和属性。由于数据集抽象的统一,从而可以将不同的计算过程组合起来进行统一的DAG调度。
- 基于内存。和MapReduce相比,MR中间结果必须落盘,RDD通过将结果保存在内存中,从而大大减低了单个算子计算延迟以及不同算子之间的加载延迟。
- 宽窄依赖。在进行DAG调度时,定义了宽窄依赖的概念,并以此进行阶段划分,优化调度计算。
- 谱系容错。主要依赖谱系图计算来进行错误恢复,而非进行冗余备份,因为内存有限,所以采用计算替换存储。
- 交互查询。修改了Scala的解释器,使得可以交互式的查询基于多机内存的大型数据集。进而支持类SQL等高阶查询语言。
RDD的由来
Dryad和MapReduce是业界中已经流行的大数据分析工具。它们给用户提供了一些高阶算子来使用,对用户屏蔽了底层的细节问题,使得用户只需要关注于业务逻辑层面。但是它们都缺少对分布式内存的抽象,不同计算过程之间只能够通过外存来耦合:前驱任务将计算结果写到外存上去,后继任务再将其作为输入加载到内存,然后才能接着执行后继计算任务。这样的设计有两个很大的缺陷:复用性差、延迟较高。这对于像K-Means,LR等要求迭代式计算的机器学习算法非常不友好;对于一些随机的交互式查询也是灾难,根本无法满足需求。因为这些框架将大部分的时间都耗费在数据备份、硬盘IO和数据序列化之上。
在RDD之前,为了解决数据复用问题,业界上已经有过很多次尝试。包括将中间结果放在内存中的迭代式图计算系统Pregel,以及将多个MR串联,缓存循环不变量的Hadoop。但是这些系统只支持受限的计算模型(MR),而且只进行隐式的数据复用。如何进行更通用的数据复用,以支持更复杂的查询计算,是一个仍未解决的难题。
RDD正是为解决这个问题而设计的,高效地复用数据的一个数据结构抽象。RDD支持数据容错、数据并行;在此之上,能够让用户利用多机内存、控制数据分区、构建一系列运算过程。从而解决很多应用中连续计算过程对于数据复用的需求。
对内存数据的容错一直是比较难以解决的问题。现有的一些基于集群内存的系统,比如分布式KV、共享内存都提供一种可以细粒度的修改的可变数据集抽象。为了支持这种抽象之上的容错,就需要进行数据多机冗余或者操作日志备份。这些操作都会导致多机间大量的数据传输,由于网络传输远慢于RAM,使得分布式利用内存这件事失去其优势。
而RDD只提供粗粒度的、基于整个数据集的计算接口,即对数据集中的所有条目都施加同一种操作。因此,对于RDD,为了容错,我们只需要备份每个操作而非数据本身。在某个分区数据出现问题进行错误恢复时,只需要从原始数据集出发,按照顺序再计算一遍即可。
RDD特点
RDD表示只读的分区的数据集。RDD只可以通过对持久存储或其他RDD进行确定性运算得来,这种运算被称为变换。常用的变化算子包括:map,filter和join。
RDD没有选择不断的做checkpoint以进行容错,而是会记录下RDD从最初的外存的数据集变化而来的变化路径,也就是lineage。理论上所有的RDD都可以在出错后从外存中依据lineage进行重建。如果lineage较长,可以通过持久化RDD来切断血缘关系。一般来说,重建的粒度是分区而非整个数据集,一是代价更小,二是不同分区可能在不同机器上。
用户可以对RDD的两个方面进行控制:持久化和分区控制。对于前者,如果某些RDD需要复用,那么用户可以指示系统按照某种策略将其进行持久化。后者来说,用户可以定制分区路由函数,将数据集中的记录按照某个KV路由到不同分区。比如进行join操作的时候,可以将待join数据集按照相同的策略进行分区,从而可以并行join。
分区
如下图所示,RDD逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个compute函数得到每个分区的数据。如果RDD是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据,如果RDD是通过其他RDD转换而来,则compute函数是执行转换逻辑将其他RDD的数据进行转换。
只读
如下图所示,RDD是只读的,要想改变RDD中的数据,只能在现有的RDD基础上创建新的RDD。
由一个RDD转换到另一个RDD,可以通过丰富的操作算子实现。不再像MapReduce那样只能写map和reduce。如下图所示:
RDD的操作算子包括两类,一类叫做transformations,它是用来将RDD进行转化,构建RDD的血缘关系;另一类叫做actions,它是用来触发RDD的计算,得到RDD的相关计算结果或者将RDD保存的文件系统中。下图是RDD所支持的操作算子列表。
依赖
RDDs通过操作算子进行转换,转换得到的新RDD包含了从其他RDDs衍生所必需的信息,RDDs之间维护着血缘关系,也称之为依赖。在RDD的接口设计中最有趣的一个点是如何对RDD间的依赖关系进行规约。最后发现可以将所有依赖归纳为两种类型:
- 窄依赖(narrow dependencies):父RDD的分区最多被一个子RDD的分区所依赖,比如map
- 宽依赖(wide dependencies):父RDD的分区可能被多个子RDD的分区所依赖,比如join
调度优化。对于窄依赖,可以对分区间进行并行流水化调度,先计算完成某个窄依赖算子(比如说map)的分区不用等待其他分区而直接进行下一个窄依赖算子(比如filter)的运算。与之相对,宽依赖的要求父RDD的所有分区就绪,并进行跨节点的传输后,才能进行计算。类似于MapReduce中的shuffle。
数据恢复。在某个分区出现错误或者丢失时,窄依赖的恢复更为高效。因为涉及到的父分区相对较少,并且可以并行恢复。而对于宽依赖,由于依赖复杂(子RDD的每个分区都会依赖父RDD的所有分区),一个分区的丢失可能就会引起全盘的重新计算。
这样将调度和算子解耦的设计大大简化了变换的实现,大部分变换都可以用少量代码实现。由于不需要了解调度细节,任何人都可以很快的上手实现一个新的变换。比如:
- HDFS文件:partitions函数返回HDFS文件的所有block,每个block被当做一个partition。preferredLocations返回每个block所在的位置,Iterator会对每个block进行读取。
- map:在任意RDD上调用map会返回一个MappedRDD对象,该对象的partitions函数和prederredLocations与父RDD保持一致。对于iterator,只需要将传给map算子的函数以此作用到其父RDD的各个分区即可。
- union:在两个RDD上调用union会返回一个新的RDD,该RDD的每个分区由对应的两个父RDD通过窄依赖计算而来。
- sample:抽样函数和map大体一致。但该函数会给每个分区保存一个随机数种子来决定父RDD的每个记录是否保留。
- join:在两个RDD上调用join操作可能会导致两个窄依赖(比如其分区都是按待join的key哈希的),两个宽依赖或者混合依赖。每种情况下,子RDD都会有一个partitioner函数,或继承自父分区,或者是默认的hash分区函数。
通过RDDs之间的这种依赖关系,一个任务流可以描述为DAG,如下图所示,在实际执行过程中宽依赖对应于shuffle(图中的reduceBykey和join),窄依赖中的所有转换操作可以通过类似于管道的方式一气呵成(图中map和union可以一起执行)。
缓存
如果应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接取缓存而不用再根据lineage重新计算,这样就加速后期的重用。如下图所示,RDD-1经过一系列的转换后得到RDD-n并保存到HDFS,RDD-1在这一过程中会有个中间结果,如果将其缓存到内存,那么在随后的RDD-1转换到RDD-m的过程中,就不会计算之前的RDD-0.
检查点机制
尽管所有失败的RDD都可以通过lineage来重新计算,但是对于某些lineage特别长的RDD来说仍然是一个很耗时的动作。因此提供RDD级别的外存检查点有很大的帮助。
对于具有很长的lineage并且lineage中存在很多宽依赖的RDD,在外存上做checkpoint会很有帮助,因为某一两个的分区可能会引起全盘的重算。对于这种冗长的计算拓扑来说,依据lineage重算是非常浪费时间的。而对于只有窄依赖、并且不那么长的lineage来说,在外存做checkpoint可能有些得不偿失,因为它们可以很简单的通过并行计算出来。
Spark现阶段提供检查点的API(给persist函数传REPLICATE标志),然后由用户来决定是否对其持久化。但我们在思考,是否可以进行一些自动的检查点计算。由于调度器知道每个数据集的内存占用以及计算使用时间,我们或许可以选择性的对某些关键RDD进行持久化以最小化宕机恢复时间。
最后,由于RDD的只读特性,我们在做检查点时不用像通用共享内存模型那样过分考虑一致性的问题,因此可以用后台线程默默地干这些事情而不用影响主要工作流,也不用使用复杂的分布式的快照算法来解决一致性问题。
不适合使用RDD的应用
RDD适用于具有批量转换需求的应用,并且相同的操作用于数据集的每一个元素上。在这种情况下,RDD能够记住每个转换操作,对应于lineage中的每一个步骤,恢复丢失分区数据时不需要写日志来记录大量数据。RDD不适合那些通过异步细粒度地更新来共享状态的应用,例如Web应用中的存储系统,或者索引Web数据的系统。RDD的应用目标是面向批量分析应用的特定系统,提供一种高效的编程模型,而不是一些异步应用程序。
小结
给定一个RDD我们至少可以知道如下几点信息:
- 分区数以及分区方式
- 由父RDDs衍生而来的相关依赖信息
- 计算每个分区的数据,计算步骤为:如果被缓存,则从缓存中获取的分区数据;如果被checkpoint,则从checkpoint处恢复数据;根据lineage计算分区数据
编程模型
在Spark中,RDD被表示为对象,通过对象上的方法调用来对RDD进行转换。经过一系列的transformation定义RDD之后,就可以调用actions触发RDD的计算,action可以是向应用程序返回结果(count、collect),或者是向存储系统保存数据(saveAsTextFile等)。在Spark中,只有遇到action,才会执行RDD的计算(即延迟计算),这样在运行时可以通过管道的方式传输多个转换。
要使用Spark,开发者需要编写一个Driver程序,它被提交到集群以调度运行Worker,如下图所示。Driver中定义了一个或多个RDD,并调用RDD上的action。Worker则执行RDD分区计算任务。
应用举例
下面介绍一个简单的spark应用程序实例WordCount,统计一个数据集中每个单词出现的次数,首先从HDFS中加载数据得到原始RDD-0,其中每条记录为数据中的一行句子,经过一个flatMap操作,将一行句子切分为多个独立的词,得到RDD-1,再通过map操作将每个词映射为key-value形式,其中key为词,value为初始计数值1,得到RDD-2,将RDD-2中的所有记录归并,统计每个词的计数,得到RDD-3,最后将其保存到HDFS。
import org.apache.spark._
import SparkContext._
object WordCount {
def main(args: Array[String]) {
if (args.length < 2) {
System.err.println("Usage: WordCount <inputfile> <outputfile>");
System.exit(1);
}
val conf = new SparkConf().setAppName("WordCount")
val sc = new SparkContext(conf)
val result = sc.textFile(args(0))
.flatMap(line => line.split(" "))
.map(word => (word, 1))
.reduceByKey(_ + _)
result.saveAsTextFile(args(1))
}
}
RDD源码解析
上面我们通过理论讲解了什么是RDD,下面从源码角度深度剖析一下RDD。
/**
* A Resilient Distributed Dataset (RDD), the basic abstraction in Spark. Represents an immutable,
* partitioned collection of elements that can be operated on in parallel. This class contains the
* basic operations available on all RDDs, such as `map`, `filter`, and `persist`. In addition,
* [[org.apache.spark.rdd.PairRDDFunctions]] contains operations available only on RDDs of key-value
* pairs, such as `groupByKey` and `join`;
* [[org.apache.spark.rdd.DoubleRDDFunctions]] contains operations available only on RDDs of
* Doubles; and
* [[org.apache.spark.rdd.SequenceFileRDDFunctions]] contains operations available on RDDs that
* can be saved as SequenceFiles.
* All operations are automatically available on any RDD of the right type (e.g. RDD[(Int, Int)]
* through implicit.
*
* Internally, each RDD is characterized by five main properties:
*
* - A list of partitions
* - A function for computing each split
* - A list of dependencies on other RDDs
* - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
* - Optionally, a list of preferred locations to compute each split on (e.g. block locations for
* an HDFS file)
*
* All of the scheduling and execution in Spark is done based on these methods, allowing each RDD
* to implement its own way of computing itself. Indeed, users can implement custom RDDs (e.g. for
* reading data from a new storage system) by overriding these functions. Please refer to the
* <a href="http://people.csail.mit.edu/matei/papers/2012/nsdi_spark.pdf">Spark paper</a>
* for more details on RDD internals.
*/
abstract class RDD[T: ClassTag](
@transient private var _sc: SparkContext,
@transient private var deps: Seq[Dependency[_]]
) extends Serializable with Logging {
if (classOf[RDD[_]].isAssignableFrom(elementClassTag.runtimeClass)) {
// This is a warning instead of an exception in order to avoid breaking user programs that
// might have defined nested RDDs without running jobs with them.
logWarning("Spark does not support nested RDDs (see SPARK-5063)")
}
private def sc: SparkContext = {
if (_sc == null) {
throw new SparkException(
"This RDD lacks a SparkContext. It could happen in the following cases: \n(1) RDD " +
"transformations and actions are NOT invoked by the driver, but inside of other " +
"transformations; for example, rdd1.map(x => rdd2.values.count() * x) is invalid " +
"because the values transformation and count action cannot be performed inside of the " +
"rdd1.map transformation. For more information, see SPARK-5063.\n(2) When a Spark " +
"Streaming job recovers from checkpoint, this exception will be hit if a reference to " +
"an RDD not defined by the streaming job is used in DStream operations. For more " +
"information, See SPARK-13758.")
}
_sc
}
/** Construct an RDD with just a one-to-one dependency on one parent */
def this(@transient oneParent: RDD[_]) =
this(oneParent.context, List(new OneToOneDependency(oneParent)))
RDD的全名是Resilient Distributed Dataset,意思是容错的分布式数据集,就像注释中写到的,每一个RDD都会有五个属性。
- 一个分区的列表(getPartitions)
- 一个用于计算分区中数据的函数(compute)
- 一个对其他RDD的依赖列表(getDependencies)。依赖还具体分为宽依赖和窄依赖,但不是所有的RDD都有依赖。
- 可选:KV型RDD应该有一个分区器,默认是根据Hash来分区的,类似于MapReduce中的partitioner接口,控制key分到哪个reduce。
- 可选:每一个分片的优先计算位置(preferred locations),比如HDFS的block的所在位置应该是优先计算的位置。
//由子类实现来计算一个给定的Partition
protected def getPartitions: Array[Partition]
//由子类实现,对一个分片进行计算,得出一个可遍历的结果
def compute(split: Partition, context: TaskContext): Iterator[T]
//由子类实现,只计算一次,计算RDD对父RDD的依赖
protected def getDependencies: Seq[Dependency[_]] = deps
//可选的,分区的方法,针对第4点,类似于mapreduce当中的Paritioner接口,控制key分到哪个reduce
@transient val partitioner: Option[Partitioner] = None
//可选的,指定优先位置,输入参数是split分片,输出结果是一组优先的节点位置
protected def getPreferredLocations(split: Partition): Seq[String] = Nil
对应上面几点我们可以在源码中找到这4个方法和1个属性。暂时先不展开,后面会根据具体源码讲解。
在上面的源码中看到了RDD的构造方法:
abstract class RDD[T: ClassTag](
@transient private var _sc: SparkContext,
@transient private var deps: Seq[Dependency[_]]
) extends Serializable with Logging {
if (classOf[RDD[_]].isAssignableFrom(elementClassTag.runtimeClass)) {
// This is a warning instead of an exception in order to avoid breaking user programs that
// might have defined nested RDDs without running jobs with them.
logWarning("Spark does not support nested RDDs (see SPARK-5063)")
}
private def sc: SparkContext = {
if (_sc == null) {
throw new SparkException(
"This RDD lacks a SparkContext. It could happen in the following cases: \n(1) RDD " +
"transformations and actions are NOT invoked by the driver, but inside of other " +
"transformations; for example, rdd1.map(x => rdd2.values.count() * x) is invalid " +
"because the values transformation and count action cannot be performed inside of the " +
"rdd1.map transformation. For more information, see SPARK-5063.\n(2) When a Spark " +
"Streaming job recovers from checkpoint, this exception will be hit if a reference to " +
"an RDD not defined by the streaming job is used in DStream operations. For more " +
"information, See SPARK-13758.")
}
_sc
}
/** Construct an RDD with just a one-to-one dependency on one parent */
def this(@transient oneParent: RDD[_]) =
this(oneParent.context, List(new OneToOneDependency(oneParent)))
这里我们看到RDD在创建时会放入SparkContext和Dependency集合。Dependency包含了当前RDD的父RDD的引用,以及足够从父RDD恢复丢失的partition的信息。
我们看到除了包含SparkContext变量和Dependencies,一个RDD还包含了自己的id和name属性。
/** The SparkContext that created this RDD. */
//创建该RDD的SparkContext
def sparkContext: SparkContext = sc
/** A unique ID for this RDD (within its SparkContext). */
// SparkContext内部的唯一ID
val id: Int = sc.newRddId()
/** A friendly name for this RDD */
//RDD的名字
@transient var name: String = null
/** Assign a name to this RDD */
def setName(_name: String): this.type = {
name = _name
this
}
RDD提供了大量的API供我们使用,从论文中我们可以看到主要分为Transformations和Actions。
RDD Actions
Actions包括count、collect、reduce、lookup和save。除了save方法外,其他四个Action都是将结果直接获取到Driver程序中的操作,由这些操作来启动Spark的计算。
/**
* Return the number of elements in the RDD.
* 返回RDD的元素个数
*/
def count(): Long = sc.runJob(this, Utils.getIteratorSize _).sum
/**
* Return an array that contains all of the elements in this RDD.
*
* @note This method should only be used if the resulting array is expected to be small, as
* all the data is loaded into the driver's memory.
* 返回一个包含所有元素的RDD。我们需要注意Driver端的OOM
*/
def collect(): Array[T] = withScope {
val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
Array.concat(results: _*)
}
/**
* Reduces the elements of this RDD using the specified commutative and
* associative binary operator.
* 使用给定的二元运算符来reduce该RDD
*/
def reduce(f: (T, T) => T): T = withScope {
val cleanF = sc.clean(f)
val reducePartition: Iterator[T] => Option[T] = iter => {
if (iter.hasNext) {
Some(iter.reduceLeft(cleanF))
} else {
None
}
}
var jobResult: Option[T] = None
//合并每个partition的reduce结果
val mergeResult = (index: Int, taskResult: Option[T]) => {
if (taskResult.isDefined) {
jobResult = jobResult match {
case Some(value) => Some(f(value, taskResult.get))
case None => taskResult
}
}
}
sc.runJob(this, reducePartition, mergeResult)
// Get the final result out of our Option, or throw an exception if the RDD was empty
jobResult.getOrElse(throw new UnsupportedOperationException("empty collection"))
}
/**
* Return the list of values in the RDD for key `key`. This operation is done efficiently if the
* RDD has a known partitioner by only searching the partition that the key maps to.
* 根据给定的RDD的key来查找它对应的Seq[value]
* 如果该RDD有给定的Partitioner,该方法会先利用getPartition方法定位Partition再进行搜索,
*/
def lookup(key: K): Seq[V] = self.withScope {
self.partitioner match {
case Some(p) =>
val index = p.getPartition(key)
val process = (it: Iterator[(K, V)]) => {
val buf = new ArrayBuffer[V]
for (pair <- it if pair._1 == key) {
buf += pair._2
}
buf
} : Seq[V]
val res = self.context.runJob(self, process, Array(index))
res(0)
case None =>
self.filter(_._1 == key).map(_._2).collect()
}
}
上面的四个行动算子都直接或间接的调用了SparkContext.runJob方法来获取结果,可见这个方法便是启动Spark计算任务的入口。runJob源码我们放到SparkContext的时候再讲解。
RDD Transformation
map
/**
* Return a new RDD by applying a function to all elements of this RDD.
*/
def map[U: ClassTag](f: T => U): RDD[U] = withScope {
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}
map函数会利用当前RDD以及用户传入的匿名函数构建出一个MapPartitionsRDD。
/**
* An RDD that applies the provided function to every partition of the parent RDD.
*
* @param prev the parent RDD.
* @param f The function used to map a tuple of (TaskContext, partition index, input iterator) to
* an output iterator.
* @param preservesPartitioning Whether the input function preserves the partitioner, which should
* be `false` unless `prev` is a pair RDD and the input function
* doesn't modify the keys.
* @param isOrderSensitive whether or not the function is order-sensitive. If it's order
* sensitive, it may return totally different result when the input order
* is changed. Mostly stateful functions are order-sensitive.
*/
private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag](
var prev: RDD[T],
f: (TaskContext, Int, Iterator[T]) => Iterator[U], // (TaskContext, partition index, iterator)
preservesPartitioning: Boolean = false,
isOrderSensitive: Boolean = false)
extends RDD[U](prev) {
override val partitioner = if (preservesPartitioning) firstParent[T].partitioner else None
override def getPartitions: Array[Partition] = firstParent[T].partitions
override def compute(split: Partition, context: TaskContext): Iterator[U] =
f(context, split.index, firstParent[T].iterator(split, context))
MapPartitionsRDD重写了getPartitions和compute方法,而且都用到了firstParent[T]。getPartitions返回的是firstParent的partition。这里的parent也就是构造函数传入的prev:RDD[T].
我们点进入RDD[U]构造函数
def this(@transient oneParent: RDD[_]) =
this(oneParent.context, List(new OneToOneDependency(oneParent)))
可以发现它把RDD复制给了deps,RDD成为了MapPartitionsRDD的父依赖,这里提到的OneToOneDependency是窄依赖,子RDD直接依赖于父RDD
/** Returns the first parent RDD */
protected[spark] def firstParent[U: ClassTag]: RDD[U] = {
dependencies.head.rdd.asInstanceOf[RDD[U]]
}
因此我们可以说getPartitions直接沿用了父RDD的分片信息。而compute函数是在父RDD遍历每一行数据时套一个匿名函数f进行处理。compute方法套用了构造参数中的方法f。map方法中传入的f是(context,pid,iter)=>iter.map(cleanF)。context是TaskContext,pid是Partition的id,iter是该Partition的iterator。
filter
/**
* Return a new RDD containing only the elements that satisfy a predicate.
*/
def filter(f: T => Boolean): RDD[T] = withScope {
val cleanF = sc.clean(f)
new MapPartitionsRDD[T, T](
this,
(context, pid, iter) => iter.filter(cleanF),
preservesPartitioning = true)
}
原理其实是一样的,区别就是应用到每个Partition iterator的方法不同
flatMap
/**
* Return a new RDD by first applying a function to all elements of this
* RDD, and then flattening the results.
*/
def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] = withScope {
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.flatMap(cleanF))
}
distinct
/**
* Return a new RDD containing the distinct elements in this RDD.
*/
def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
map(x => (x, null)).reduceByKey((x, y) => x, numPartitions).map(_._1)
}
使用了reduceBykey算子实现了distinct
groupBy
/**
* Return an RDD of grouped items. Each group consists of a key and a sequence of elements
* mapping to that key. The ordering of elements within each group is not guaranteed, and
* may even differ each time the resulting RDD is evaluated.
*
* @note This operation may be very expensive. If you are grouping in order to perform an
* aggregation (such as a sum or average) over each key, using `PairRDDFunctions.aggregateByKey`
* or `PairRDDFunctions.reduceByKey` will provide much better performance.
*/
def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])] = withScope {
groupBy[K](f, defaultPartitioner(this))
}
/**
* Return an RDD of grouped elements. Each group consists of a key and a sequence of elements
* mapping to that key. The ordering of elements within each group is not guaranteed, and
* may even differ each time the resulting RDD is evaluated.
*
* @note This operation may be very expensive. If you are grouping in order to perform an
* aggregation (such as a sum or average) over each key, using `PairRDDFunctions.aggregateByKey`
* or `PairRDDFunctions.reduceByKey` will provide much better performance.
*/
def groupBy[K](
f: T => K,
numPartitions: Int)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])] = withScope {
groupBy(f, new HashPartitioner(numPartitions))
}
/**
* Return an RDD of grouped items. Each group consists of a key and a sequence of elements
* mapping to that key. The ordering of elements within each group is not guaranteed, and
* may even differ each time the resulting RDD is evaluated.
*
* @note This operation may be very expensive. If you are grouping in order to perform an
* aggregation (such as a sum or average) over each key, using `PairRDDFunctions.aggregateByKey`
* or `PairRDDFunctions.reduceByKey` will provide much better performance.
*/
def groupBy[K](f: T => K, p: Partitioner)(implicit kt: ClassTag[K], ord: Ordering[K] = null)
: RDD[(K, Iterable[T])] = withScope {
val cleanF = sc.clean(f)
this.map(t => (cleanF(t), t)).groupByKey(p)
}
利用groupByKey。
reduceBykey
reduceBykey源码比较复杂,后面会单开一篇文章单独讲解,包括比较关键的Dependency宽窄依赖的源码也会一同讲解。