当前位置:   article > 正文

大数据最全Spark分区 partition 详解_spark partition(2),2024年最新大数据开发插件化主流框架和实现原理_spark.sql.partitionprovider

spark.sql.partitionprovider

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

如果资源不变,你的RDD只有2个分区,那么同一时刻只有2个task运行,其余18个核空转,造成资源浪费。

这就是在spark调优中,增大RDD分区数目,增大任务并行度的原因

2、spark分区 什么时候增加的,增加有什么用?

接下来的描述,是针对于sparksql(也就是把数据加载成Dataset之后再处理)来说的。

1.增加分区数,可以增加并行度,当spark申请的cpu核心足够的情况下,可以同时跑不同分区的数据(因为一个分区的数据,只能由一个核心来跑,不能多个)

2.手动增加,使用repartition来将所有数据打散

3.自动增加,spark有个参数:spark.sql.shuffle.partitions,默认值为200。也就是说当触发shuffle逻辑的时候,数据会自动分为200个分区运行,但是在数据量大的情况下,每个分区的数据量太大,而且假设spark申请到了300个核心,但是因为分区数只有200,会导致只有200个核心在运行,另外100个核心在空转(虽然占用资源但是却不干活)。所以可以将该参数设置为500甚至更大,来增加分区数和并行度。

二、repartition适用场景

使用repartition 使得任务能够并行执行的话,分配的core的数量一定要略微大于最大的分区数,才能使得所有的task能够并行执行。

–conf spark.sql.shuffle.partitions=100 #默认大小为200。
        这里举个例子,比如当前RDD有100个分区,如果我分配的是50个core(spark.executor.instances * spark.executor.cores),那么同时执行的任务最多只有50。同一个操作需分成两部分串行执行,这个时候如果有一个节点有问题(并行度是50 -1*2 = 48),那么就需要再次申请资源,如果资源紧张,需要等待,如果申请的时候申请的是54个core,虽然看起来有点浪费,但是对于任务的稳定性而言,却可能会有很大的提升。当然最好的视情况是分配102-110比较合理。

1、处理能力不够

RDD单个分区数据量比较大,或者单个分区处理比较慢,都可以通过repartition进行操作,这个时候numPartitions自然是要比当前的分区数要大一些。

单个分区数据量比较大,这种case在读取和处理hdfs文件的时候并不常见,因为默认spark读取hdfs文件的分区数和hdfs文件的block的数量是一致的。这个mr的mapper数量类似。但是对于Spark Streaming 任务,如果是window窗口较长,就比较容易出现单个分区数据量较大的情况。

还有一种case,比如每个需要有外部的一些读写操作,这个时候大量数据在一个partition中,会因为外部存储、单机处理能力,网络等的性能瓶颈导致数据每个partition的数据处理变慢,举个例子,往redis里写数据,如果每个partition数据量特别大,如果使用的是redis的非批量的,基本每个数据需要一次数据请求,假设采用的是pipleline的形式,依然会因为数据量很大导致这个分区的写入很慢,从而导致任务执行时间较长。这种问题在Spark Streaming的任务中影响比较大。这个时候通过repartition增加分区数量效果也会很明显。

2、数据倾斜

数据倾斜,这个自然不不用说了,使用repartition可以将数据进行打散,避免倾斜导致的执行耗时不均匀的问题。虽然这种情况是完全可以减少任务执行的时间,但是对于数据量比价小的分区而言,很快就能处理完,此时如果executor空闲的话,其实是比较浪费资源的。所以如果是通过这个处理数据倾斜的话,建议开启动态资源分配。另外使用reapartition处理数据倾斜的时候,最好提前统计一下key的分布,这个比较常见的方法就是:
1 如果是hive的数据

select  count(1)  from table group by key

  • 1
  • 2

2 如果是rdd中统计

val keyCount =rdd.map((k,v) =>(k,1)).reduceByKey((a,b) =>(a+b)).sortBy(_._2,false).take(100)
print keyCount

  • 1
  • 2
  • 3
需要进行合并场景

本身需要进行分区合并的时候使用coalesce是最合适的,原因就在于coalesce默认shuffle是false,也就是说,如果100个分区 想要变成50个分区,这个时候coalesce并不会进行shuffle操作。进行coalesce的场景,数据分区很多,但是每个分区数较少,这个时候其实并不太影响执行效率,但是对于一些需要外部链接,或者写入文件的场景来说,这是很浪费资源的。
需要使用shuffle进行分区数减少的场景,原始的rdd分区数据分散不是很均匀,比如 当前RDD是100个分区,想要合并成50个分区,但是100个分区总数据分布从10K-200M分配不均,这个时候为了方便下游数据处理,我们可以将数据进行shuffle形式的合并。

repartition是spark开发中使用非常多的一个操作,而且使用的是否合理直接影响到任务执行的快慢以及成功与否。是否需要进行repartition,以及repartition的数量是一个需要开发的同学多方面综合考虑的结果,并没有一成不变的经验。但是我这儿依然给出一个具体的思路以供参考:确定原始数据分区数量,以及分区大小,key的分布式,参考集群资源的是否充足以及资源使用分布(cpu和内存使用比例),然后综合考虑。

