Spark RDD的基本概念

1. 什么是RDD

RDD的全称为Resilient Distributed Dataset,是⼀个弹性、可复原的分布式数据集,是Spark中最基本的抽象,是⼀个不可变的、有多个分区的、可以并⾏计算的集合。
RDD中并不装真正要计算的数据,⽽装的是描述信息,描述以后从哪⾥读取数据,调⽤了⽤什么⽅法,传⼊了什么函数,以及依赖关系等。

resilient的含义:

  1. 有弹性(或弹力)的;
  2. 可迅速恢复的; 有适应力的; 能复原的;

所以,RDD本质更像是Scala中的迭代器

2. RDD的五大属性

  1. 有⼀些列连续的分区(分区列表):

分区编号从0开始,分区的数量决定了对应阶段Task的并⾏度。对于RDD 来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD 时指定RDD 的分片个数,如果没有指定,那么就会采用默认值。默认值就是程序所分配到的CPU Core的数目。
protected def getPartitions: Array[Partition]

  1. 有⼀个函数作⽤在每个输⼊切⽚上(compute计算函数):

每⼀个分区都会⽣成⼀个Task,对该分区的数据进⾏计算,这个函数就是具体的计算逻辑。
Spark 中RDD 的计算是以分片为单位的,每个RDD都会实现compute 函数以达到这个目的。ompute 函数会对迭代器进行复合,不需要保存每次计算的结果。
def compute(split: Partition, context: TaskContext): Iterator[T]

  1. RDD之间的⼀系列依赖关系:

RDD调⽤Transformation后会⽣成⼀个新的RDD,⼦RDD会记录⽗RDD的依赖关系,包括宽依赖(有shuffle)和窄依赖(没有shuffle)。
在部分分区数据丢失时,Spark 可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD 的所有分区进行重新计算。
protected def getDependencies: Seq[Dependency[_]] = deps

  1. (可选的)K-V的RDD在Shuffle会有分区器

当前Spark 中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于于key-value 的RDD , 才会有Partitioner, 非key-value 的RDD 的Parititioner 的值是None。Partitioner 函数不但决定了RDD 本身的分片数量, 也决定了parent RDD Shuffle 输出时的分片数量。
默认使⽤HashPartitioner

  1. 处理 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

  1. (可选的)每个分区的首选计算执行位置:

对于一个HDFS 文件来说,这个列表保存的就是每个Partition 所在的块的位置。按照“移动数据不如移动计算”的理念,Spark 在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。
protected def getPreferredLocations(split: Partition): Seq[String] = Nil
RDD 是一个应用层面的逻辑概念。一个RDD 多个分片。RDD 就是一个元数据记录集,记录了RDD 内存所有的关系数据

image.png

3. RDD的算⼦分类

  1. **Transformation:**即转换算⼦,调⽤转换算⼦会⽣成⼀个新的RDD,Transformation是Lazy的,不会触发job执⾏。
  2. **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()
  }
}

断点执行

第一次断点

image.png

第二次断点

image.png

第三次断点

image.png

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),点击运行无运行结果。

image.png

第二次:
放开注释,点击运行

image.png

5.1.1.3 解释

有此可见,调用scala中的迭代器的方法(map、filter、flatmap)时,并不会立刻执行。
直到调用Iteratorforeach()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())
  }
  1. 迭代器要真正执行运算,需要调用hasNext判断是否还是数据,然后调用next返回下条数据
  2. 而scala的map中并没有调用hasNext也没有调用next
  3. map算子只是返回一个新的迭代器:
    1. **hasNext()调用父迭代器的hasNext()**方法
    2. **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之间的依赖关系一般分为两种,宽依赖和窄依赖。
image.png

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 实际情况

image.png
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)

image.png

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
image.png

因此,当使用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对应的逻辑图
image.png

进入
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)
}
  1. 分区号有两个因素决定:key(的hashcode值)和分区数
  2. key相同,分区号一定相同;
  3. 一个分区号可能会有不同的key;
  4. 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)

image.png
DAG图
image.png
由此可见,reduceByKey在map(stage 0)端进行了预聚合。

未完待续…

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值