Day68_Spark(三)Spark RDD的分区与依赖关系

课程大纲

课程内容

学习效果

掌握目标

RDD数据分区

分区策略

掌握

自定义分区

掌握

RDD依赖关系

依赖关系

掌握

DAG有向无环图

掌握

RDD任务划分

掌握

一、RDD分区

RDD,Resiliennt Distributed Datasets,弹性式分布式数据集,是由若干个分区构成的,那么这每一个分区中的数据又是如何产生的呢?这就是RDD分区策略所要解决的问题,下面我们就一道来学习RDD分区相关。

(一)、RDD数据分区

Spark目前支持Hash分区和Range分区,用户也可以自定义分区,Hash分区为当前的默认分区,Spark中分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle过程属于哪个分区和Reduce的个数。

分区的决定,就是在宽依赖的过程中才有,窄依赖因为是一对一,分区确定的,所以不需要指定分区操作。

1、Partitioner

在Spark中涉及RDD的分区策略的抽象类为Partitioner,其继承体系如下图3-1所示,有两个核心的子类实现,一个HashPartitioner,一个RangePartitioner。

Spark中数据分区的主要工具类(数据分区类),主要用于Spark底层RDD的数据重分布的情况中,主要方法两个,如下图3-2所示:

 

2、HashPartitioner

Spark中非常重要的一个分区器,也是默认分区器,默认用于90%以上的RDD相关API上。

功能:依据RDD中key值的hashCode的值将数据取模后得到该key值对应的下一个RDD的分区id值,支持key值为null的情况,当key为null的时候,返回0;该分区器基本上适合所有RDD数据类型的数据进行分区操作;但是需要注意的是,由于JAVA中数组的hashCode是基于数组对象本身的,不是基于数组内容的,所以如果RDD的key是数组类型,那么可能导致数据内容一致的数据key没法分配到同一个RDD分区中,这个时候最好自定义数据分区器,采用数组内容进行分区或者将数组的内容转换为集合。HashPartitioner代码如下所示:

def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setAppName("demo").setMaster("local[*]")
    val sc = new SparkContext(conf)
    sc.setLogLevel("WARN")
    //加载数据
    val rdd = sc.parallelize(List((1,3),(1,2),(2,4),(2,3),(3,6),(3,8)),8)
    //通过Hash分区
    val result: RDD[(Int, Int)] = rdd.partitionBy(new org.apache.spark.HashPartitioner(2))
    //获取分区方式
    println(result.partitioner)
    //获取分区数
    println(result.getNumPartitions)
}

 HashPartitioner

3、RangePartitioner

SparkCore中除了HashPartitioner分区器外,另外一个比较重要的已经实现的分区器,主要用于RDD的数据排序相关API中,比如sortByKey底层使用的数据分区器就是RangePartitioner分区器;该分区器的实现方式主要是通过两个步骤来实现的,第一步:先从整个RDD中抽取出样本数据,将样本数据排序,计算出每个分区的最大key值,形成一个Array[KEY]类型的数组变量rangeBounds;第二步:判断key在rangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标;该分区器要求RDD中的KEY类型必须是可以排序的。

注意:

将一定范围内的数映射到某一个分区内,在实现中,分界(rangeBounds)的算法尤为重要。用到了水塘抽样算法,其实RangePartitioner的重点是在于构建rangeBounds数组对象,主要步骤是:

1. 如果分区数量小于2或者rdd中不存在数据的情况下,直接返回一个空的数组,不需要计算range的边界;如果分区数据大于1的情况下,而且rdd中有数据的情况下,才需要计算数组对象

2. 计算总体的数据抽样大小sampleSize,计算规则是:至少每个分区抽取20个数据或者最多1M的数据

3. 根据sampleSize和分区数量计算每个分区的数据抽样样本数量sampleSizePrePartition

4. 调用RangePartitioner的sketch函数进行数据抽样,计算出每个分区的样本

5. 计算样本的整体占比以及数据量过多的数据分区,防止数据倾斜

6. 对于数据量比较多的RDD分区调用RDD的sample函数API重新进行数据抽取

7. 将最终的样本数据通过RangePartitoner的determineBounds函数进行数据排序分配,计算出rangeBounds。

4、总结

一般使用默认的HashPartitioner即可,RangePartitioner的使用有一定的局限性。

(二)、RDD自定义分区

我们都知道Spark内部提供了HashPartitioner和RangePartitioner两种分区策略,这两种分区策略在很多情况下都适合我们的场景。但是有些情况下,Spark内部不能符合我们的需求,这时候我们就可以自定义分区策略。

要实现自定义的分区器,你需要继承 org.apache.spark.Partitioner 类并实现下面三个方法。

numPartitions: Int:返回创建出来的分区数。

getPartition(key: Any): Int:返回给定键的分区编号(0到numPartitions-1)。

equals():Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个 RDD 的分区方式是否相同。

1、案例一:模拟实现HashPartitioner

class CustomerPartitoner(numPartiton:Int) extends Partitioner{

  // 返回分区的总数

  override def numPartitions: Int = numPartiton

  // 根据传入的Key返回分区的索引

  override def getPartition(key: Any): Int = {

    key.toString.toInt % numPartiton

  }

}

