RDD(弹性分布式数据集)是Spark中最基本的抽象数据类型,具有以下五个主要特性:
(1)分区列表(a list of partitions)。
Spark RDD是被分区的,每一个分区都会被一个计算任务(Task)处理,分区数决定并行计算数量,RDD的并行度默认从父RDD传给子RDD。默认情况下,一个HDFS上的数据分片就是一个Partition,RDD分片数决定了并行计算的力度,可以在创建RDD时指定RDD分片个数,如果不指定分区数量,当RDD从集合创建时,则默认分区数量为该程序所分配到的资源的CPU核数(每个Core可以承载2~4个Partition),如果是从HDFS文件创建,默认为文件的Block数。
(2)每一个分区都有一个计算函数(a function for computing each split)。
每个分区都会有计算函数,Spark的RDD的计算函数是以分片为基本单位的,每个RDD都会实现compute函数,对具体的分片进行计算,RDD中的分片是并行的,所以是分布式并行计算。有一点非常重要,就是由于RDD有前后依赖关系,遇到宽依赖关系,例如,遇到reduceBykey等宽依赖操作的算子,Spark将根据宽依赖划分Stage,Stage内部通过Pipeline操作,通过Block Manager获取相关的数据,因为具体的split要从外界读数据,也要把具体的计算结果写入外界,所以用了一个管理器,具体的split都会映射成BlockManager的Block,而具体split会被函数处理,函数处理的具体形式是以任务的形式进行的。
(3)依赖于其他RDD的列表(a list of dependencies on other RDDs)。
RDD的依赖关系,由于RDD每次转换都会生成新的RDD,所以RDD会形成类似流水线的前后依赖关系,当然,宽依赖就不类似于流水线了,宽依赖后面的RDD具体的数据分片会依赖前面所有的RDD的所有的数据分片,这时数据分片就不进行内存中的Pipeline,这时一般是跨机器的。因为有前后的依赖关系,所以当有分区数据丢失的时候,Spark会通过依赖关系重新计算,算出丢失的数据,而不是对RDD所有的分区进行重新计算。RDD之间的依赖有两种:窄依赖(Narrow Dependency)、宽依赖(Wide Dependency)。RDD是Spark的核心数据结构,通过RDD的依赖关系形成调度关系。通过对RDD的操作形成整个Spark程序。
RDD有Narrow Dependency和Wide Dependency两种不同类型的依赖,其中的Narrow Dependency指的是每一个parent RDD的Partition最多被child RDD的一个Partition所使用,而Wide Dependency指的是多个child RDD的Partition会依赖于同一个parent RDD的Partition。可以从两个方面来理解RDD之间的依赖关系:一方面是该RDD的parent RDD是什么;另一方面是依赖于parent RDD的哪些Partitions;根据依赖于parent RDD的Partitions的不同情况,Spark将Dependency分为宽依赖和窄依赖两种。Spark中宽依赖指的是生成的RDD的每一个partition都依赖于父RDD的所有partition,宽依赖典型的操作有groupByKey、sortByKey等,宽依赖意味着shuffle操作,这是Spark划分Stage边界的依据,Spark中宽依赖支持两种Shuffle Manager,即HashShuffleManager和SortShuffleManager,前者是基于Hash的Shuffle机制,后者是基于排序的Shuffle机制。Spark 2.2现在的版本中已经没有Hash Shuffle的方式。
(4)key-value数据类型的RDD有一个分区器(Optionally,a Partitioner for key-value RDDS),控制分区策略和分区数。
每个key-value形式的RDD都有Partitioner属性,它决定了RDD如何分区。当然,Partition的个数还决定每个Stage的Task个数。RDD的分片函数,想控制RDD的分片函数的时候可以分区(Partitioner)传入相关的参数,如HashPartitioner、RangePartitioner,它本身针对key-value的形式,如果不是key-value的形式,它就不会有具体的Partitioner。Partitioner本身决定了下一步会产生多少并行的分片,同时,它本身也决定了当前并行(parallelize)Shuffle输出的并行数据,从而使Spark具有能够控制数据在不同节点上分区的特性,用户可以自定义分区策略,如Hash分区等。Spark提供了“partitionBy”运算符,能通过集群对RDD进行数据再分配来创建一个新的RDD。
(5)每个分区都有一个优先位置列表(-Optionally,a list of preferred locations to compute each split on)。
它会存储每个Partition的优先位置,对于一个HDFS文件来说,就是每个Partition块的位置。观察运行spark集群的控制台会发现Spark的具体计算,具体分片前,它已经清楚地知道任务发生在什么节点上,也就是说,任务本身是计算层面的、代码层面的,代码发生运算之前已经知道它要运算的数据在什么地方**(在哪一台机器上)**,有具体节点的信息。这就符合大数据中数据不动代码动的特点。数据不动代码动的最高境界是数据就在当前节点的内存中。这时有可能是memory级别或Alluxio级别的,Spark本身在进行任务调度时候,会尽可能将任务分配到处理数据的数据块所在的具体位置。据Spark的RDD.Scala源码函数getPreferredLocations可知,每次计算都符合完美的数据本地性。
package org.example.spark
import org.apache.spark.{SparkConf, SparkContext}
object RDDDemo {
def main(args: Array[String]): Unit = {
// 创建SparkConf对象并设置应用程序名称
val conf = new SparkConf().setAppName("RDD Demo").setMaster("local[*]")
// 创建SparkContext对象
val sc = new SparkContext(conf)
// 1. 分区 - A list of partitions
val data = sc.parallelize(Seq(1, 2, 3, 4, 5), 2) // 将数据分为两个分区
println("Partitions: " + data.getNumPartitions)
// Partitions: 2
// 2. 每一个分区都有一个计算函数 - A function for computing each split
val transformedData = data.map(_ * 2) // 对每个元素进行转换操作,生成新的RDD
println("Compute Function: " + transformedData.toDebugString)
// Compute Function: (2) MapPartitionsRDD[1] at map at RDDDemo.scala:17 []
// | ParallelCollectionRDD[0] at parallelize at RDDDemo.scala:14 []
// 3. 依赖于其他RDD的列表(a list of dependencies on other RDDs)
val otherData = sc.parallelize(Seq(6, 7, 8, 9, 10))
val combinedData = transformedData.union(otherData) // 将两个RDD合并成一个新的RDD
println("otherData Dependencies: " + otherData.dependencies)
println("combinedData Dependencies: " + combinedData.dependencies)
// otherData Dependencies: List()
// combinedData Dependencies: ArrayBuffer(org.apache.spark.RangeDependency@57b75756, org.apache.spark.RangeDependency@5327a06e)
// 4. 可选项:key-value数据类型的RDD有个分区器 Partitioner for key-value RDDs
val keyValueData = combinedData.keyBy(_.%(2)).partitionBy(new org.apache.spark.HashPartitioner(3)) // 将RDD中的元素以奇偶数为键进行分组
println("Partitioner: " + keyValueData.partitioner)
// Partitioner: Some(org.apache.spark.HashPartitioner@3)
// 5. 可选项:每个分区都有一个优先位置列表 Preferred locations to compute each split
val splitLocations = data.preferredLocations(data.partitions(0)) // 获取第一个分区的首选计算位置
println("Preferred Locations: " + splitLocations.mkString(", "))
// Preferred Locations:
// 关闭SparkContext对象
sc.stop()
}
}
/**
* Resilient Distributed Dataset(RDD)是Spark中的基本抽象概念。它代表一个不可变、分区的元素集合,可以并行操作。
* 这个类包含了所有RDD都可用的基本操作,比如`map`、`filter`和`persist`。另外,[[org.apache.spark.rdd.PairRDDFunctions]]
* 包含了仅适用于键值对RDD的操作,比如`groupByKey`和`join`;
* [[org.apache.spark.rdd.DoubleRDDFunctions]]包含了仅适用于Double类型RDD的操作;而
* [[org.apache.spark.rdd.SequenceFileRDDFunctions]]包含了可以保存为SequenceFiles的RDD的操作。
* 所有这些操作在正确类型的RDD上都会自动可用(例如,RDD[(Int, Int)]),通过隐式转换实现。
*
* 在内部,每个RDD由五个主要属性描述:
*
* - 分区列表
* - 用于计算每个分区的函数
* - 对其他RDD的依赖列表
* - 可选的键值对RDD的Partitioner(例如,指明RDD是哈希分区的)
* - 可选的计算每个分区的首选位置列表(例如,HDFS文件的块位置)
*
* Spark中的调度和执行都是基于这些方法完成的,允许每个RDD实现自己的计算方式。
* 实际上,用户可以通过重写这些函数来实现自定义的RDD(例如,用于从新的存储系统读取数据)。
* 有关RDD内部的更多细节,请参阅Spark论文。
*/
abstract class RDD[T: ClassTag](
@transient private var _sc: SparkContext,
@transient private var deps: Seq[Dependency[_]]
) extends Serializable with Logging {
/**
* 由子类实现,返回此RDD中的分区集合。这个方法只会被调用一次,因此可以在其中实现耗时的计算。
*
* 此数组中的分区必须满足以下属性:
* `rdd.partitions.zipWithIndex.forall { case (partition, index) => partition.index == index }`
*/
protected def getPartitions: Array[Partition]
/**
* 由子类实现,返回此RDD对父RDD的依赖关系。这个方法只会被调用一次,因此可以在其中实现耗时的计算。
*/
protected def getDependencies: Seq[Dependency[_]] = deps
/**
* 可以由子类选择性地重写,指定位置偏好。
*/
protected def getPreferredLocations(split: Partition): Seq[String] =
Nil // 默认情况下没有位置偏好
1)关系:
两者都是用来改变RDD的partition数量的,repartition底层调用的就是coalesce方法:coalesce(numPartitions, shuffle = true)
2)区别:
repartition一定会发生shuffle,coalesce 根据传入的参数来判断是否发生shuffle。
一般情况下增大rdd的partition数量使用repartition,减少partition数量时使用coalesce。
综上所述,需要根据具体的需求和数据情况来选择使用哪个算子。如果需要增加分区数、进行全量洗牌或调整数据分布,可以使用repartition()
;如果需要减少分区数、避免全量洗牌开销或简单调整数据分布,可以使用coalesce()
。
/**
* 返回一个具有恰好numPartitions个分区的新RDD。
*
* 可以增加或减少此RDD中的并行级别。在内部,这使用shuffle操作重新分发数据。
*
* 如果你要减少此RDD中的分区数,请考虑使用`coalesce`,它可以避免执行shuffle操作。
*
* TODO 修复SPARK-23207中描述的Shuffle+Repartition数据丢失问题。
*/
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
coalesce(numPartitions, shuffle = true)
}
/**
* 返回一个新的RDD,它被合并为`numPartitions`个分区。
*
* 这会导致一个窄依赖关系,例如如果从1000个分区缩减到100个分区,
* 将不会有shuffle操作,而是每个新分区将占用10个当前分区。
* 如果请求更大数量的分区,则会保持当前分区数不变。
*
* 然而,如果你进行了激进的合并,例如numPartitions = 1,
* 这可能导致你的计算在比你想要的更少的节点上执行(例如,在numPartitions = 1的情况下只有一个节点)。
* 为了避免这种情况,你可以传递shuffle = true。这将添加一个shuffle步骤,
* 但意味着当前的上游分区将并行执行(根据当前的分区方式)。
*
* @note 使用shuffle = true时,实际上可以合并到更多的分区。
* 这在有少量分区(例如100个)且其中一些分区异常大的情况下非常有用。
* 调用coalesce(1000, shuffle = true)将导致使用哈希分区器分发数据的1000个分区。
* 传入的可选分区合并器必须是可序列化的。
*/
def coalesce(numPartitions: Int, shuffle: Boolean = false,
partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
(implicit ord: Ordering[T] = null)
: RDD[T] = withScope {
require(numPartitions > 0, s"分区数($numPartitions)必须为正数。")
if (shuffle) {
/** 在输出分区上均匀分布元素,从一个随机分区开始。 */
val distributePartition = (index: Int, items: Iterator[T]) => {
var position = new Random(hashing.byteswap32(index)).nextInt(numPartitions)
items.map { t =>
// 注意,键的哈希码将是键本身。HashPartitioner将使用总分区数对其进行取模。
position = position + 1
(position, t)
}
} : Iterator[(Int, T)]
// 包含一个shuffle步骤,以便我们的上游任务仍然是分布式的
new CoalescedRDD(
new ShuffledRDD[Int, T, T](
mapPartitionsWithIndexInternal(distributePartition, isOrderSensitive = true),
new HashPartitioner(numPartitions)),
numPartitions,
partitionCoalescer).values
} else {
new CoalescedRDD(this, numPartitions, partitionCoalescer)
}
}
/**
* 表示一个具有比其父RDD更少分区的合并RDD
* 此类使用PartitionCoalescer类来找到父RDD的良好分区,
* 以便每个新分区大致具有相同数量的父分区,并且每个新分区的首选位置与其父分区的尽可能多的首选位置重叠
* @param prev 要合并的RDD
* @param maxPartitions 合并后的RDD中所需分区的数量(必须为正数)
* @param partitionCoalescer 用于合并的[[PartitionCoalescer]]实现
*/
private[spark] class CoalescedRDD[T: ClassTag](
@transient var prev: RDD[T],
maxPartitions: Int,
partitionCoalescer: Option[PartitionCoalescer] = None)
extends RDD[T](prev.context, Nil) { // Nil表示我们实现了getDependencies
require(maxPartitions > 0 || maxPartitions == prev.partitions.length,
s"分区数($maxPartitions)必须为正数。")
if (partitionCoalescer.isDefined) {
require(partitionCoalescer.get.isInstanceOf[Serializable],
"传入的分区合并器必须可序列化。")
}
override def getPartitions: Array[Partition] = {
val pc = partitionCoalescer.getOrElse(new DefaultPartitionCoalescer())
pc.coalesce(maxPartitions, prev).zipWithIndex.map {
case (pg, i) =>
val ids = pg.partitions.map(_.index).toArray
new CoalescedRDDPartition(i, prev, ids, pg.prefLoc)
}
}
override def compute(partition: Partition, context: TaskContext): Iterator[T] = {
partition.asInstanceOf[CoalescedRDDPartition].parents.iterator.flatMap { parentPartition =>
firstParent[T].iterator(parentPartition, context)
}
}
override def getDependencies: Seq[Dependency[_]] = {
Seq(new NarrowDependency(prev) {
def getParents(id: Int): Seq[Int] =
partitions(id).asInstanceOf[CoalescedRDDPartition].parentsIndices
})
}
override def clearDependencies() {
super.clearDependencies()
prev = null
}
/**
* 返回分区的首选机器。如果分区的类型是CoalescedRDDPartition,
* 则首选机器将是大多数父分区也喜欢的机器。
* @param partition
* @return split最喜欢的机器
*/
override def getPreferredLocations(partition: Partition): Seq[String] = {
partition.asInstanceOf[CoalescedRDDPartition].preferredLocation.toSeq
}
}
reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]。
groupByKey:按照key进行分组,直接进行shuffle
所以,在实际开发过程中,reduceByKey比groupByKey,更建议使用。但是需要注意是否会影响业务逻辑。
reduceByKey和reduceByKey都使用combineByKeyWithClassTag函数。combineByKeyWithClassTag有个一个参数mapSideCombine是否map阶段预聚合每个节点上的数据,reduceByKey中使用的true,groupByKey则是false。
/**
* 将RDD中每个键的值分组成单个序列。允许通过传递Partitioner来控制结果键值对RDD的分区。
* 每个组内元素的顺序不能保证,并且甚至在每次评估结果RDD时可能会有所不同。
*
* @note 此操作可能非常昂贵。如果您要对每个键执行聚合(例如求和或平均值),使用`PairRDDFunctions.aggregateByKey`
* 或`PairRDDFunctions.reduceByKey`将提供更好的性能。
*
* @note 目前实现的groupByKey必须能够在内存中保存所有键值对的所有值。如果一个键有太多的值,可能会导致`OutOfMemoryError`。
*/
def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])] = self.withScope {
// groupByKey不应该使用map side combine,因为map side combine不会减少洗牌的数据量,
// 并且需要将所有的map side数据插入到哈希表中,导致老年代中的对象更多。
val createCombiner = (v: V) => CompactBuffer(v)
val mergeValue = (buf: CompactBuffer[V], v: V) => buf += v
val mergeCombiners = (c1: CompactBuffer[V], c2: CompactBuffer[V]) => c1 ++= c2
val bufs = combineByKeyWithClassTag[CompactBuffer[V]](
createCombiner, mergeValue, mergeCombiners, partitioner, mapSideCombine = false)
bufs.asInstanceOf[RDD[(K, Iterable[V])]]
}
/**
* 使用关联和可交换的reduce函数合并每个键的值。
* 这也会在每个mapper上本地执行合并,然后将结果发送给reducer,类似于MapReduce中的“combiner”。
*/
def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = self.withScope {
combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
}
/**
* 使用关联和可交换的reduce函数合并每个键的值。
* 这也会在每个mapper上本地执行合并,然后将结果发送给reducer,类似于MapReduce中的“combiner”。
* 输出将使用numPartitions个分区进行哈希分区。
*/
def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)] = self.withScope {
reduceByKey(new HashPartitioner(numPartitions), func)
}
/**
* 使用关联和可交换的reduce函数合并每个键的值。
* 这也会在每个mapper上本地执行合并,然后将结果发送给reducer,类似于MapReduce中的“combiner”。
* 输出将使用现有的分区器/并行度级别进行哈希分区。
*/
def reduceByKey(func: (V, V) => V): RDD[(K, V)] = self.withScope {
reduceByKey(defaultPartitioner(self), func)
}
/**
* 通用函数,根据自定义的聚合函数来合并每个键的元素。将RDD[(K, V)]转换为类型为RDD[(K, C)]的结果,
* 其中C为“combined type”。
*
* 用户需要提供三个函数:
*
* - `createCombiner`,将V转换为C(例如,创建一个包含单个元素的列表)
* - `mergeValue`,将V合并到C中(例如,将其添加到列表的末尾)
* - `mergeCombiners`,将两个C合并为一个C
*
* 此外,用户还可以控制输出RDD的分区方式,并决定是否执行map-side聚合(如果一个mapper可以生成具有相同键的多个项)。
*
* 注意:V和C可以是不同的类型 - 例如,可以将(Int, Int)类型的RDD分组为(Int, Seq[Int])类型的RDD。
*/
@Experimental
def combineByKeyWithClassTag[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C,
partitioner: Partitioner,
mapSideCombine: Boolean = true,
serializer: Serializer = null)(implicit ct: ClassTag[C]): RDD[(K, C)] = self.withScope {
require(mergeCombiners != null, "mergeCombiners must be defined") // required as of Spark 0.9.0
if (keyClass.isArray) {
if (mapSideCombine) {
throw new SparkException("Cannot use map-side combining with array keys.")
}
if (partitioner.isInstanceOf[HashPartitioner]) {
throw new SparkException("HashPartitioner cannot partition array keys.")
}
}
val aggregator = new Aggregator[K, V, C](
self.context.clean(createCombiner),
self.context.clean(mergeValue),
self.context.clean(mergeCombiners))
if (self.partitioner == Some(partitioner)) {
self.mapPartitions(iter => {
val context = TaskContext.get()
new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, context))
}, preservesPartitioning = true)
} else {
new ShuffledRDD[K, V, C](self, partitioner)
.setSerializer(serializer)
.setAggregator(aggregator)
.setMapSideCombine(mapSideCombine)
}
}
ShuffledRDD是Spark中的一个RDD类型,它是由转换操作(例如groupByKey、reduceByKey等)引起的数据重分区(shuffle)过程产生的结果。当执行需要对数据进行聚合或排序等操作时,Spark会根据键(key)对数据进行重分区,并将相同键的数据集中在同一个分区中。这个过程称为shuffle。ShuffledRDD就是通过shuffle操作生成的一个RDD类型。
private[spark] class ShuffledRDDPartition(val idx: Int) extends Partition {
override val index: Int = idx
}
/**
* :: DeveloperApi ::
* Shuffle操作产生的结果RDD(例如,数据的重新分区)。
* @param prev 父RDD。
* @param part 用于对RDD进行分区的分区器。
* @tparam K 键的类型。
* @tparam V 值的类型。
* @tparam C 组合器的类型。
*/
// TODO: 让它返回RDD[Product2[K, C]]或者有一种方式来配置可变的键值对
@DeveloperApi
class ShuffledRDD[K: ClassTag, V: ClassTag, C: ClassTag](
@transient var prev: RDD[_ <: Product2[K, V]],
part: Partitioner)
extends RDD[(K, C)](prev.context, Nil) {
private var userSpecifiedSerializer: Option[Serializer] = None
private var keyOrdering: Option[Ordering[K]] = None
private var aggregator: Option[Aggregator[K, V, C]] = None
private var mapSideCombine: Boolean = false
/** 为此RDD的Shuffle设置一个序列化器,如果为null则使用默认值(spark.serializer)。 */
def setSerializer(serializer: Serializer): ShuffledRDD[K, V, C] = {
this.userSpecifiedSerializer = Option(serializer)
this
}
/** 设置RDD的Shuffle的键排序。 */
def setKeyOrdering(keyOrdering: Ordering[K]): ShuffledRDD[K, V, C] = {
this.keyOrdering = Option(keyOrdering)
this
}
/** 设置RDD的Shuffle的组合器。 */
def setAggregator(aggregator: Aggregator[K, V, C]): ShuffledRDD[K, V, C] = {
this.aggregator = Option(aggregator)
this
}
/** 设置RDD的Shuffle的mapSideCombine标志。 */
def setMapSideCombine(mapSideCombine: Boolean): ShuffledRDD[K, V, C] = {
this.mapSideCombine = mapSideCombine
this
}
override def getDependencies: Seq[Dependency[_]] = {
val serializer = userSpecifiedSerializer.getOrElse {
val serializerManager = SparkEnv.get.serializerManager
if (mapSideCombine) {
serializerManager.getSerializer(implicitly[ClassTag[K]], implicitly[ClassTag[C]])
} else {
serializerManager.getSerializer(implicitly[ClassTag[K]], implicitly[ClassTag[V]])
}
}
List(new ShuffleDependency(prev, part, serializer, keyOrdering, aggregator, mapSideCombine))
}
override val partitioner = Some(part)
override def getPartitions: Array[Partition] = {
Array.tabulate[Partition](part.numPartitions)(i => new ShuffledRDDPartition(i))
}
override protected def getPreferredLocations(partition: Partition): Seq[String] = {
val tracker = SparkEnv.get.mapOutputTracker.asInstanceOf[MapOutputTrackerMaster]
val dep = dependencies.head.asInstanceOf[ShuffleDependency[K, V, C]]
tracker.getPreferredLocationsForShuffle(dep, partition.index)
}
override def compute(split: Partition, context: TaskContext): Iterator[(K, C)] = {
val dep = dependencies.head.asInstanceOf[ShuffleDependency[K, V, C]]
SparkEnv.get.shuffleManager.getReader(dep.shuffleHandle, split.index, split.index + 1, context)
.read()
.asInstanceOf[Iterator[(K, C)]]
}
override def clearDependencies() {
super.clearDependencies()
prev = null
}
private[spark] override def isBarrier(): Boolean = false
}
/**
* 使用Java的`Object.hashCode`实现基于哈希的分区的[[org.apache.spark.Partitioner]]。
*
* Java数组的hashCode是基于数组的标识而不是内容的,因此尝试使用HashPartitioner对RDD[Array[_]]或RDD[(Array[_], _)]进行分区将产生意外或错误的结果。
*/
class HashPartitioner(partitions: Int) extends Partitioner {
require(partitions >= 0, s"分区数($partitions)不能为负数。")
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
}
/**
* 为类似于cogroup的操作选择要使用的分区器。
*
* 如果设置了spark.default.parallelism,我们将使用SparkContext defaultParallelism的值作为默认的分区数,否则我们将使用上游分区的最大数量。
*
* 当可用时,我们从具有最大分区数的rdds中选择分区器。如果此分区器是合适的(分区数与rdds中的最大分区数相差不多),或者分区数高于默认的分区数 - 我们将使用该分区器。
*
* 否则,我们将使用具有默认分区数的新HashPartitioner。
*
* 除非设置了spark.default.parallelism,否则分区数将与最大上游RDD中的分区数相同,因为这应该是最不可能导致内存错误的情况。
*
* 我们使用两个方法参数(rdd, others)来强制调用者至少传递1个RDD。
*/
def defaultPartitioner(rdd: RDD[_], others: RDD[_]*): Partitioner = {
val rdds = (Seq(rdd) ++ others)
val hasPartitioner = rdds.filter(_.partitioner.exists(_.numPartitions > 0))
val hasMaxPartitioner: Option[RDD[_]] = if (hasPartitioner.nonEmpty) {
Some(hasPartitioner.maxBy(_.partitions.length))
} else {
None
}
val defaultNumPartitions = if (rdd.context.conf.contains("spark.default.parallelism")) {
rdd.context.defaultParallelism
} else {
rdds.map(_.partitions.length).max
}
// 如果现有的最大分区器是合适的,或者其分区数大于默认的分区数,则使用现有的分区器。
if (hasMaxPartitioner.nonEmpty && (isEligiblePartitioner(hasMaxPartitioner.get, rdds) ||
defaultNumPartitions < hasMaxPartitioner.get.getNumPartitions)) {
hasMaxPartitioner.get.partitioner.get
} else {
new HashPartitioner(defaultNumPartitions)
}
}
一个类似于ArrayBuffer的追加缓冲区,但对于小型缓冲区更节省内存。
/**
* 一个类似于ArrayBuffer的追加缓冲区,但对于小型缓冲区更节省内存。
* ArrayBuffer总是分配一个对象数组来存储数据,默认情况下有16个条目,因此它的开销约为80-100字节。
* 相反,CompactBuffer可以在主对象的字段中保留最多两个元素,并且只有在超过这些元素时才分配一个Array[AnyRef]。
* 这使得它在像groupBy这样的操作中更高效,我们期望一些键具有非常少的元素。
*/
private[spark] class CompactBuffer[T: ClassTag] extends Seq[T] with Serializable {
// First two elements
private var element0: T = _
private var element1: T = _
// Number of elements, including our two in the main object
private var curSize = 0
// Array for extra elements
private var otherElements: Array[T] = null
/**
* 获取指定位置的元素
* @param position 元素的位置
* @return 指定位置的元素值
*/
def apply(position: Int): T = {
if (position < 0 || position >= curSize) {
throw new IndexOutOfBoundsException
}
if (position == 0) {
element0
} else if (position == 1) {
element1
} else {
otherElements(position - 2)
}
}
/**
* 更新指定位置的元素值
* @param position 元素的位置
* @param value 新的元素值
*/
private def update(position: Int, value: T): Unit = {
if (position < 0 || position >= curSize) {
throw new IndexOutOfBoundsException
}
if (position == 0) {
element0 = value
} else if (position == 1) {
element1 = value
} else {
otherElements(position - 2) = value
}
}
/**
* 向缓冲区追加一个元素
* @param value 要追加的元素
* @return 当前的CompactBuffer对象
*/
def += (value: T): CompactBuffer[T] = {
val newIndex = curSize
if (newIndex == 0) {
element0 = value
curSize = 1
} else if (newIndex == 1) {
element1 = value
curSize = 2
} else {
growToSize(curSize + 1)
otherElements(newIndex - 2) = value
}
this
}
/**
* 向缓冲区追加多个元素
* @param values 要追加的元素集合
* @return 当前的CompactBuffer对象
*/
def ++= (values: TraversableOnce[T]): CompactBuffer[T] = {
values match {
// 优化CompactBuffer的合并,用于cogroup和groupByKey
case compactBuf: CompactBuffer[T] =>
val oldSize = curSize
// 复制其他缓冲区的大小和元素到本地变量,以防它与我们相等
val itsSize = compactBuf.curSize
val itsElements = compactBuf.otherElements
growToSize(curSize + itsSize)
if (itsSize == 1) {
this(oldSize) = compactBuf.element0
} else if (itsSize == 2) {
this(oldSize) = compactBuf.element0
this(oldSize + 1) = compactBuf.element1
} else if (itsSize > 2) {
this(oldSize) = compactBuf.element0
this(oldSize + 1) = compactBuf.element1
// 此时我们的大小也大于2,所以直接将其数组复制到我们的数组中。
// 注意,由于我们在上面添加了两个元素,我们应该将其复制到this.otherElements中的索引为oldSize的位置。
System.arraycopy(itsElements, 0, otherElements, oldSize, itsSize - 2)
}
case _ =>
values.foreach(e => this += e)
}
this
}
override def length: Int = curSize
override def size: Int = curSize
override def iterator: Iterator[T] = new Iterator[T] {
private var pos = 0
override def hasNext: Boolean = pos < curSize
override def next(): T = {
if (!hasNext) {
throw new NoSuchElementException
}
pos += 1
apply(pos - 1)
}
}
/**
* 将缓冲区的大小增加到newSize,并在需要时扩展支持数组
* @param newSize 新的缓冲区大小
*/
private def growToSize(newSize: Int): Unit = {
// 由于两个字段存储在element0和element1中,一个数组存储newSize-2个元素
val newArraySize = newSize - 2
val arrayMax = ByteArrayMethods.MAX_ROUNDED_ARRAY_LENGTH
if (newSize < 0 || newArraySize > arrayMax) {
throw new UnsupportedOperationException(s"无法将缓冲区扩展到超过$arrayMax个元素")
}
val capacity = if (otherElements != null) otherElements.length else 0
if (newArraySize > capacity) {
var newArrayLen = 8L
while (newArraySize > newArrayLen) {
newArrayLen *= 2
}
if (newArrayLen > arrayMax) {
newArrayLen = arrayMax
}
val newArray = new Array[T](newArrayLen.toInt)
if (otherElements != null) {
System.arraycopy(otherElements, 0, newArray, 0, otherElements.length)
}
otherElements = newArray
}
curSize = newSize
}
}
private[spark] object CompactBuffer {
def apply[T: ClassTag](): CompactBuffer[T] = new CompactBuffer[T]
def apply[T: ClassTag](value: T): CompactBuffer[T] = {
val buf = new CompactBuffer[T]
buf += value
}
}
在Spark中,Action是一种触发RDD计算并返回结果的操作。当我们调用Action操作时,Spark会对RDD执行计算,并将结果返回给驱动程序或将结果写入外部存储系统。
下面是几个常见的Spark Action操作的定义和说明:
collect()
: 将整个RDD的数据收集到驱动程序中,以本地数组的形式返回。count()
: 返回RDD中的元素数量。first()
: 返回RDD中的第一个元素。take(n: Int)
: 返回RDD中的前n个元素。reduce(func: (T, T) => T)
: 使用指定的二元操作函数对RDD中的元素进行聚合,返回一个单一的值。foreach(func: T => Unit)
: 对RDD中的每个元素应用指定的函数。saveAsTextFile(path: String)
: 将RDD的内容保存为文本文件。saveAsObjectFile(path: String)
: 将RDD的内容保存为序列化的对象文件。saveAsSequenceFile(path: String)
: 将RDD的内容保存为Hadoop序列文件。countByKey()
: 对(K, V)类型的RDD执行计数操作,返回一个包含每个键和其对应的出现次数的Map。countByValue()
: 对T类型的RDD执行计数操作,返回一个包含每个元素和其对应的出现次数的Map。aggregate(zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U)
: 使用指定的初始值、序列操作函数和组合操作函数对RDD中的元素进行聚合。这些是Spark中一些常用的Action操作,你可以根据具体的需求选择适当的操作来触发RDD计算并获取结果。
package org.example.spark
import org.apache.spark.{SparkConf, SparkContext}
import java.util.UUID
object ActionDemo extends App{
// 创建SparkContext
val conf = new SparkConf().setAppName("ActionDemo").setMaster("local[*]")
val sc = new SparkContext(conf)
// 创建一个包含整数的RDD
val data = sc.parallelize(Seq(1, 2, 3, 4, 5))
// collect() - 将整个RDD的数据收集到驱动程序中,以本地数组的形式返回
val collectedData = data.collect()
println("Collected Data: " + collectedData.mkString(", "))
// count() - 返回RDD中的元素数量
val count = data.count()
println("Count: " + count)
// first() - 返回RDD中的第一个元素
val firstElement = data.first()
println("First Element: " + firstElement)
// take(n: Int) - 返回RDD中的前n个元素
val nElements = data.take(3)
println("First 3 Elements: " + nElements.mkString(", "))
// reduce(func: (T, T) => T) - 使用指定的二元操作函数对RDD中的元素进行聚合,返回一个单一的值
val sum = data.reduce((x, y) => x + y)
println("Sum: " + sum)
// foreach(func: T => Unit) - 对RDD中的每个元素应用指定的函数
data.foreach(x => println("Element: " + x))
// aggregate(zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U) - 使用指定的初始值、序列操作函数和组合操作函数对RDD中的元素进行聚合
val initialValue = 0
val sumAggregate = data.aggregate(initialValue)((acc, value) => acc + value, (acc1, acc2) => acc1 + acc2)
println("Sum Aggregate: " + sumAggregate)
// saveAsTextFile(path: String) - 将RDD的内容保存为文本文件
data.saveAsTextFile(UUID.randomUUID()+"output")
// saveAsObjectFile(path: String) - 将RDD的内容保存为序列化的对象文件
data.saveAsObjectFile(UUID.randomUUID()+"output")
// saveAsSequenceFile(path: String) - 将RDD的内容保存为Hadoop序列文件
data.map(x => (x, x * 2)).saveAsSequenceFile(UUID.randomUUID()+"output")
// countByKey() - 对(K, V)类型的RDD执行计数操作,返回一个包含每个键和其对应的出现次数的Map
val keyValueData = sc.parallelize(Seq(("A", 1), ("B", 2), ("C", 3), ("A", 4), ("B", 5)))
val countByKey = keyValueData.countByKey()
println("Count by Key: " + countByKey)
// countByValue() - 对T类型的RDD执行计数操作,返回一个包含每个元素和其对应的出现次数的Map
val countByValue = data.countByValue()
println("Count by Value: " + countByValue)
// 关闭SparkContext
sc.stop()
}
//Collected Data: 1, 2, 3, 4, 5
//Count: 5
//First Element: 1
//First 3 Elements: 1, 2, 3
//Sum: 15
//Element: 4
//Element: 3
//Element: 1
//Element: 5
//Element: 2
//Sum Aggregate: 15
所有的action行动算子都会调用 runJob方法
/**
* 返回一个包含此RDD中所有元素的数组。
*
* 注意:只有当预期结果数组较小且所有数据都加载到驱动程序的内存中时,才应使用此方法。
*/
def collect(): Array[T] = withScope {
val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
Array.concat(results: _*)
}
/**
* 在RDD的所有分区上运行一个作业,并将结果以数组的形式返回。
*
* @param rdd 目标RDD,用于在其上运行任务
* @param func 在RDD的每个分区上运行的函数
* @return 内存中的集合,包含作业的结果(每个集合元素将包含来自一个分区的结果)
*/
def runJob[T, U: ClassTag](rdd: RDD[T], func: Iterator[T] => U): Array[U] = {
runJob(rdd, func, 0 until rdd.partitions.length)
}
/**
* 在RDD的给定一组分区上运行函数,并将结果作为数组返回。
*
* @param rdd 目标RDD,用于在其上运行任务
* @param func 在RDD的每个分区上运行的函数
* @param partitions 要运行的分区集合;某些作业可能不希望在目标RDD的所有分区上计算,
* 例如对于`first()`等操作
* @return 内存中的集合,包含作业的结果(每个集合元素将包含来自一个分区的结果)
*/
def runJob[T, U: ClassTag](
rdd: RDD[T],
func: Iterator[T] => U,
partitions: Seq[Int]): Array[U] = {
val cleanedFunc = clean(func)
runJob(rdd, (ctx: TaskContext, it: Iterator[T]) => cleanedFunc(it), partitions)
}
/**
* 在RDD的给定一组分区上运行函数,并将结果传递给给定的处理函数。这是Spark中所有action操作的主要入口点。
*
* @param rdd 目标RDD,用于在其上运行任务
* @param func 在RDD的每个分区上运行的函数
* @param partitions 要运行的分区集合;某些作业可能不希望在目标RDD的所有分区上计算,
* 例如对于`first()`等操作
* @param resultHandler 将每个结果传递给的回调函数
*/
def runJob[T, U: ClassTag](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
partitions: Seq[Int],
resultHandler: (Int, U) => Unit): Unit = {
if (stopped.get()) {
throw new IllegalStateException("SparkContext has been shutdown")
}
val callSite = getCallSite
val cleanedFunc = clean(func)
logInfo("Starting job: " + callSite.shortForm)
if (conf.getBoolean("spark.logLineage", false)) {
logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
}
dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
progressBar.foreach(_.finishAll())
rdd.doCheckpoint()
}
在Spark中,Transformation算子是惰性求值的。这意味着当我们调用Transformation算子时,并不会立即执行计算,而是记录下操作的转换步骤以构建一个执行计划。
只有当遇到一个Action算子时,Spark才会触发实际的计算。Action算子会触发Spark执行之前定义的Transformation算子,并将结果返回给驱动程序或将结果写入外部系统。
通过延迟计算,Spark能够优化执行计划、自动进行数据分区和任务调度,并提供更高效的数据处理。此外,惰性求值还允许我们构建更复杂的数据流水线,并根据需要动态调整计算步骤。
需要注意的是,在使用Transformation算子时,我们应该避免对数据进行频繁的缓存操作,因为这可能会导致内存消耗过大。相反,我们可以通过合理地选择缓存点来减少计算开销,以及使用持久化机制(如checkpointing)来确保数据的可靠性和容错性。
总结起来,Spark中的Transformation算子采用了惰性求值的策略,通过构建执行计划并在遇到Action算子时进行实际计算,以提高计算效率和灵活性。
map(func: T => U): RDD[U]
:对RDD中的每个元素应用给定的函数,返回一个新的RDD,其中包含应用函数后的结果。filter(func: T => Boolean): RDD[T]
:根据给定的条件过滤出符合要求的元素,返回一个新的RDD。flatMap(func: T => TraversableOnce[U]): RDD[U]
:对RDD中的每个元素应用给定的函数,并将函数返回的所有元素展平为一个新的RDD。distinct(numPartitions: Int): RDD[T]
:返回一个去重后的新的RDD。union(other: RDD[T]): RDD[T]
:将当前RDD与另一个RDD进行合并,返回一个包含两个RDD中所有元素的新的RDD。intersection(other: RDD[T]): RDD[T]
:返回两个RDD的交集,即包含两个RDD中都存在的元素的新的RDD。subtract(other: RDD[T]): RDD[T]
:返回一个新的RDD,其中包含当前RDD中存在但另一个RDD中不存在的元素。cartesian(other: RDD[U]): RDD[(T, U)]
:返回一个新的RDD,其中包含当前RDD和另一个RDD所有可能的元素对。这些Transformation算子用于对RDD进行转换操作,生成一个新的RDD。它们是惰性求值的,只有在遇到一个Action算子时才会触发实际的计算。您可以根据具体的业务逻辑和数据操作需求,选择适合的Transformation算子来处理数据。
object TransformationExample extends App{
import org.apache.spark.{SparkConf, SparkContext}
// 创建SparkConf和SparkContext
val conf = new SparkConf().setAppName("TransformationExample").setMaster("local")
val sc = new SparkContext(conf)
// 创建一个RDD
val rdd = sc.parallelize(Seq(1, 2, 3, 4, 5))
// 使用map算子对RDD中的每个元素进行加倍操作
val doubledRDD = rdd.map(x => x * 2)
println("doubledRDD: " + doubledRDD.collect().mkString(", "))
// 使用filter算子过滤出偶数元素
val evenRDD = rdd.filter(x => x % 2 == 0)
println("evenRDD: " + evenRDD.collect().mkString(", "))
// 创建另一个RDD
val otherRDD = sc.parallelize(Seq(4, 5, 6, 7, 8))
// 使用union算子合并两个RDD
val unionRDD = rdd.union(otherRDD)
println("unionRDD: " + unionRDD.collect().mkString(", "))
// 使用intersection算子获取两个RDD的交集
val intersectionRDD = rdd.intersection(otherRDD)
println("intersectionRDD: " + intersectionRDD.collect().mkString(", "))
// 使用subtract算子获取当前RDD中存在但另一个RDD中不存在的元素
val subtractRDD = rdd.subtract(otherRDD)
println("subtractRDD: " + subtractRDD.collect().mkString(", "))
// 使用cartesian算子获取两个RDD的笛卡尔积
val cartesianRDD = rdd.cartesian(otherRDD)
println("cartesianRDD: " + cartesianRDD.collect().mkString(", "))
// 使用flatMap算子将每个元素拆分成多个单词
val wordsRDD = rdd.flatMap(x => x.toString.toCharArray.map(_.toString))
println("wordsRDD: " + wordsRDD.collect().mkString(", "))
// 使用distinct算子去重
val distinctRDD = rdd.distinct()
println("distinctRDD: " + distinctRDD.collect().mkString(", "))
// 关闭SparkContext
sc.stop()
}
//doubledRDD: 2, 4, 6, 8, 10
//evenRDD: 2, 4
//unionRDD: 1, 2, 3, 4, 5, 4, 5, 6, 7, 8
//intersectionRDD: 4, 5
//subtractRDD: 1, 2, 3
//cartesianRDD: (1,4), (1,5), (1,6), (1,7), (1,8), (2,4), (2,5), (2,6), (2,7), (2,8), (3,4), (3,5), (3,6), (3,7), (3,8), (4,4), (4,5), (4,6), (4,7), (4,8), (5,4), (5,5), (5,6), (5,7), (5,8)
//wordsRDD: 1, 2, 3, 4, 5
/**
* Return a new RDD by applying a function to all elements of this RDD.
*/
def map[U: ClassTag](f: T => U): RDD[U] = withScope {
val cleanF = sc.clean(f)
new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}
map
方法是一个Transformation算子,用于对RDD中的每个元素应用给定的函数。它接收一个函数f
,该函数将类型为T
的元素转换为类型为U
的元素,并返回一个新的RDD,其中包含应用函数后的结果。
在内部实现中,map
方法创建了一个新的MapPartitionsRDD
对象,该对象继承自RDD[U]
。它通过迭代每个分区中的元素,并将函数f
应用于每个元素来生成转换后的结果。最终,新的MapPartitionsRDD
将作为一个新的RDD返回。
注意,在函数应用之前,map
方法使用sc.clean
方法对函数进行了清理,以确保可以正确地序列化和传输函数。这是因为函数可能会引用外部的变量或对象。
通过使用map
方法,我们可以对RDD中的元素进行一对一的转换操作,从而生成一个新的RDD,以满足特定的业务需求。
以下是对MapPartitionsRDD
的定义和功能的解释:
/**
* 对父RDD的每个分区应用提供的函数的RDD。
*
* @param prev 父RDD。
* @param f 用于将元组(TaskContext、分区索引、输入迭代器)映射为输出迭代器的函数。
* @param preservesPartitioning 输入函数是否保留分区器,除非prev是一个键值对RDD并且输入函数不修改键,否则应为false。
* @param isFromBarrier 指示此RDD是否是从RDDBarrier转换而来的,包含至少一个RDDBarrier的阶段将转变为barrier阶段。
* @param isOrderSensitive 函数是否依赖于顺序。如果它依赖于顺序,则当输入顺序改变时可能会返回完全不同的结果。大多数有状态的函数都是order-sensitive的。
*/
private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag](
var prev: RDD[T],
f: (TaskContext, Int, Iterator[T]) => Iterator[U], // (TaskContext, partition index, iterator)
preservesPartitioning: Boolean = false,
isFromBarrier: Boolean = false,
isOrderSensitive: Boolean = false)
extends RDD[U](prev) {
override val partitioner = if (preservesPartitioning) firstParent[T].partitioner else None
override def getPartitions: Array[Partition] = firstParent[T].partitions
override def compute(split: Partition, context: TaskContext): Iterator[U] =
f(context, split.index, firstParent[T].iterator(split, context))
override def clearDependencies() {
super.clearDependencies()
prev = null
}
@transient protected lazy override val isBarrier_ : Boolean =
isFromBarrier || dependencies.exists(_.rdd.isBarrier())
override protected def getOutputDeterministicLevel = {
if (isOrderSensitive && prev.outputDeterministicLevel == DeterministicLevel.UNORDERED) {
DeterministicLevel.INDETERMINATE
} else {
super.getOutputDeterministicLevel
}
}
}
MapPartitionsRDD
是一个表示对父RDD的每个分区应用函数的RDD。它接收一个父RDD prev
,一个函数 f
,以及一些额外的参数。函数 f
将一个元组 (TaskContext, partition index, input iterator)
映射为一个输出迭代器。
在内部实现中,MapPartitionsRDD
继承自 RDD[U]
,其中 U
是转换后的元素类型。它重写了一些方法来实现对父RDD的分区计算和依赖关系管理。通过调用函数 f
来计算每个分区的结果,并将其作为一个新的迭代器返回。
此外,MapPartitionsRDD
还提供了一些其他功能,如处理分区器、清除依赖关系和判断是否是Barrier RDD等。
通过使用 MapPartitionsRDD
,我们可以对RDD的每个分区应用函数,从而实现更灵活和高效的数据转换操作。