之前对RDD有一个大体概念了解,这一节我们需要深入了解它,了解在源码中到底是什么,
怎么产生的,给个rdd之间怎么依赖的,最后又怎么生成spark job
我们还是先从一个例子下手:
val sc = new SparkContext(sparkConf)
val res = sc
.textFile(args(0))
.flatMap(line => line.split(" "))
.map(w => (w, 1))
.reduceByKey(_ + _)
.collect()
res.foreach(tuple => println(tuple._1 + " => " + tuple._2))
RDD如何生成
SparkContext是spark方法的主要的入口,代表着与spark集群建立连接了,它被用来创建RDDs,accumulators(累加器),
broadcast var(广播变量),每个JVM只容许一个SPARK-2243,
这里我们只讨论与RDDs相关方法:
方法 | 功能 | 生成RDD |
---|---|---|
textFile | 从hadoop所支持的文件系统上读取文件 | HadoopRDD |
parallelize | 把本地的数据集合分发行程RDD,懒加载 | ParallelCollectionRDD |
range | 原理同上,只是做了一些分装 | ParallelCollectionRDD => MapPartitionsRDD |
makeRDD | 同parallelize | 生成ParallelCollectionRDD |
wholeTextFiles | 从hadoop所支持的文件系统上读取文件目录下所有文件 | WholeTextFileRDD |
binaryFiles | 读二进制文件 | BinaryFileRDD |
通过对SparkContext的部分方法解释,我们能看出最开始的RDD怎么行程的,我先针对HadoopRDD看都有哪些内部成员:
`getDependencies: 获取依赖RDDs,我们发现是Nil很容易理解,第一个RDD,之前没有依赖
sc SparkContext
: spark上下文inputFormatClass<:InputFormat
: hadoop中InputFormat负责生成InputSplit(文件块),实现RecordReader,读数据,
我们也看出spark与hadoop的内在联系了getPartitions
: 通过inputFormat.getSplits得到文件块,然后包装成HadoopPartition,至此得到了该RDD的分区数了compute
: 先通过inputFormat.getRecordReader
获取RecordReader,分装成自己的NextIterator
,该迭代器最主要的
方法hasNext,next,细节我们就略掉;最后与SparkContext一起分装成InterruptibleIterator
我们大概了解此RDD主要功能,借助hadoop inputFormat的获取文件块/对文件块读取,然后分装成自己的格式比如:
HadoopPartition,InterruptibleIterator
再看一个ParallelCollectionRDD,该RDD主要对本地数据集合分发到集群上执行:
- getPartitions
: ParallelCollectionRDD.slice
把数据集根据numSlices
分片数量切分成一组一组数据,
然后用ParallelCollectionPartition
分装
- compute
: 把Seq集合与sparkContext分装到InterruptibleIterator
至此我们基本有一个大致的感官,这些初始RDD基本通过getPartitions分好区,再通过compute
把
partition包装成InterruptibleIterator
,需要注意的是此时并没有真实的处理数据,只是做准备工作,但是next
是
可以直接获取该分区下的下一条数据.
接着上面的例子rdd.flatMap
生成MapPartitionsRDD(pre,f)
前一个RDD,compute中使用的函数(spark到处都是函数式编程):
- getPartitions
: 前一个RDD的partition
- compute
: 执行成员变量f,上面的例子中line => line.split(" ")
空格分割返回数组,这是一个表达式,
iter.flapMap(f)
是这个RDD主要功能,写一个直观的小例子:
val res = Seq(
"Return a new RDD by applying",
"a function to all elements of this RDD"
).flatMap(line=>line.split(" "))
println(res)
//执行结果如下:
//List(Return, a, new, RDD, by, applying, a, function, to, all, elements, of, this, RDD)
iterator
: 这是RDD内部公用的不可变方法,先从cache中读取,如果没有再去计算,不能被直接调用,用户可以在自定义RDD中调用
MapPartitionsRDD.map(w => (w, 1))
生成MapPartitionsRDD,f
是的主要逻辑是iter.map(cleanF)
,转换成键值对,
mapPartitionsRDD.reduceByKey(_ + _)
,有同学发现RDD中没有这个方法,而PairRDDFunctions却有,那么就引入一个问题:
PairRDDFunctions与RDD关系
PairRDDFunctions
构造函数中接受的是RDD[(K, V)
这种格式,我们可以推断出mapPartitionsRDD.reduceByKey(_ + _)
真实执行逻辑new PairRDDFunctions(mapPartitionsRDD).reduceByKey(_ + _)
,这其实涉及到了scala中隐式转换,
而我们确实在object RDD
中发现了implicit def rddToPairRDDFunctions[K, V](rdd: RDD[(K, V)])
,说白了就是你定义好
隐式转换的方法,编译器在编译过程中帮你自动根据类型与转换转换方法自动转换,这样代码写上去行云流水很顺畅:
我们写一个小例子:
class MyRDD {
def map(): Unit = {
println("this map")
}
override def toString = s"MyRDD()"
}
object MyRDD {
implicit def rddToPairRDDFunctions(rdd: MyRDD): MyPairRDDFunctions = {
new MyPairRDDFunctions(rdd)
}
}
class MyShuffledRDD(prev: MyRDD) extends MyRDD {
override def toString = s"MyShuffledRDD()"
}
class MyPairRDDFunctions(self: MyRDD) {
def reduceByKey(): MyShuffledRDD = {
println("this reduceByKey")
new MyShuffledRDD(self)
}
}
def main(args: Array[String]): Unit = {
println(new MyRDD().reduceByKey())
}
执行结果为:
this reduceByKey
MyShuffledRDD()
如果理解了上面这块代码,那么也就清楚了RDD,PairRDDFunctions之间的内在联系,
下面继续往下跟代码reduceByKey
,最终跟到此方法combineByKeyWithClassTag
,发现最终返回ShuffledRDD
,
根据上面对RDD分析我们基本可以确定只要分析以下方法:
- getPartitions
: 根据Partitioner.numPartitions
(这里其实就是文件块数量),然后返回Array[ShuffledRDDPartition]
- getDependencies
: 返回ShuffleDependency(Dependency TODO 需要详谈)
- compute
: 次方法是在Executor中的执行ShuffleMapTask的时候被执行,主要逻辑是combineCombinersByKey
,然后落地到内存或者文件
这里说的很笼统,细节我们后面专门写一节详谈(TODO)
这个例子最后collect
,RDD提交j