object CustomerPartitoner {

  def main(args: Array[String]): Unit = {

val sparkConf = new SparkConf()

.setAppName("CustomerPartitoner").setMaster("local[*]")

    val sc = new SparkContext(sparkConf)

    //zipWithIndex该函数将RDD中的元素和这个元素在RDD中的ID(索引号)组合成键/值对。

    val rdd = sc.parallelize(0 to 10,1).zipWithIndex()

    val func = (index:Int,iter:Iterator[(Int,Long)]) =>{

      iter.map(x => "[partID:"+index + ", value:"+x+"]")

    }

    val r = rdd.mapPartitionsWithIndex(func).collect()

    for (i <- r){

      println(i)

    }

    val rdd2 = rdd.partitionBy(new CustomerPartitoner(5))

    val r1 = rdd2.mapPartitionsWithIndex(func).collect()

    println("----------------------------------------")

    for (i <- r1){

      println(i)

    }

    println("----------------------------------------")

    sc.stop()

  }

}   

2、总结

  • 分区主要面对KV结构数据,Spark内部提供了两个比较重要的分区器,Hash分区器和Range分区器
  • hash分区主要通过key的hashcode来对分区数求余,hash分区可能会导致数据倾斜问题,Range分区是通过水塘抽样的算法来将数据均匀的分配到各个分区中
  • 自定义分区主要通过继承partitioner抽象类来实现,必须要实现两个方法:numPartitions 和 getPartition(key: Any)

二、RDD依赖关系

   RDD和它依赖的父RDD的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。

(一)、依赖关系

1、宽窄依赖

所谓窄依赖,指的是子RDD一个分区中的数据,来自于上游RDD中一个分区。

所谓宽依赖,指的是子RDD一个分区中的数据,来自于上游RDD所有的分区。

宽窄依赖关系示例如下图3-5所示。

2、血统Lineage

RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(即血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。关于linage说明示意图如下图3-6所示。

3、DAG有向无环图

如果一个有向图无法从某个顶点出发经过若干条边回到该点,则这个图是一个有向无环图。

有向图中一个点经过两种路线到达另一个点未必形成环,因此有向无环图未必能转化成树,但任何有向树均为有向无环图。

通俗的来说就是有方向,没有回流的图可以称为有向无环图,示意图入3-7所示。

3、RDD任务的切分

对于RDD的任务切分,可以很形象的如下图3-9所示,

并行度:程序同一时间执行作业的线程个数。

原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage,如下图3-10所示。

 对于窄依赖,partition的转换处理在Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据。Stage阶段计算过程如下图所示3-11所示。

Spark中的Stage分为了2类,一类对应窄依赖操作,称之为ShuffleMapStage,其对应的partition称之为ShuffleMapPartition,对应的任务task称之为ShuffleMapTask;一类对应宽依赖操作,称之为ResultStage,对应的partition称之为ResultPartition,对应的任务task称之为ResultTask。

ShuffleMapStage阶段的输出,就是ResultStage的输入。

(二)、Spark Web UI

在spark-shell中执行wordcount。

val text = sc.textFile("hdfs://node01:8020/wordcount/input/words.txt",2)
.flatMap(_.split(" "))
.map((_,1))
.reduceByKey(_+_).cache
.collect

要是出现skipped那么就会减少对应的task,但是这是没有问题的并且是对   

任务出现skipped是正常的,之所以出现skipped是因为要计算的数据已经缓存到了内存,没有必要再重复计算。出现skipped对结果没有影响,并且也是一种计算优化   

在发生shuffle的过程中,会发生shuffle write和shuffle read。

shuffle write:发生在shuffle之前,把要shuffle的数据写到磁盘

为什么:为了保证数据的安全性、避免占用大量的内存

(三)、任务生成和提交的四个阶段

Spark任务生产和提交的四个步骤可以归纳如下:

1,构建DAG

​    用户提交的job将首先被转换成一系列RDD并通过RDD之间的依赖关系构建DAG,然后将DAG提交到调度系统;

​    DAG描述多个RDD的转换过程,任务执行时,可以按照DAG的描述,执行真正的计算;

​    DAG是有边界的:开始(通过sparkcontext创建的RDD),结束(触发action,调用runjob就是一个完整的DAG形成了,一旦触发action,就形成了一个完整的DAG);

​    一个RDD描述了数据计算过程中的一个环节,而一个DAG包含多个RDD,描述了数据计算过程中的所有环节;

​   一个spark application可以包含多个DAG,取决于具体有多少个action。

2,DAGScheduler将DAG切分stage(切分依据是shuffle),将stage中生成的task以taskset的形式发送给TaskScheduler

为什么要切分stage?

​    一个是复杂业务逻辑(将多台机器上具有相同属性的数据聚合到一台机器上:shuffle)

​    如果有shuffle,那么就意味着前面阶段产生结果后,才能执行下一个阶段,下一个阶段的计算依赖上一个阶段的数据

​    在同一个stage中,会有多个算子,可以合并到一起,我们很难称其为pipeline(流水线,严格按照流程、顺序执行)

3,TaskScheduler 调度task(根据资源情况将task调度到Executors)

4,Executors接收task,然后将task交给线程池执行。

具体可以简化为如下图3-14所示。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值