一、RDD 是什么
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据抽象。代码中是一个抽象类,它代表一个不可变、可分区、里面的元素可并行计算的集合。
不可变:RDD 一旦创建就把它封装的计算都确定下来了,不能再对其进行改变,只能产生新的 RDD,这样的好处是可以让在任务失败的时候,顺着 RDD 中的血缘推回到失败前的状态,直接重新计算,而不需要重新复制数据。
可分区和并行计算:RDD 内部是进行分区的,这样才可以实现并行计算。
二、RDD 体现了装饰者的设计模式
例:wordCount
代码:
object WordCount {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("wordCount").setMaster("local[*]")
val sc = new SparkContext(conf)
//获取文件
val text: RDD[String] = sc.textFile("spark-review/input/wordcount.txt")
//切割
val word: RDD[String] = text.flatMap(line => line.split(" "))
//映射成 (word, one)
val word_one: RDD[(String, Int)] = word.map(word => (word, 1))
//统计每个 word 的个数
val word_count: RDD[(String, Int)] = word_one.reduceByKey(_ + _)
//输出
word_count.foreach(x => println(x._1, x._2))
}
}
图解:可以看到每个 RDD 的内部包含了它的父 RDD,这就是 RDD 的依赖
二、RDD 的属性
-
一组分区,即数据集的基本组成单位,分区可以提高并行度;
-
一个计算每个分区的函数;
-
RDD 之间的依赖关系,依赖关系记录下来了 RDD 的血缘;
依赖可以分为宽依赖和窄依赖,宽依赖是指下游 RDD 的分区是从上游父 RDD 多个分区中计算得到的(一对多),窄依赖是指下游每个 RDD 的分区值对应上游父 RDD 的一个分区(一对一)。
-
一个 Partitioner,即 RDD 的分区函数;
-
一个存储每个 Partition 优先位置的列表,把计算发送到距离数据最近的地方可以提高效率。
优先位置:Executor > 节点本地化 > 机架本地化
三、RDD 的创建
3.1 从集合中创建
两个函数:makeRDD() 和
源码:
def parallelize[T: ClassTag]( //从集合中创建
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
assertNotStopped()
new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]())
}
def makeRDD[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
parallelize(seq, numSlices) //底层调用的还是 parallelize()
}
源码中的 numSlices 参数代表了分区个数,默认值如下定义:
override def defaultParallelism(): Int = {
//可以看到,分区个数与 CPU 核数有关
conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))
}
也可以手动设定 numSlices 的值,例:
object CreateRDD {
def main(args: Array[String]): Unit = {
val conf: SparkConf =new SparkConf()
.setMaster("local[*]")
.setAppName("createRDD")
val sc: SparkContext = new SparkContext(conf)
val listRDD1: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 8)
listRDD1.saveAsTextFile("output") //保存了 8 个文件
}
}
3.2 按照指定步长创建 RDD
3.2 从外部文件中创建
源码:
def textFile(
path: String,
//注意这里是 minPartition,所以有可能分区的结果会大于这个数
minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
assertNotStopped()
//这里用的是 hadoop 的读取文件规则
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
}
// defaultMinPartitions 源码,默认值与 2 取较小者
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
// defaultParallelism 源码
override def defaultParallelism(): Int = {
//可以看到,分区个数与 CPU 核数有关
conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))
}
例:
val textRDD: RDD[String] = sc.textFile("spark-review/input/file.txt", 2) //file.txt: 123456(只有这 6 个数字)
textRDD.saveAsTextFile("output") //结果存储了两个文件
但是注意,这是从外部文件创建的 RDD,所以必然会经过 hdfs,这里就会采用 hdfs 的分片规则,这就导致了最后的分区个数不是在函数里指定多少就是多少,而是与 hdfs 的分片规则有关:
//hadoop分片规则核心源码
//分片期望值大小 = 总的文件大小/分片数量,这里的 numSplits 对应的就是 minPartition
long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
//最小分片数量,默认为 1
long minSize = Math.max(job.getLong("mapred.min.split.size", 1), minSplitSize);
//默认为 128M,与磁盘速度有关
long blockSize = file.getBlockSize();
//totalSize / splitSize 即为分区个数,如 5 / 2 = 2...1 ,所以分为 2,2,1,即三个分区
long splitSize = computeSplitSize(goalSize, minSize, blockSize);
//分片核心函数
protected long computeSplitSize(long goalSize, long minSize, long blockSize) {
//分片大小的决定性公式:max(最小分片大小,min(期望分片大小,块的大小))
//如果文件大小小于 blockSize,就会返回 goalSize
return Math.max(minSize, Math.min(goalSize, blockSize));
}
所以就有了下面的例子:
val textRDD: RDD[String] = sc.textFile("spark-review/input/file.txt") //file.txt: 12345(只有这 6 个数字)
textRDD.saveAsTextFile("output") //结果存储了三个文件
3.3 range 函数
val rangeRDD: RDD[Long] = sc.range(1, 100, 4, 1)
rangeRDD.saveAsTextFile("output")//1, 5, 9, 13, ...