Spark RDD sortBy算子执行时进行数据 “采样”是什么意思?

一、sortBy 和 RangePartitioner

sortBy 在 Spark 中会在执行排序时采用 rangePartitioner 进行分区,这会影响数据的分区方式,并且这一步骤是通过对数据进行 "采样" 来计算分区的范围。不过,重要的是,sortBy 本身仍然是一个 transformation,它不会立即触发计算,但在执行过程中会涉及到对数据的排序、分区和最终计算。

1. sortBy 和 RangePartitioner

sortBy 会利用 RangePartitioner 来决定数据如何进行分区。RangePartitioner 会在排序之前,首先对数据进行采样,从而得出每个分区的范围,然后根据这些范围进行数据的分区。这是因为数据排序是一个全局操作,而 RangePartitioner 提供了一个合理的划分策略,使得 Spark 在执行排序时能够并行化。

  • 采样过程

    当调用 sortBy 时,Spark 会对数据进行 采样 ,通常使用的是 SampledRDD,这种采样会用来估计数据的分布范围,并为后续的分区计算提供依据。

  • RangePartitioner 的使用
    RangePartitioner 会根据数据的值划分成不同的范围。通常在分布式环境中,我们需要将数据按某种方式划分为多个分区,这个过程会使用一个范围来决定数据分布。

2. 是否会触发 runJob

sortBy 作为 transformation 不会立即触发作业执行。它返回一个新的 RDD,并仅在后续执行 action 操作时才会触发实际的计算。因此,sortBy 不会直接导致 runJob 的执行。只有在你执行类似 collect(), count(), saveAsTextFile() 等行动算子时,整个作业才会执行。

但是,sortBy 内部会涉及到 采样范围分区 ,这些过程是为了确保排序能够在多个分区上并行高效地完成,所有这些操作都在 Spark 内部的 task 中完成。runJob 会在行动算子执行时启动,但在执行过程中,rangePartitioner 的计算、数据的重新分区等步骤会被逐步执行。

3. 源码分析

我们可以通过查看 Spark 源码来更清楚地理解这些步骤。以下是关于 sortBy 和其内部处理的一些关键源码:

RDD.sortBy 源码
scala 复制代码
def sortBy[K: ClassTag, U: Ordering](f: T => K, ascending: Boolean = true, numPartitions: Int)(implicit ord: Ordering[K]): RDD[T] = {
  val partitioner = new RangePartitioner(numPartitions, this) // 使用 RangePartitioner
  val map = this.mapPartitionsWithIndex { (index, iter) =>
    // 计算分区内的排序
    val partitioned = iter.toArray.sortBy(f) 
    partitioned.iterator
  }
  map
}

在这个方法中,RangePartitioner 被用来决定如何将数据分成多个分区。而在实际执行时,分区是通过 mapPartitionsWithIndex 来执行的。

RangePartitioner 源码
scala 复制代码
class RangePartitioner[K, V](partitions: Int, rdd: RDD[(K, V)]) extends Partitioner {
  def getPartition(key: Any): Int = {
    // 根据 key 的范围来决定在哪个分区
    val partitionIndex = rangePartition(key)
    partitionIndex
  }

  def rangePartition(key: Any): Int = {
    // 进行采样,并将数据按范围分到对应的分区
  }
}

4. 触发计算的条件

  • sortBy 是一个 transformation 操作,它会生成一个新的 RDD,并不会立即执行排序。
  • RangePartitioner 会在后台进行数据的分区计算和范围分割,但这一切都不会触发作业执行,直到 action 操作 被调用。

5. 总结

  • sortBy 会利用 RangePartitioner 进行数据的分区和范围划分,这过程中会对数据进行采样以确定每个分区的范围。
  • 这个过程本身不会触发作业执行,只有当你执行一个 action 操作时(如 collect()saveAsTextFile()),Spark 才会触发计算,并启动实际的作业执行,进行排序和分区。

二、 RangePartitioner 的 采样过程

在 Spark 中,RangePartitioner采样过程 是其核心部分之一,它确保能够为数据分配适当的分区,并保证每个分区的数据范围在排序时能够合理地分布。这里我们将深入探讨 RangePartitioner 是如何通过采样来计算分区范围的。

1. RangePartitioner 概述

RangePartitioner 是 Spark 中的一个分区器,常用于按范围将数据进行分区。它通常用于类似 sortBy 这类需要全局排序的操作,目的是为了在分布式环境中进行高效的并行排序。

RangePartitioner 在执行分区时,会利用 采样 来估算每个分区的范围(即每个分区的边界)。这种采样过程通过从数据中提取一个小样本,帮助计算出数据在不同分区上的分布,从而保证数据能够均匀地分配到各个分区中。