spark.default.parallelism    只有在处理RDD时才会起作用,对Spark SQL的无效。
spark.sql.shuffle.partitions 则是对sparks SQL专用的设置

RDD分区的一个分区原则:尽可能是得分区的个数等于集群核心数目

无论是本地模式、Standalone模式、YARN模式或Mesos模式,我们都可以**通过spark.default.parallelism来配置其默认分区个数,**若没有设置该值,则根据不同的集群环境确定该值

三、Spark数据分区方式

在Spark中,RDD(Resilient Distributed Dataset)是其最基本的抽象数据集,其中每个RDD是由若干个Partition组成。在Job运行期间,参与运算的Partition数据分布在多台机器的内存当中。这里可将RDD看成一个非常大的数组其中Partition是数组中的每个元素,并且这些元素分布在多台机器中。

图一中,RDD1包含了5个Partition,RDD2包含了3个Partition,这些Partition分布在4个节点中。

Spark包含两种数据分区方式:HashPartitioner(哈希分区)和RangePartitioner(范围分区)。一般而言,对于初始读入的数据是不具有任何的数据分区方式的。数据分区方式只作用于<Key,Value>形式的数据。因此,当一个Job包含Shuffle操作类型的算子时,如groupByKey,reduceByKey etc,此时就会使用数据分区方式来对数据进行分区,即确定某一个Key对应的键值对数据分配到哪一个Partition中。在Spark Shuffle阶段中,共分为Shuffle Write阶段和Shuffle Read阶段,其中在Shuffle Write阶段中,Shuffle Map Task对数据进行处理产生中间数据,然后再根据数据分区方式对中间数据进行分区。最终Shffle Read阶段中的Shuffle Read Task会拉取Shuffle Write阶段中产生的并已经分好区的中间数据。图2中描述了Shuffle阶段与Partition关系。下面则分别介绍Spark中存在的两种数据分区方式。

一、HashPartitioner(哈希分区)

HashPartitioner采用哈希的方式对<Key,Value>键值对数据进行分区。其数据分区规则为 partitionId = Key.hashCode % numPartitions,其中partitionId代表该Key对应的键值对数据应当分配到的Partition标识,Key.hashCode表示该Key的哈希值,numPartitions表示包含的Partition个数。图3简单描述了HashPartitioner的数据分区过程。

2、HashPartitioner源码详解

  • HashPartitioner源码较为简单,这里不再进行详细解释。
class HashPartitioner(partitions: Int) extends Partitioner {
  require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")
 
  /**
    * 包含的分区个数
    */
  def numPartitions: Int = partitions
 
  /**
    * 获得Key对应的partitionId
    */
  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
}
 
