赞
踩
以Spark2.X为例,其支持Hash、Range以及自定义分区器。
分区器决定了rdd数据在分布式运算时的分区个数以及数据在shuffle中发往的分区号,而分区的个数决定了reduce的个数;同样的shuffle过程中若分区器定义或选择不合适将大大增加数据倾斜的风险。综上,分区器的重要性不言而喻。
首先要知道
(1)Key-Value类型RDD才有分区器,非Key-Value类型RDD的分区值是None。
(2)每个RDD的分区ID范围为0~numPartitions-1,其决定数据所属分区。
Hash分区
对于给定的key,计算其hashCode并对分区个数取余。如果余数小于0,则用余数+分区的个数(否则加0),最后返回的值就是这个key所属的分区ID
弊端:
可能导致每个分区中数据量的不均匀,导致数据倾斜问题。
源码如下:
/** * A [[org.apache.spark.Partitioner]] that implements hash-based partitioning using * Java's `Object.hashCode`. * * Java arrays have hashCodes that are based on the arrays' identities rather than their contents, * so attempting to partition an RDD[Array[_]] or RDD[(Array[_], _)] using a HashPartitioner will * produce an unexpected or incorrect result. */ 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 }
Ranger分区
1、将一定范围内的数据映射到某一分区内,尽量保证数据的均匀分布。
2、分区间的数据是有序的,但分区内的元素不能保证有序的。如分区1内的数据为[1,5)内的整数值且无序,分区2的数据为[5,10)内的整数且无序,但分区1的数据全体小于分区2的数据。
实现过程:
第一步:从整个RDD中抽取样本数据进行排序得到每个分区的最大key值,而后形成一个Array[KEY],也就是key值范围rangeBounds。
第二步:判断key在rangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标。
部分源码如下:
/** * A [[org.apache.spark.Partitioner]] that partitions sortable records by range into roughly * equal ranges. The ranges are determined by sampling the content of the RDD passed in. * * @note The actual number of partitions created by the RangePartitioner might not be the same * as the `partitions` parameter, in the case where the number of sampled records is less than * the value of `partitions`. */ class RangePartitioner[K : Ordering : ClassTag, V]( partitions: Int, rdd: RDD[_ <: Product2[K, V]], private var ascending: Boolean = true) extends Partitioner { // We allow partitions = 0, which happens when sorting an empty RDD under the default settings. require(partitions >= 0, s"Number of partitions cannot be negative but found $partitions.") private var ordering = implicitly[Ordering[K]] // An array of upper bounds for the first (partitions - 1) partitions private var rangeBounds: Array[K] = { if (partitions <= 1) { Array.empty } else { // This is the sample size we need to have roughly balanced output partitions, capped at 1M. val sampleSize = math.min(20.0 * partitions, 1e6) // Assume the input partitions are roughly balanced and over-sample a little bit. val sampleSizePerPartition = math.ceil(3.0 * sampleSize / rdd.partitions.length).toInt val (numItems, sketched) = RangePartitioner.sketch(rdd.map(_._1), sampleSizePerPartition) if (numItems == 0L) { Array.empty } else { // If a partition contains much more than the average number of items, we re-sample from it // to ensure that enough items are collected from that partition. val fraction = math.min(sampleSize / math.max(numItems, 1L), 1.0) val candidates = ArrayBuffer.empty[(K, Float)] val imbalancedPartitions = mutable.Set.empty[Int] sketched.foreach { case (idx, n, sample) => if (fraction * n > sampleSizePerPartition) { imbalancedPartitions += idx } else { // The weight is 1 over the sampling probability. val weight = (n.toDouble / sample.length).toFloat for (key <- sample) { candidates += ((key, weight)) } } } if (imbalancedPartitions.nonEmpty) { // Re-sample imbalanced partitions with the desired sampling probability. 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)) } RangePartitioner.determineBounds(candidates, partitions) } } } } }
从上我们知道在获取rangeBounds时涉及到排序,也就是说该分区器要求RDD中的KEY类型是可以排序的。默认情况下采用的hash分区器。If any of the RDDs already has a partitioner, choose that one.Otherwise, we use a default HashPartitioner.
而实际上在生产环境中很多情况下key都是不可排序的复杂数据类型,同时由于实际业务需求的多样性,系统自带的分区器无法满足的情况下,自定义分区器就上场了。
Spark自定义分区器简单示例
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。