2. RangePartitioner 采样过程

采样是 RangePartitioner 计算每个分区的范围的关键。这个过程涉及到以下步骤:

2.1 数据采样

RangePartitioner 会从数据中 随机采样 一部分元素,用来估算数据的分布和计算每个分区的边界。这个采样过程通常不会采用全部数据,而是通过一定比例的数据来进行推测。这是为了减少计算开销,同时确保分区的均衡性。

采样操作通常是在 分布式环境中并行执行 的,Spark 会在多个分区上并行地获取样本数据。

  • 采样的比例:采样比例通常是一个相对较小的数值,目的是减少计算量。Spark 内部会在每个分区中执行采样,以确保最终分区的边界能够反映整个数据集的分布。
2.2 计算分区边界

一旦采样完成,RangePartitioner 就会使用这些采样数据来计算每个分区的边界。这个过程基于采样数据的排序:

  • 排序样本数据:首先,对采样数据进行排序,确保数据可以按顺序进行分区。
  • 计算分割点 :然后,RangePartitioner 会根据排序后的数据划分出多个边界点。这些边界点代表了每个分区的数据范围。例如,如果数据有 1000 个元素,并且要求将数据划分为 10 个分区,那么就会在排序后的数据中选取 9 个分割点。
2.3 创建分区

RangePartitioner 利用这些边界点来创建新的分区。数据根据其值所在的范围,决定落入哪个分区。具体来说,RangePartitioner 会为每个分区计算出一个边界值,然后将所有数据按这些边界值进行分配。

  • 分区计算 :对于每个数据元素,RangePartitioner 会根据元素的值和这些边界值,决定该元素属于哪个分区。

3. 代码实现中的采样部分

在 Spark 的源码中,RangePartitioner 的采样过程是通过以下代码来实现的:

3.1 RangePartitioner 类中的采样
scala 复制代码
class RangePartitioner[K, V](partitions: Int, rdd: RDD[(K, V)]) extends Partitioner {
  // 进行数据的采样
  val sample = rdd.sample(withReplacement = false, fraction = 0.1, seed = 12345)
  val sortedSample = sample.map(_._1).sortBy(identity)

  // 计算每个分区的分割点
  val splits = sortedSample.zipWithIndex.map { case (key, index) =>
    if (index % (sampleCount / partitions) == 0) key else null
  }.filter(_ != null)

  def getPartition(key: Any): Int = {
    // 根据采样的分割点进行分区
    var low = 0
    var high = partitions - 1
    while (low < high) {
      val mid = (low + high) / 2
      if (key < splits(mid)) high = mid - 1 else low = mid + 1
    }
    low
  }
}

在上面的代码中,sample 操作会对 RDD 中的数据进行采样,并将其按值排序。然后,通过分割排序后的数据,计算出每个分区的边界点。这些边界点随后用于 getPartition 方法中来确定数据的分配。

3.2 采样与排序
  • rdd.sample(withReplacement = false, fraction = 0.1):从原始 RDD 中采样 10% 的数据(fraction = 0.1),并且不进行重复采样。
  • sortBy(identity):对采样的数据进行排序,确保采样数据的顺序正确,便于后续计算边界。

4. 触发计算

在执行 sortBy 操作时,Spark 会根据 RangePartitioner 对数据进行采样、排序和分区计算。这些操作会在你执行 action 操作 (如 collect()saveAsTextFile())时触发,具体的分区计算会在计算过程中完成。直到行动算子触发,计算过程才会开始,RangePartitioner 会根据采样数据生成分区,并最终执行数据的排序。

5. 总结

  • 采样RangePartitioner 会从数据中随机采样一部分元素(通常是 10% 或其他比例),用来估算数据的分布。
  • 排序与计算分区边界:采样数据被排序,并根据排序后的数据计算出每个分区的边界。这样可以确保数据均匀分配到不同的分区。
  • 数据分区 :根据采样和计算出的边界,RangePartitioner 会将数据分配到相应的分区中。

通过这种采样与分区机制,RangePartitioner 能够高效地支持 Spark 的排序操作,使得数据在分布式环境中能够有效地并行处理。


三、举例介绍RangePartitioner采样过程

理解 RangePartitioner 如何通过采样来获得数据分布、计算边界,并将数据分配到相应分区的过程,确实比较抽象。我会通过一个简单的例子来帮助你更直观地理解这个过程。

问题场景

假设你有一个数据集,包含了以下的 10 个整数:

[10, 23, 1, 9, 15, 37, 2, 16, 40, 3]