def nonNegativeMod(x: Int, mod: Int): Int = {
  val rawMod = x % mod
  rawMod + (if (rawMod < 0) mod else 0)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
二、RangePartitioner(范围分区)

Spark引入RangePartitioner的目的是为了解决HashPartitioner所带来的分区倾斜问题,也即分区中包含的数据量不均衡问题。HashPartitioner采用哈希的方式将同一类型的Key分配到同一个Partition中,因此当某一或某几种类型数据量较多时,就会造成若干Partition中包含的数据过大问题,而在Job执行过程中,一个Partition对应一个Task,此时就会使得某几个Task运行过慢。RangePartitioner基于抽样的思想来对数据进行分区。图4简单描述了RangePartitioner的数据分区过程。

2、RangePartitioner源码详解
① 确定采样数据的规模:RangePartitioner默认对生成的子RDD中的每个Partition采集20条数据,样本数据最大为1e6条。

// 总共需要采集的样本数据个数,其中partitions代表最终子RDD中包含的Partition个数
val sampleSize = math.min(20.0 * partitions, 1e6)
  • 1
  • 2

② 确定父RDD中每个Partition中应当采集的数据量:这里注意的是,对父RDD中每个Partition采集的数据量会在平均值上乘以3,这里是为了后继在进行判断一个Partition是否发生了倾斜,当一个Partition包含的数据量超过了平均值的三倍,此时会认为该Partition发生了数据倾斜,会对该Partition调用sample算子进行重新采样。

// 被采样的RDD中每个partition应该被采集的数据,这里将平均采集每个partition中数据的3倍
val sampleSizePerPartition = math.ceil(3.0 * sampleSize / rdd.partitions.length).toInt

  • 1
  • 2
  • 3

③ 调用sketch方法进行数据采样:sketch方法返回的结果为<采样RDD的数据量,<partitionId, 分区数据量,分区采样的数据量>>。在sketch方法中会使用水塘抽样算法对待采样的各个分区进行数据采样,这里采用水塘抽样算法是由于实现无法知道每个Partition中包含的数据量,而水塘抽样算法可以保证在不知道整体的数据量下仍然可以等概率地抽取出每条数据。图4简单描述了水塘抽样过程。

// 使用sketch方法进行数据抽样
val (numItems, sketched) = RangePartitioner.sketch(rdd.map(_._1), sampleSizePerPartition)
 
/**
  * @param rdd 需要采集数据的RDD
  * @param sampleSizePerPartition 每个partition采集的数据量
  * @return <采样RDD数据总量,<partitionId, 当前分区的数据量,当前分区采集的数据量>>
  */
def sketch[K : ClassTag](
    rdd: RDD[K],
    sampleSizePerPartition: Int): (Long, Array[(Int, Long, Array[K])]) = {
  val shift = rdd.id
  val sketched = rdd.mapPartitionsWithIndex { (idx, iter) =>
    val seed = byteswap32(idx ^ (shift << 16))
    // 使用水塘抽样算法进行抽样,抽样结果是个二元组<Partition中抽取的样本量,Partition中包含的数据量>
    val (sample, n) = SamplingUtils.reservoirSampleAndCount(
      iter, sampleSizePerPartition, seed)
    Iterator((idx, n, sample))
  }.collect()
  val numItems = sketched.map(_._2).sum
  (numItems, sketched)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

④ 数据抽样完成后,需要对不均衡的Partition重新进行抽样,默认当Partition中包含的数据量大于平均值的三倍时,该Partition是不均衡的。当采样完成后,利用样本容量和RDD中包含的数据总量,可以得到整体的一个数据采样率fraction。利用此采样率对不均衡的Partition调用sample算子重新进行抽样。

// 计算数据采样率
val fraction = math.min(sampleSize / math.max(numItems, 1L), 1.0)
// 存放采样Key以及采样权重
val candidates = ArrayBuffer.empty[(K, Float)]
// 存放不均衡的Partition
val imbalancedPartitions = mutable.Set.empty[Int]
//(idx, n, sample)=> (partition id, 当前分区数据个数,当前partition的采样数据)
sketched.foreach { case (idx, n, sample) =>
  // 当一个分区中的数据量大于平均分区数据量的3倍时,认为该分区是倾斜的
  if (fraction * n > sampleSizePerPartition) {
    imbalancedPartitions += idx
  }
  // 在三倍之内的认为没有发生数据倾斜
  else {
    // 每条数据的采样间隔 = 1/采样率 = 1/(sample.size/n.toDouble) = n.toDouble/sample.size
    val weight = (n.toDouble / sample.length).toFloat
    // 对当前分区中的采样数据,对每个key形成一个二元组<key, weight>
    for (key <- sample) {
      candidates += ((key, weight))
    }
  }
}
// 对于非均衡的partition,重新采用sample算子进行抽样
if (imbalancedPartitions.nonEmpty) {
  val imbalanced = new PartitionPruningRDD(rdd.map(_._1), imbalancedPartitions.contains)
  val seed = byteswap32(-rdd.id - 1)
  val reSampled = imbalanced.sample(withReplacement = false, fraction, seed).collect()
  val weight = (1.0 / fraction).toFloat
  candidates ++= reSampled.map(x => (x, weight))
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

⑤ 确定各个Partition的Key范围:使用determineBounds方法来确定每个Partition中包含的Key范围,先对采样的Key进行排序,然后计算每个Partition平均包含的Key权重,然后采用平均分配原则来确定各个Partition包含的Key范围。如当前采样Key以及权重为:

<1, 0.2>, 
<2, 0.1>, 
❤️, 0.1>, 
<4, 0.3>,
<5, 0.1>,
<6, 0.3>,

现在将其分配到3个Partition中,则每个Partition的平均权重为:(0.2 + 0.1 + 0.1 + 0.3 + 0.1 + 0.3) / 3 = 0.36。

此时Partition1 ~ 3分配的Key以及总权重为

<Partition1, {1, 2, 3}, 0.4> 
<Partition2, {4, 5}, 0.4> 
<Partition1, {6}, 0.3>。

/**
  * @param candidates 未按采样间隔排序的抽样数据
  * @param partitions 最终生成的RDD包含的分区个数
  * @return 分区边界
  */
def determineBounds[K : Ordering : ClassTag](
    candidates: ArrayBuffer[(K, Float)],
    partitions: Int): Array[K] = {
  val ordering = implicitly[Ordering[K]]
  // 对样本按照key进行排序
  val ordered = candidates.sortBy(_._1)
  // 抽取的样本容量
  val numCandidates = ordered.size
  // 抽取的样本对应的采样间隔之和
  val sumWeights = ordered.map(_._2.toDouble).sum
  // 平均每个分区的步长


![img](https://img-blog.csdnimg.cn/img_convert/0ad5c809d945dc9737cd43cbf7055f27.png)
![img](https://img-blog.csdnimg.cn/img_convert/c36053d39777ae23dbeaf25b466dc1ba.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

-1715423350463)]
[外链图片转存中...(img-r9KJtYR5-1715423350464)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/727483
推荐阅读
相关标签
  

闽ICP备14008679号