spark高频面试题100题源码解答【建议收藏】—持续更新中
文章目录
1.RDD五个主要特性
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内部的更多细节,请参阅<a href="http://people.csail.mit.edu/matei/papers/2012/nsdi_spark.pdf">Spark论文</a>。
*/
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 // 默认情况下没有位置偏好
2.Spark重分区 Repartition Coalesce关系区别
关系区别
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
}
}
3.reduceByKey与groupByKey的区别,哪一种更具优势?
区别
reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]。
groupByKey:按照key进行分组,直接进行shuffle
所以,在实际开发过程中,reduceByKey比groupByKey,更建议使用。但是需要注意是否会影响业务逻辑。
源码
reduceByKey和reduceByKey都使用combineByKeyWithClassTag函数。combineByKeyWithClassTag有个一个参数mapSideCombine是否map阶段预聚合每个节点上的数据,reduceByKey中使用的true,groupByKey则是false。
groupByKey reduceByKey
/**
* 将RDD中每个键的值分组成单个序列。允许通过传递Partitioner来控制结果键值对RDD的分区。
* 每个组内元素的顺序不能保证,并且甚至在每次评估结果RDD时可能会有所不同。
*
* @note 此操作可能非常昂贵。如果您要对每个键执行聚合(例如求和或平均值),使用`PairRDDFunctions.aggregateByKey`
* 或`PairRDDFunctions.reduceByKey`将提供更好的性能。
*
* @note 目前实现的groupByKey必须能够在内存中保存所有键值对的所有值。如果一个键有太多的值,可能会导致`OutOfMemoryError`。
*/

本文围绕Spark展开,介绍了RDD的五个主要特性,包括分区列表、计算函数等;分析了Spark重分区中Repartition和Coalesce的关系区别;对比了reduceByKey与groupByKey;讲解了常用的transformation和action算子及导致Shuffle的情况;还阐述了Spark窄依赖和宽依赖的定义、Stage划分及相关源码。
最低0.47元/天 解锁文章
2153

被折叠的 条评论
为什么被折叠?