你想用 RangePartitioner 来将这些数据分为 3 个分区,并且根据它们的值进行排序。

1. 采样数据

首先,为了计算每个分区的边界,RangePartitioner 会对数据集进行采样。假设我们采样 30% 的数据(即随机选择 3 个数据点)。假设采样到的数据是:

[10, 23, 3]

2. 排序采样数据

然后,对采样的数据进行排序,确保它们按大小排列。对于这个例子,排序后的采样数据是:

[3, 10, 23]

3. 计算分区边界

通过对采样数据进行排序,RangePartitioner 可以计算出分区的边界。在我们的例子中,我们有 3 个分区,因此我们需要为数据计算 2 个边界(因为 n 个分区需要 n-1 个边界)。

根据排序后的采样数据 [3, 10, 23]RangePartitioner 可以选择分割点来确定边界:

  • 第一个边界 :选择采样数据的第一个元素(3)。
  • 第二个边界 :选择采样数据的最后一个元素(23)。

现在我们有了两个边界:

  • 分区 1:所有小于 10 的数据
  • 分区 2:所有大于等于 10 小于 23 的数据
  • 分区 3:所有大于等于 23 的数据

4. 分配数据到分区

接下来,RangePartitioner 会根据这些边界将数据分配到相应的分区中。具体的分区规则是:

  • 分区 1 :所有小于 10 的元素 → [1, 2, 3, 9]
  • 分区 2 :所有大于等于 10 且小于 23 的元素 → [10, 15, 16]
  • 分区 3 :所有大于等于 23 的元素 → [23, 37, 40]

所以最终的分区结果是:

  • 分区 1[1, 2, 3, 9]
  • 分区 2[10, 15, 16]
  • 分区 3[23, 37, 40]

5. 总结过程

通过这个例子,我们可以看到 RangePartitioner 的整个过程:

  1. 采样数据:从整个数据集中随机抽取一部分数据(这里是 30%)。
  2. 排序采样数据:对采样数据进行排序,确保我们能根据数据的范围计算边界。
  3. 计算分区边界:根据排序后的采样数据,选择边界来划分数据(例如第一个和最后一个元素)。
  4. 分配数据到分区:根据边界将所有数据分配到相应的分区中。

6. 实际执行的情况

  • 采样比例 :在实际的 Spark 中,采样比例并不一定是 30%,通常是根据数据的大小和分区数量进行调整的。采样可以确保 RangePartitioner 在计算边界时不会消耗过多资源。
  • 多个分区 :如果数据集更大,分区数量更多,RangePartitioner 会选择更多的采样点来划分分区。边界点会根据排序后的采样数据来动态选择。

7. 关键源码中的采样部分

在实际 Spark 的源码中,采样是通过 sample 方法实现的:

scala 复制代码
val sample = rdd.sample(withReplacement = false, fraction = 0.1, seed = 12345)
val sortedSample = sample.map(_._1).sortBy(identity)

然后通过这些采样的排序数据,计算每个分区的边界。例如,当分区数量是 3 时,RangePartitioner 会选取采样数据的前几个元素作为边界,并用这些边界来确定每个分区的范围。


8. 进一步优化

在实际使用中,Spark 的 RangePartitioner 会通过自适应调整采样的比例和算法来优化性能,确保在处理大型数据集时依然高效。在某些情况下,Spark 会使用更智能的策略来决定采样的方式,以便在并行处理中避免过多的计算开销。

总结

通过采样、排序和计算边界,RangePartitioner 确保了数据可以均匀地分配到不同的分区中,从而为排序等操作提供并行化的支持。这一过程使得 Spark 在处理大规模数据时能够有效地进行全局排序。

相关推荐
Data跳动4 小时前
Spark内存都消耗在哪里了?
大数据·分布式·spark
lucky_syq5 小时前
流式处理,为什么Flink比Spark Streaming好?
大数据·flink·spark
goTsHgo10 小时前
在 Spark 上实现 Graph Embedding
大数据·spark·embedding
程序猿小柒10 小时前
【Spark】Spark SQL执行计划-精简版
大数据·sql·spark
隔着天花板看星星10 小时前
Spark-Streaming集成Kafka
大数据·分布式·中间件·spark·kafka
lucky_syq2 天前
Spark和Hive的区别
大数据·hive·spark
隔着天花板看星星2 天前
Spark-Streaming receiver模式源码解析
大数据·分布式·spark
Data跳动3 天前
Spark 运行时对哪些数据会做缓存?
java·缓存·spark
隔着天花板看星星3 天前
Spark-Streaming性能调优
大数据·分布式·spark