sprak Core中比较常用的排序方法sortBy和sortKByKey,这是一个shuffle类算法子,宽依赖,出发DAGSchedular划分Stage,那么他们排序的原理是啥呢?
第一步Stage0:
分区采样Sample,创建RangePartitioner,先对输入的数据的key做采样,来估算Key的分布,然后按照指定的排序切分range,尽量让每个partition对应的range里的key分布均匀,计算出一个RangeBounds,长度为partitions - 1的Array[Key],里面存放的数据为前partition - 1分区中每个分区的key的上界,最后一个partition就是存放剩余的其他数据
此时的采样大小:
//此处确定采样大小,前提假设数据分区大致平衡,即不存在倾斜的情况
// 总体采样数量
val sampleSize = math.min(20.0 * partitions, 1e6)
// 每个分区采样数量
val sampleSizePerPartition = math.ceil(3.0 * sampleSize / rdd.partitions.length).toInt
// 数据采样
val (numItems, sketched) = RangePartitioner.sketch(rdd.map(_._1), sampleSizePerPartition)
// 此方法数据采样 返回的数据
def sketch[K : ClassTag](
rdd: RDD[K],
sampleSizePerPartition: Int): (Long, Array[(Int, Long, Array[K])]) = {
val shift = rdd.id
// val classTagK = classTag[K] // to avoid serializing the entire partitioner object
val sketched = rdd.mapPartitionsWithIndex { (idx, iter) =>
val seed = byteswap32(idx ^ (shift << 16))
val (sample, n) = SamplingUtils.reservoirSampleAndCount(
iter, sampleSizePerPartition, seed)
Iterator((idx, n, sample))
}.collect()
val numItems = sketched.map(_._2).sum
(numItems, sketched)
}
如果一个分区包含的项远远超过了平均数,我们将从中重新采样以确保从该分区收集足够的项。
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 {
// 采样的数据占整体的比例
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 {
val weight = (n.toDouble / sample.length).toFloat
for (key <- sample) {
candidates += ((key, weight))
}
}
}
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))
}
RangePartitioner.determineBounds(candidates, partitions)
}
第二步:shuffle write
开始shuffle在map side做shuffle write,每个计算节点根据前面计算出来的rangeBounds对输入数据重新分片,分片采用rangePartitioner 使得重新分区后的数据在分区之间只排好序的,
然后此时分区内的数据不一定是已经排好序
第三步:shuffle read
在reducer side,对每个分区内的数据做排序
这样完成之后,partition之间的数据在map side就保证有序,而每个分区内的数据在reducer side也保证有排序,从而达到了全局排序的效果
大致流程图如下:
此时引发一个问题:我相信很多小伙伴在面试的时候都遇到过这个问题:给定1T的文件和只有1G内存的机器,如何实现全排序?
1,产生随机数,生成10个测试文件,10个线程同时进行。
2,将大文件分割1BM的小文件,每个线程对分割而成的内容进行内部排序后,写入文件,利用自定义阻塞线程池,每次同时写入3~4个文件。
3,将所有小文件排序后,利用多路排序算法将小文件写入最终文件。
如果可以的话是否也可采用类似spark排序的的原理呢?欢迎交流!