Spark分区器HashPartitioner和RangePartitioner/全局排序

本文介绍了Spark中的两种主要分区器:HashPartitioner和RangePartitioner,详细阐述了它们的工作原理和适用场景。HashPartitioner通过hashCode进行分区,可能导致数据不均匀;而RangePartitioner确保数据均匀分布且分区间有序。此外,文章还讨论了自定义分区器的实现以及如何在Spark中实现全局数据排序。

Spark分区器

  在Spark中分区器直接决定了RDD中分区的个数,RDD中每条数据经过Shuffle过程属于哪个分区以及Reduce的个数。只有Key-Value类型的RDD才有分区的,非Key-Value类型的RDD分区的值是None的。
  在Spark中,存在两类分区函数:HashPartitionerRangePartitioner,它们都是继承自Partitioner,主要提供了每个RDD有几个分区(numPartitions)以及对于给定的值返回一个分区ID(0~numPartitions-1),也就是决定这个值是属于那个分区的。

HashPartitioner分区

  HashPartitioner分区的原理很简单,对于给定的key,计算其hashCode,并除于分区的个数取余,最后返回的值就是这个key所属的分区ID。

RangePartitioner分区

  从HashPartitioner分区的实现原理可以看出,其结果可能导致每个分区中数据量的不均匀。而RangePartitioner分区则尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,但是分区内的元素是不能保证顺序的。sortByKey底层就是RangePartitioner分区器。
  首先了解蓄水池抽样(Reservoir Sampling),它能够在O(n)时间内对n个数据进行等概率随机抽取。首先构建一个可放k个元素的蓄水池,将序列的前k个元素放入蓄水池中。然后从第k+1个元素开始,以k/n的概率来替换掉蓄水池中国的某个元素即可。当遍历完所有元素之后,就可以得到随机挑选出的k个元素,复杂度为O(n)。
  RangePartitioner分区器的主要作用就是将一定范围内的数映射到某一个分区内。该分区器的实现方式主要是通过两个步骤来实现的,第一步,先从整个RDD中抽取出样本数据,将样本数据排序,计算出每个分区的最大key值,形成一个Array[KEY]类型的数组变量rangeBounds;第二步,判断key在rangeBounds中所处的范围,给出该key的分区ID。

RangePartitioner的重点是在于构建rangeBounds数组对象,主要步骤是:

  1. 计算总体的数据抽样大小sampleSize,计算规则是:(math.min(20.0 * partitions, 1e6)),至少每个分区抽取20个数据或者最多1M的数据量
  2. 根据sampleSize和分区数量计算每个分区的数据抽样样本数量sampleSizePrePartition(math.ceil(3.0 * sampleSize / rdd.partitions.length).toInt),即每个分区抽取的数据量一般会比之前计算的大一点)
  3. 调用RangePartitioner的sketch函数进行数据抽样,计算出每个分区的样本
  4. 计算样本的整体占比以及数据量过多的数据分区,防止数据倾斜
  5. 对于数据量比较多的RDD分区调用RDD的sample函数API重新进行数据抽取
  6. 将最终的样本数据通过RangePartitoner的determineBounds函数进行数据排序分配,计算出rangeBounds

  RangePartitioner的sketch函数的作用是对RDD中的数据按照需要的样本数据量进行数据抽取,主要调用SamplingUtils类的reservoirSampleAndCount方法对每个分区进行数据抽取,抽取后计算出整体所有分区的数据量大小;reservoirSampleAndCount方法的抽取方式是先从迭代器中获取样本数量个数据(顺序获取), 然后对剩余的数据进行判断,替换之前的样本数据,最终达到数据抽样的效果。RangePartitioner的determineBounds函数的作用是根据样本数据记忆权重大小确定数据边界

自定义分区器

  可以通过扩展Spark中的默认Partitioner类来自定义我们需要的分区数以及应该存储在这些分区中的内容。然后通过partitionBy()在RDD上应用自定义分区逻辑

Spark 数据全局排序实现

  可以局部排序最后合并到同一个文件,保证全局有序,这样可以设置一个reduce任务实现,但是对于更大量的数据容易出现OOM。如果不合并到同一个文件的话,可以将每一个分区有序的数据输出到磁盘。最后借鉴Kafka的数据管理方式建立稀疏索引方便以后的数据访问。

1、定义一个Partitioner保证某一范围内的所有数据都在同一个分区

import org.apache.spark.Partitioner

class SortPartitoner(num: Int) extends Partitioner {
  override def numPartitions: Int = num
  val partitionerSize = Integer.MAX_VALUE / num + 1
  override def getPartition(key: Any): Int = {
    val intKey = key.asInstanceOf[Int]
    intKey / partitionerSize
  }
}

该分区根据数据范围划分为num个子范围,然后将每个数字分配到对应的子范围中,这种情况下当数据在各个子范围分布均匀时候可以表现良好。但是当数据严重聚集时候,会发生数据倾斜。当存在数据倾斜时候可以使用Spark提供的 RangePartitioner分区器进行分区。

2、分区内部排序,保证分区内有序

object Sort {
  def main(args: Array[String]) {
    val conf = new SparkConf()
    val sc = new SparkContext(conf)
    val numbers = sc.textFile("/random.txt").flatMap(_.split(" ")).map(x => (x.toInt, 1)).cache()
    val result = numbers.repartitionAndSortWithinPartitions(new SortPartitoner(numbers.partitions.length)).map(x=>x._1)
    result.saveAsTextFile("/bigdatasort")
    sc.stop()
  }
}

使用repartitionAndSortWithinPartitions算子进行分区并分区内部排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值