spark2原理分析-RDD的Partitioner原理分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zg_hover/article/details/83511860

概述

本文介绍了Spark分区的实现原理,并对其源码进行了分析。

分区器(Partitioner)的基本概念

关于Spark分区的基本概念和介绍,可以参考我的这篇文章:Spark2.0-RDD分区原理分析。这里我们再回顾一下Spark分区的概念:

从概念上讲,分区器(Partitioner)定义了如何分布记录,从而定义每个任务将处理哪些记录。

从实现层面来说,Partitioner是一个带有以下两个方法的抽象类:numPartitions和getPartition。该类的定义如下:

abstract class Partitioner extends Serializable {
  def numPartitions: Int
  def getPartition(key: Any): Int
}
  • numPartitions:定义分区后RDD中的分区数。
  • getPartition:定义从key到分区的整数索引的映射,其中应该发送具有该key的记录。

Spark提供的分区器(Partitioner)对象有两种实现:HashPartitioner和RangePartitioner(在2.3中有更多的实现)。如果这些都不满足需要,可以自定义分区程序。

HashPartitioner

当RDD没有Partitioner时,会把HashPartitioner作为默认的Partitioner。它通过计算key的hashcode来对数据进行分区。该类的实现代码如下:

class HashPartitioner(partitions: Int) extends Partitioner {
  require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")

  // 确定的分区的数量
  def numPartitions: Int = partitions

  // key到分区id的映射,这里是通过取模的方式实现
  def getPartition(key: Any): Int = key match {
    case null => 0
    // 取模运算:hashcode%分区数
    case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
  }

  // 重新定义equal函数,若是HashPartitioner且分区数相等,返回true
  override def equals(other: Any): Boolean = other match {
    case h: HashPartitioner =>
      h.numPartitions == numPartitions
    case _ =>
      false
  }
  // 把HashPartitioner的hashCode设置为分区数
  override def hashCode: Int = numPartitions
}

注意:传给HashPartitioner(partitions: Int)的参数partitions不能为负。

HashPartitioner具有以下特点:

  • HashPartitioner根据key的哈希值(hashcode)确定子分区的索引位置。
  • HashPartitioner需要一个分区参数,该参数确定输出RDD中的分区数和散列函数中使用的分区数。若没有指定该参数,Spark则使用SparkConf中spark.default.parallelism值的值来确定分区数。
  • 若没有设置默认并行度值(spark.default.parallelism参数的值),则Spark默认为RDD在其血缘(lineage)中具有的最大分区数。
  • 在使用HashPartitioner的宽转换(wide transform)(例如aggregateByKey)中,可选的分区数参数用作散列分区程序的参数。

RangePartitioner

RangePartitioner(范围分区)将其key位于相同范围内的记录分配给给定分区。排序需要RangePartitioner,因为RangePartitioner能够确保:通过对给定分区内的记录进行排序,完成整个RDD的排序。

RangePartitioner首先通过采样确定每个分区的范围边界:优化跨分区的记录进行均匀分布。然后,RDD中的每个记录将被shuffled到其范围界限内包括该key的分区。

高度不平衡的数据(即,某些key的许多值而不是其他key,如果key的分布不均匀)会使采样不准确,不均匀的分区可能导致下游任务比其他任务更慢,而导致整个任务变慢。

如果与某个关键字相关联的所有记录的重复key太多而被分配到一个执行器(executor),则范围分区(如散列分区)可能会导致内存错误。与排序相关的性能问题通常是由范围分区步骤的这些问题引起的。

使用Spark创建RangePartitioner不仅需要分区数量的参数,还需要实际的RDD,用来获取样本。 RDD必须是元组,并且key必须具有已定义的顺序。

实际上,采样需要部分评估RDD,从而导致执行图(graph)中断。 因此,范围分区实际上既是转换(transformation)操作又是action(动作)操作。 在范围分区中采样需要消耗资源,有一定成本,通常,RangePartitioner(范围分区)比HashPartitioner(散列分区)更耗性能。由于key要求被排序,这样就无法在元组的所有RDD上进行范围分区。

因此,键/值操作(例如聚合)需要使用HashPartitioner作为默认值,这些操作需要每个key都位于同一台机器上但不以特定方式排序的记录。但是,也可以使用自定义分区程序或范围分区程序执行这些方法。

RangePartitioner的实现:

成员变量 说明
ascending 定义分区数据的排序方式,默认是升序。定义如下:private var ascending: Boolean = true
samplePointsPerPartitionHint 每个分区具有的样本数目。
partitions 分区数,可以为0。当该参数为0时,表示对空RDD进行排序。
ordering 排序需要用到的工具类,定义了:大于,小于等操作
rangeBounds 保存分区数的,带上限的数组
numPartitions 该RDD的分区数量。
binarySearch 获取一个二分查找的对象。由于key是可排序的,所以使用二分加快查询性能。
getPartition 获取分区对应的索引id

自定义分区器(Partitioner)

通过继承Partitioner抽象类,可以定制自己的分区器。要定义自己的分区器可能需要实现以下函数:

成员名 说明
numPartitions 分区数
getPartition key作为参数(与被分区的RDD类型相同),返回表示分区索引的整数,该分区指定具有该key的记录所在的位置。 返回的整数必须介于零和分区数之间(在numPartitions定义)。
equals 定义两个分区是否相等的方法。
hashcode 仅当重写了equals方法时才需要此方法。 HashPartitioner的hashcode就是它的分区数。 RangePartitioner的hashcode是从范围边界派生的哈希函数。

总结

本文介绍了RDD的Partitioner原理,并对其实现进行了简要分析。

猜你喜欢

转载自blog.csdn.net/zg_hover/article/details/83511860