1. 什么是RDD
RDD的全称为Resilient Distributed Dataset,是⼀个弹性、可复原的分布式数据集,是Spark中最基本的抽象,是⼀个不可变的、有多个分区的、可以并⾏计算的集合。
RDD中并不装真正要计算的数据,⽽装的是描述信息,描述以后从哪⾥读取数据,调⽤了⽤什么⽅法,传⼊了什么函数,以及依赖关系等。
resilient的含义:
- 有弹性(或弹力)的;
- 可迅速恢复的; 有适应力的; 能复原的;
所以,RDD本质更像是Scala中的迭代器
2. RDD的五大属性
- 有⼀些列连续的分区(分区列表):
分区编号从0开始,分区的数量决定了对应阶段Task的并⾏度。对于RDD 来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD 时指定RDD 的分片个数,如果没有指定,那么就会采用默认值。默认值就是程序所分配到的CPU Core的数目。
protected def getPartitions: Array[Partition]
- 有⼀个函数作⽤在每个输⼊切⽚上(compute计算函数):
每⼀个分区都会⽣成⼀个Task,对该分区的数据进⾏计算,这个函数就是具体的计算逻辑。
Spark 中RDD 的计算是以分片为单位的,每个RDD都会实现compute 函数以达到这个目的。ompute 函数会对迭代器进行复合,不需要保存每次计算的结果。
def compute(split: Partition, context: TaskContext): Iterator[T]
- RDD之间的⼀系列依赖关系:
RDD调⽤Transformation后会⽣成⼀个新的RDD,⼦RDD会记录⽗RDD的依赖关系,包括宽依赖(有shuffle)和窄依赖(没有shuffle)。
在部分分区数据丢失时,Spark 可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD 的所有分区进行重新计算。
protected def getDependencies: Seq[Dependency[_]] = deps
- (可选的)K-V的RDD在Shuffle会有分区器:
当前Spark 中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于于key-value 的RDD , 才会有Partitioner, 非key-value 的RDD 的Parititioner 的值是None。Partitioner 函数不但决定了RDD 本身的分片数量, 也决定了parent RDD Shuffle 输出时的分片数量。
默认使⽤HashPartitioner
- 处理 HDFS 上的文件时,会按文件及偏移量范围划分 partition,通常一个 hdfs 的 block 块对应一个 partition,比如:
PARTITION_0:/inputdata/aaa.txt ,0~128M
PARTITION_1:/inputdata/aaa.txt,128M~200M
2. 处理 MySQL 等数据库中的数据时,会按照用户指定的某个字段的取值范围和指定的分区数进行 partition 划分,比如:
PARTITION_0: from xdb.table_a where id < 100000
PARTITION_1: from xdb.table_a where id >=100000 and id<200000
PARTITION_2: from xdb.table_a where id >=200000
val partitioner: Option[Partitioner] = None
- (可选的)每个分区的首选计算执行位置:
对于一个HDFS 文件来说,这个列表保存的就是每个Partition 所在的块的位置。按照“移动数据不如移动计算”的理念,Spark 在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。
protected def getPreferredLocations(split: Partition): Seq[String] = Nil
RDD 是一个应用层面的逻辑概念。一个RDD 多个分片。RDD 就是一个元数据记录集,记录了RDD 内存所有的关系数据
3. RDD的算⼦分类
- **Transformation:**即转换算⼦,调⽤转换算⼦会⽣成⼀个新的RDD,Transformation是Lazy的,不会触发job执⾏。
- **Action:**⾏动算⼦,调⽤⾏动算⼦会触发job执⾏,本质上是调⽤了sc.runJob⽅法,该⽅法从最后⼀个RDD,根据其依赖关系,从后往前,划分Stage,⽣成TaskSet。
4. 创建RDD的⽅法
4.1 从文件系统
4.1.1 本地文件系统
val rdd = sc.textFile("/tmp/student.txt", 2)
rdd.foreach(println)
4.1.2 HDFS文件系统
val rdd = sc.textFile("hdfs:///tmp/student.txt", 2)
rdd.foreach(println)
4.2 通过并行化方式
val rdd: RDD[Int] = sc.parallelize(Array(1,2,3,4,5,6,7,8,9))
5. RDD的转换
RDD中的所有转换都是情性的,也就是说,它们并不会直接计算结果。相反的,它们只是记住这些应用到基础数据集(例如一个文件)上的转换动作。只有当发生一个要求返回结果给Driver的动作时,这些转换才会真正运行。这个设计让Spark更加有效率地运行。例如我们可以实现:通过map创建的一个新数据集,并在reduce 中使用,最终只返回reduce的结果给Driver,而不是整个大的新数据集。
5.1 RDD的本质
5.1.1 scala迭代器案例
5.1.1.1 案例01
object IteratorDemo {
def main(args: Array[String]): Unit = {
val source = Source.fromFile("data/wordcount/input/wc.txt")
val lines: Iterator[String] = source.getLines()
val filtered: Iterator[String] = lines.filter(line => {
!line.startsWith("aaa")
})
val upper: Iterator[String] = filtered.map(line => {
line.toUpperCase()
})
while (upper.hasNext) {
val upperWord = upper.next()
println(upperWord)
}
source.close()
}
}
断点执行
第一次断点
第二次断点
第三次断点
5.1.1.2 案例02
object IteratorDemo2 {
def main(args: Array[String]): Unit = {
val iterator = List(1, 2).iterator
val iter1 = iterator.map(it => {
println("第一次map执行:")
it * 10
})
val iter2 = iter1.map(it => {
println("第二次map执行:")
it * 1000
})
iter2.foreach(println)
}
}
第一次:
注释掉iter2.foreach(println)
,点击运行无运行结果。
第二次:
放开注释,点击运行
5.1.1.3 解释
有此可见,调用scala中的迭代器的方法(map、filter、flatmap)时,并不会立刻执行。
直到调用Iterator的foreach()、next()方法,才会真正的触发迭代器执行,这就是所谓的lazy特性。
这就类似RDD中的转换算子和行动算子。
5.1.1.4 scala中迭代器的lazy原理
map源码
def map[B](f: A => B): Iterator[B] = new AbstractIterator[B] {
def hasNext = self.hasNext
def next() = f(self.next())
}
- 迭代器要真正执行运算,需要调用hasNext判断是否还是数据,然后调用next返回下条数据
- 而scala的map中并没有调用hasNext也没有调用next
- map算子只是返回一个新的迭代器:
- **hasNext()调用父迭代器的hasNext()**方法
- **next()在父迭代器的next()**的返回结果上,应用了map算子传入的函数f
5.1.2 RDD的lazy原理
Rdd 转换算子的 lazy 原理,与scala 的迭代器算子的 lazy 原理很类似。
**Scala 迭代器算子:**map/fatMap 等,方法的内部实现只是创建了一个新的迭代器返回(并没有去调用迭代器的 hasNext 和next 进行迭代计算)
**Rdd 的转换算子:**map/fatMap 等,方法的内部实现只是创建了一个新的 RDD返回(并没有去调用RDD 内部的迭代器的hasNext 和 next进行迭代计算)
5.2 RDD的依赖关系
RDD之间的依赖关系一般分为两种,宽依赖和窄依赖。
5.2.1 窄依赖
/**
* Base class for dependencies where each partition of the parent RDD is used by at most one
* partition of the child RDD. Narrow dependencies allow for pipelined execution.
*/
abstract class NarrowDependency[T](rdd: RDD[T]) extends Dependency(rdd) {
/**
* Get the parent partitions for a child partition.
* @param partitionId a partition of the child RDD
* @return the partitions of the parent RDD that the child partition depends upon
*/
def getParents(partitionId: Int): Seq[Int]
}
如果新生成的child RDD中每个分区都依赖parent RDD中的一部分分区,那么这个分区依赖关系被称为NarrowDependency。
5.2.2 宽依赖
/**
* Represents a dependency on the output of a shuffle stage.
* @param rdd the parent RDD
* @param partitioner partitioner used to partition the shuffle output
* @param serializerClass class name of the serializer to use
*/
class ShuffleDependency[K, V](
@transient rdd: RDD[_ <: Product2[K, V]],
val partitioner: Partitioner,
val serializerClass: String = null)
extends Dependency(rdd.asInstanceOf[RDD[Product2[K, V]]]) {
val shuffleId: Int = rdd.context.newShuffleId()
}
宽依赖表示新生成的child RDD中的分区依赖parent RDD中的每个分区的一部分。
5.2.3 实际情况
NarrowDependency:
窄依赖有如上图几种形式:OneToOne,Range,ManyToMany。
按照通常的定义,其中左下角的ManyToMany这种形式被定义为宽依赖。但参考Spark算子:cartesian。
cartesian算子逻辑就是ManyToMany的数据流转方式,但是参考cartesian算子的源码(待后面再具体分析),该算子是一个窄依赖。
ShuffleDependency:
宽依赖通常都是ManyToMany这种形式。但与窄依赖中ManyToMany不同的是,窄依赖中子RDD会依赖父RDD的整个分区的所有数据,而宽依赖中子RDD只会依赖父RDD分区中的一部分数据。
6. RDD的算子
常见算子参考:
https://spark.apache.org/docs/latest/rdd-programming-guide.html
6.1 keys & values
6.1.1 使用
val sc = new SparkContext(conf)
val lst: Seq[(String, Int)] = List(
("spark", 1), ("hadoop", 1), ("hive", 1), ("spark", 1),
("spark", 1), ("flink", 1), ("hbase", 1), ("spark", 1),
("kafka", 1), ("kafka", 1), ("kafka", 1), ("kafka", 1),
("hadoop", 1), ("flink", 1), ("hive", 1), ("flink", 1)
)
val wordAndOne: RDD[(String, Int)] = sc.parallelize(lst, 4)
// 获取keys
val keyRDD1: RDD[String] = wordAndOne.keys
// 使用map实现同样的效果
val keyRDD2: RDD[String] = wordAndOne.map(_._1)
val values1: Array[String] = keyRDD1.collect()
val values2: Array[String] = keyRDD2.collect()
println(values1.toBuffer)
println(values2.toBuffer)
6.1.2 深入理解
进入wordAndOne.keys
PairRDDFunctions.scala
class PairRDDFunctions[K, V](self: RDD[(K, V)])
(implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null)
extends Logging with Serializable {
// 省略若干方法
def keys: RDD[K] = self.map(_._1)
}
进入RDD类
RDD.scala
因此,当使用PairRDDFunctions的构造方法来调用keys方法:
val wordAndOne: RDD[(String, Int)] = sc.parallelize(lst, 4)
//不使用隐式转换
val pairRDDFunc = new PairRDDFunctions[String, Int](wordAndOne)
val keyRDD = pairRDDFunc.keys
由此可见,keys和values本是PairRDDFunctions的方法,此处涉及隐式转换,将rdd.keys
转化为调用rdd.map(_._1)
。
此类算子出了keys,还有:values、mapValues、flatMapValues等
6.2 ReduceByKey
6.2.1 使用
val conf = new SparkConf()
.setAppName("WordCount")
.setMaster("local[*]") //本地模式,开多个线程
//1.创建SparkContext
val sc = new SparkContext(conf)
val lst: Seq[(String, Int)] = List(
("spark", 1), ("hadoop", 1), ("hive", 1), ("spark", 1),
("spark", 1), ("flink", 1), ("hbase", 1), ("spark", 1),
("kafka", 1), ("kafka", 1), ("kafka", 1), ("kafka", 1),
("hadoop", 1), ("flink", 1), ("hive", 1), ("flink", 1)
)
//通过并行化的方式创建RDD,分区数量为4
val wordAndOne: RDD[(String, Int)] = sc.parallelize(lst, 4)
val reduced: RDD[(String, Int)] = wordAndOne.reduceByKey(_ + _)
6.2.2 分区理解
ReduceByKey对应的逻辑图
进入
wordAndOne.reduceByKey(_ + _)
def reduceByKey(func: (V, V) => V): RDD[(K, V)] = self.withScope {
reduceByKey(defaultPartitioner(self), func)
}
进入
defaultPartitioner
def defaultPartitioner(rdd: RDD[_], others: RDD[_]*): Partitioner = {
val rdds = (Seq(rdd) ++ others)
val hasPartitioner = rdds.filter(_.partitioner.exists(_.numPartitions > 0))
val hasMaxPartitioner: Option[RDD[_]] = if (hasPartitioner.nonEmpty) {
Some(hasPartitioner.maxBy(_.partitions.length))
} else {
None
}
val defaultNumPartitions = if (rdd.context.conf.contains("spark.default.parallelism")) {
rdd.context.defaultParallelism
} else {
rdds.map(_.partitions.length).max
}
// If the existing max partitioner is an eligible one, or its partitions number is larger
// than or equal to the default number of partitions, use the existing partitioner.
if (hasMaxPartitioner.nonEmpty && (isEligiblePartitioner(hasMaxPartitioner.get, rdds) ||
defaultNumPartitions <= hasMaxPartitioner.get.getNumPartitions)) {
hasMaxPartitioner.get.partitioner.get
} else {
new HashPartitioner(defaultNumPartitions)
}
}
class HashPartitioner(partitions: Int) extends Partitioner {
require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")
def numPartitions: Int = partitions
def getPartition(key: Any): Int = key match {
case null => 0
case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
}
override def equals(other: Any): Boolean = other match {
case h: HashPartitioner =>
h.numPartitions == numPartitions
case _ =>
false
}
override def hashCode: Int = numPartitions
}
该种情况会使用HashPartitioner。并且,在key位null的时候,会发生数据倾斜。
具体什么什么情况使用什么Partitioner在后续分区和分区器的文章中再具体介绍。
进入
Utils.nonNegativeMod(key.hashCode, numPartitions)
def nonNegativeMod(x: Int, mod: Int): Int = {
val rawMod = x % mod
rawMod + (if (rawMod < 0) mod else 0)
}
- 分区号有两个因素决定:key(的hashcode值)和分区数
- key相同,分区号一定相同;
- 一个分区号可能会有不同的key;
- key.hashcode可能是负数,但分区号一定是非负的;
6.2.3 深入理解
在reduceByKey的底层调用的是combineByKeyWithClassTag
def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = self.withScope {
combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
}
调用combineBykey
val lst: Seq[(String, Int)] = List(
("spark", 1), ("hadoop", 1), ("hive", 1), ("spark", 1),
("spark", 1), ("flink", 1), ("hbase", 1), ("spark", 1),
("kafka", 1), ("kafka", 1), ("kafka", 1), ("kafka", 1),
("hadoop", 1), ("flink", 1), ("hive", 1), ("flink", 1)
)
val wordAndOne = sc.parallelize(lst, 4)
//调用combineByKey实现与reduceByKey同样的功能
//前两个函数是在上游执行的
//在每个分区中,key第一次出现,将value进行相应的操作
val f1 = (x: Int) => {
val stage = TaskContext.get().stageId()
val partition = TaskContext.getPartitionId()
println(s"f1 function invoked in state: $stage, partition: $partition")
x
}
//在每个分区内,将key相同的value进行局部聚合操作
val f2 = (a: Int, b: Int) => {
val stage = TaskContext.get().stageId()
val partition = TaskContext.getPartitionId()
println(s"f2 function invoked in state: $stage, partition: $partition")
a + b
}
//第三个函数是在下游完成的
val f3 = (m: Int, n: Int) => {
val stage = TaskContext.get().stageId()
val partition = TaskContext.getPartitionId()
println(s"f3 function invoked in state: $stage, partition: $partition")
m + n
}
val reduced: RDD[(String, Int)] = wordAndOne.combineByKey(f1, f2, f3)
DAG图
由此可见,reduceByKey在map(stage 0)端进行了预聚合。
未完待续…