spark-RDD的理解

一、src

1. RDD是什么,有什么用,为了实现这些作用而具有的特性?

RDD是分布式内存的一个抽象概念,是一种高度受限的共享内存模型,这句话的意思是RDD是只读的记录分区的集合,记录的是跨节点并行计算所需要的逻辑,是跨节点并行计算逻辑的抽象。对RDD操作,就是对各分区数据进行并行和shuffle操作

RDD直译是弹性分布式数据集,它抽象的是分布式数据的处理逻辑,这些逻辑包括:

  1. 执行转换和行动算子的入口。记录数据分区的位置,实现计算向数据移动
  2. 划分job、stage,实现窄依赖的pipeline计算,宽依赖间的shuffle
  3. 通过血缘关系进行缓存以复用计算结果
  4. 通过血缘关系进行容错处理
    通过sparkContext创建,拥有5个关键属性。

{1} 作用

  1. RDD就是用来操作数据的入口。如果说sc是用来跟集群通信的入口,那RDD就是用来操作数据的入口,RDD是通过sc创建的。

  2. 一个RDD在相当于元数据,存储着数据分区及其逻辑结构映射关系,存储着RDD之前的依赖转换关系。多个RDD一起将整个分布式计算过程抽象

{2} 5个属性,用来实现上述的作用

  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可知,每次计算都符合完美的数据本地性。
    RDD类源码文件中的4个方法和一个属性对应上述阐述的RDD的5大特性。RDD.scala的源码如下:

/**
 * :: DeveloperApi ::
 * Implemented by subclasses to compute a given partition. 通过子类实现给定分区的计算
 */
@DeveloperApi
def compute(split: Partition, context: TaskContext): Iterator[T]
 
/**
 * Implemented by subclasses to return the set of partitions in this RDD. This method will only
 * be called once, so it is safe to implement a time-consuming computation in it.
 * 通过子类实现,返回一个RDD分区列表,这个方法只被调用一次,它是安全的执行一次耗时计算
 *
 * 数组中的分区必须符合以下属性设置
 * The partitions in this array must satisfy the following property:
 *   `rdd.partitions.zipWithIndex.forall { case (partition, index) => partition.index == index }`
 */
protected def getPartitions: Array[Partition]
 
/**
 * 返回对父RDD的依赖列表,这个方法仅只被调用一次,它是安全的执行一次耗时计算
 * Implemented by subclasses to return how this RDD depends on parent RDDs. This method will only
 * be called once, so it is safe to implement a time-consuming computation in it.
 */
protected def getDependencies: Seq[Dependency[_]] = deps
 
/**
 * 可选的,指定优先位置,输入参数是spilt分片,输出结果是一组优先的节点位置
 * Optionally overridden by subclasses to specify placement preferences.
 */
protected def getPreferredLocations(split: Partition): Seq[String] = Nil
 
/**
 * Optionally overridden by subclasses to specify how they are partitioned.
 * 可选的,通过子类实现,指定如何分区
 */
@transient val partitioner: Option[Partitioner] = None

其中,TaskContext是读取或改变执行任务的环境,用org.apache.spark.TaskContext.get()可返回当前可用的TaskContext,可以调用内部的函数访问正在运行任务的环境信息。Partitioner是一个对象,定义了如何在key-Value类型的RDD元素中用Key分区,从0到numPartitions-1区间内映射每一个Key到Partition ID。Partition是一个RDD的分区标识符。Partition.scala的源码如下。

/**
 * An identifier for a partition in an RDD.
 */
trait Partition extends Serializable {
  /**
   * Get the partition's index within its parent RDD
   */
  def index: Int
 
  // A better default implementation of HashCode
  override def hashCode(): Int = index
 
  override def equals(other: Any): Boolean = super.equals(other)
}	

2. 3个数据结构的关系

RDD是DF、DS的基础,DF是在RDD上增加了schema,意味着每行数据都有相同的结构,所以在序列化的时候效率更高,可以直接获取列。DataSet 结合了 RDD 和 DataFrame 的优点,并带来的一个新的概念 Encoder。当序列化数据时,Encoder 产生字节码与 off-heap 进行交互,能够达到按需访问数据的效果,而不用反序列化整个对象,比如访问某个属性时,DF需要将整个ROW反序列化构建到内存中。

3. RDD的弹性如何理解

RDD的命名也是很讲究的:

{1} 自动进行内存和磁盘数据存储的切换

Spark会优先把数据放到内存中,如果内存实在放不下,会放到磁盘里面,不但能计算内存放下的数据,也能计算内存放不下的数据。如果实际数据大于内存,则要考虑数据放置策略和优化算法。当应用程序内存不足时,Spark应用程序将数据自动从内存存储切换到磁盘存储,以保障其高效运行。

{2} 基于Lineage(血统)的高效容错机制

Lineage是基于Spark RDD的依赖关系来完成的(依赖分为窄依赖和宽依赖两种形态),每个操作只关联其父操作,各个分片的数据之间互不影响,出现错误时只要恢复单个Split的特定部分即可。常规容错有两种方式:一个是数据检查点;另一个是记录数据的更新。数据检查点的基本工作方式,就是通过数据中心的网络链接不同的机器,然后每次操作的时候都要复制数据集,就相当于每次都有一个复制,复制是要通过网络传输的,网络带宽就是分布式的瓶颈,对存储资源也是很大的消耗。记录数据更新就是每次数据变化了就记录一下,这种方式不需要重新复制一份数据,但是比较复杂,消耗性能。
  Spark的RDD通过记录数据更新的方式为何很高效?因为
  1. RDD是不可变的且Lazy;
  2. RDD的写操作是粗粒度的。但是,RDD读操作既可以是粗粒度的,也可以是细粒度的。

{3} Task如果失败,会自动进行特定次数的重试

默认重试次数为4次。TaskSchedulerImpl的源码如下所示:

private[spark] class TaskSchedulerImpl(
    val sc: SparkContext,
    val maxTaskFailures: Int,
    isLocal: Boolean = false)
  extends TaskScheduler with Logging
{
  def this(sc: SparkContext) = this(sc, sc.conf.getInt("spark.task.maxFailures", 4))
 
  val conf = sc.conf

TaskSchedulerImpl是底层的任务调度接口TaskScheduler的实现,这些Schedulers从每一个Stage中的DAGScheduler中获取TaskSet,运行它们,尝试是否有故障。DAGScheduler是高层调度,它计算每个Job的Stage的DAG,然后提交Stage,用TaskSets的形式启动底层TaskScheduler调度在集群中运行。

{4} Stage如果失败,会自动进行特定次数的重试

这样,Stage对象可以跟踪多个StageInfo(存储SparkListeners监听到的Stage的信息,将Stage信息传递给Listeners或web UI)。默认重试次数为4次,且可以直接运行计算失败的阶段,只计算失败的数据分片,Stage的源码如下所示:

private[scheduler] abstract class Stage(
    val id: Int,
    val rdd: RDD[_],
    val numTasks: Int,
    val parents: List[Stage],
    val firstJobId: Int,
    val callSite: CallSite)
  extends Logging {
 
  val numPartitions = rdd.partitions.length
 
  /** Set of jobs that this stage belongs to.属于这个工作集的Stage */
  val jobIds = new HashSet[Int]
 
  val pendingPartitions = new HashSet[Int]
 
  /** The ID to use for the next new attempt for this stage.用于此Stage的下一个新attempt的ID */
  private var nextAttemptId: Int = 0
 
  val name: String = callSite.shortForm
  val details: String = callSite.longForm
 
  /**
   * Pointer to the [StageInfo] object for the most recent attempt. This needs to be initialized
   * here, before any attempts have actually been created, because the DAGScheduler uses this
   * StageInfo to tell SparkListeners when a job starts (which happens before any stage attempts
   * have been created).
   * 最新的[StageInfo] Object指针,需要被初始化,任何attempts都是被创造出来的,因为DAGScheduler使用
   * StageInfo告诉SparkListeners工作何时开始(即发生前的任何阶段已经创建)
   */
  private var _latestInfo: StageInfo = StageInfo.fromStage(this, nextAttemptId)
 
  /**
   * Set of stage attempt IDs that have failed with a FetchFailure. We keep track of these
   * failures in order to avoid endless retries if a stage keeps failing with a FetchFailure.
   * We keep track of each attempt ID that has failed to avoid recording duplicate failures if
   * multiple tasks from the same stage attempt fail (SPARK-5945).
   * 设置Stage attempt IDs当失败时可以读取失败信息,跟踪这些失败,为了避免无休止的重复失败
   * 跟踪每一次attempt,以便避免记录重复故障,如果从同一stage创建多任务失败(SPARK-5945)
   */
  private val fetchFailedAttemptIds = new HashSet[Int]
 
  private[scheduler] def clearFailures() : Unit = {
    fetchFailedAttemptIds.clear()
  }
 
  /**
   * Check whether we should abort the failedStage due to multiple consecutive fetch failures.
   * 检查是否应该中止由于连续多次读取失败的stage
   * This method updates the running set of failed stage attempts and returns
   * true if the number of failures exceeds the allowable number of failures.
   * 如果失败的次数超过允许的次数,此方法更新失败stage attempts和返回的运行集
   */
  private[scheduler] def failedOnFetchAndShouldAbort(stageAttemptId: Int): Boolean = {
    fetchFailedAttemptIds.add(stageAttemptId)
    fetchFailedAttemptIds.size >= Stage.MAX_CONSECUTIVE_FETCH_FAILURES
  }
 
  /** Creates a new attempt for this stage by creating a new StageInfo with a new attempt ID. */
  // 在stage中创建一个新的attempt
  def makeNewStageAttempt(
      numPartitionsToCompute: Int,
      taskLocalityPreferences: Seq[Seq[TaskLocation]] = Seq.empty): Unit = {
    val metrics = new TaskMetrics
    metrics.register(rdd.sparkContext)
    _latestInfo = StageInfo.fromStage(
      this, nextAttemptId, Some(numPartitionsToCompute), metrics, taskLocalityPreferences)
    nextAttemptId += 1
  }
 
  /** Returns the StageInfo for the most recent attempt for this stage. */
  // 返回当前stage中最新的stageinfo
  def latestInfo: StageInfo = _latestInfo
 
  override final def hashCode(): Int = id
 
  override final def equals(other: Any): Boolean = other match {
    case stage: Stage => stage != null && stage.id == id
    case _ => false
  }
 
  /** Returns the sequence of partition ids that are missing (i.e. needs to be computed). */
  // 返回需要重新计算的分区标识的序列
  def findMissingPartitions(): Seq[Int]
}
 
private[scheduler] object Stage {
  // The number of consecutive failures allowed before a stage is aborted
  // 允许一个stage中止的连续故障数
  val MAX_CONSECUTIVE_FETCH_FAILURES = 4
}

Stage是Spark Job运行时具有相同逻辑功能和并行计算任务的一个基本单元。Stage中所有的任务都依赖同样的Shuffle,每个DAG任务通过DAGScheduler在Stage的边界处发生Shuffle形成Stage,然后DAGScheduler运行这些阶段的拓扑顺序。每个Stage都可能是ShuffleMapStage,如果是ShuffleMapStage,则跟踪每个输出节点(nodes)上的输出文件分区,它的任务结果是输入其他的Stage(s),或者输入一个ResultStage,若输入一个ResultStage,这个ResultStage的任务直接在这个RDD上运行计算这个Spark Action的函数(如count()、 save()等),并生成shuffleDep等字段描述Stage和生成变量,如outputLocs和numAvailableOutputs,为跟踪map输出做准备。每个Stage会有firstjobid,确定第一个提交Stage的Job,使用FIFO调度时,会使得其前面的Job先行计算或快速恢复(失败时)。

ShuffleMapStage是DAG产生数据进行Shuffle的中间阶段,它发生在每次Shuffle操作之前,可能包含多个Pipelined操作,ResultStage阶段捕获函数在RDD的分区上运行Action算子计算结果,有些Stage不是运行在RDD的所有的分区上,例如,first()、lookup()等。SparkListener是Spark调度器的事件监听接口。注意,这个接口随着Spark版本的不同会发生变化。

{5} checkpoint和persist(检查点和持久化),可主动或被动触发

checkpoint是对RDD进行的标记,会产生一系列的文件,且所有父依赖都会被删除,是整个依赖(Lineage)的终点。checkpoint也是Lazy级别的。persist后RDD工作时每个工作节点都会把计算的分片结果保存在内存或磁盘中,下一次如果对相同的RDD进行其他的Action计算,就可以重用。

因为用户只与Driver Program交互,因此只能用RDD中的cache()方法去cache用户能看到的RDD。所谓能看到,是指经过Transformation算子处理后生成的RDD,而某些在Transformation算子中Spark自己生成的RDD是不能被用户直接cache的。例如,reduceByKey()中会生成的ShuffleRDD、MapPartitionsRDD是不能被用户直接cache的。在Driver Program中设定RDD.cache()后,系统怎样进行cache?首先,在计算RDD的Partition之前就去判断Partition要不要被cache,如果要被cache,先将Partition计算出来,然后cache到内存。cache可使用memory,如果写到HDFS磁盘的话,就要检查checkpoint。调用RDD.cache()后,RDD就变成persistRDD了,其StorageLevel为MEMORY_ONLY,persistRDD会告知Driver说自己是需要被persist的。此时会调用RDD.iterator()。 RDD.scala的iterator()的源码如下:

/**
 * Internal method to this RDD; will read from cache if applicable, or otherwise compute it.
 * This should ''not'' be called by users directly, but is available for implementors of custom
 * subclasses of RDD.
 * RDD的内部方法,将从合适的缓存中读取,否则计算它。这不应该被用户直接使用,但可用于实现自定义的子RDD
 */
final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
  if (storageLevel != StorageLevel.NONE) {
    getOrCompute(split, context)
  } else {
    computeOrReadCheckpoint(split, context)
  }
}

当RDD.iterator()被调用的时候,也就是要计算该RDD中某个Partition的时候,会先去cacheManager那里获取一个blockId,然后去BlockManager里匹配该Partition是否被checkpoint了,如果是,那就不用计算该Partition了,直接从checkpoint中读取该Partition的所有records放入ArrayBuffer里面。如果没有被checkpoint过,先将Partition计算出来,然后将其所有records放到cache中。总体来说,当RDD会被重复使用(不能太大)时,RDD需要cache。Spark自动监控每个节点缓存的使用情况,利用最近最少使用原则删除老旧的数据。如果想手动删除RDD,可以使用RDD.unpersist()方法。

此外,可以利用不同的存储级别存储每一个被持久化的RDD。例如,它允许持久化集合到磁盘上,将集合作为序列化的Java对象持久化到内存中、在节点间复制集合或者存储集合到Alluxio中。可以通过传递一个StorageLevel对象给persist()方法设置这些存储级别。cache()方法使用默认的存储级别-StorageLevel.MEMORY_ONLY。RDD根据useDisk、useMemory、 useOffHeap、deserialized、replication 5个参数的组合提供了常用的12种基本存储,完整的存储级别介绍如下。StorageLevel.scala的源码如下:

val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(true, true, true, false, 1)

StorageLevel是控制存储RDD的标志,每个StorageLevel记录RDD是否使用memory,或使用ExternalBlockStore存储,如果RDD脱离了memory或ExternalBlockStore,是否扔掉RDD,是否保留数据在内存中的序列化格式,以及是否复制多个节点的RDD分区。另外,org.apache.spark.storage.StorageLevel是单实例(singleton)对象,包含了一些静态常量和常用的存储级别,且可用singleton对象工厂方法StorageLevel(…)创建定制化的存储级别。

Spark的多个存储级别意味着在内存利用率和CPU利用率间的不同权衡。推荐通过下面的过程选择一个合适的存储级别:①如果RDD适合默认的存储级别(MEMORY_ONLY),就选择默认的存储级别。因为这是CPU利用率最高的选项,会使RDD上的操作尽可能地快。②如果不适合用默认级别,就选择MEMORY_ONLY_SER。选择一个更快的序列化库提高对象的空间使用率,但是仍能够相当快地访问。③除非算子计算RDD花费较大或者需要过滤大量的数据,不要将RDD存储到磁盘上,否则重复计算一个分区,就会和从磁盘上读取数据一样慢。④如果希望更快地恢复错误,可以利用replicated存储机制,所有的存储级别都可以通过replicated计算丢失的数据来支持完整的容错。另外,replicated的数据能在RDD上继续运行任务,而不需要重复计算丢失的数据。在拥有大量内存的环境中或者多应用程序的环境中,Off_Heap(将对象从堆中脱离出来序列化,然后存储在一大块内存中,这就像它存储到磁盘上一样,但它仍然在RAM内存中。Off_Heap对象在这种状态下不能直接使用,须进行序列化及反序列化。序列化和反序列化可能会影响性能,Off_Heap堆外内存不需要进行GC)。Off_Heap具有如下优势:Off_Heap运行多个执行者共享的Alluxio中相同的内存池,显著地减少GC。如果单个的Executor崩溃,缓存的数据也不会丢失。

{6} 数据调度弹性,DAGScheduler、TASKScheduler和资源管理无关

Spark将执行模型抽象为通用的有向无环图计划(DAG),这可以将多Stage的任务串联或并行执行,从而不需要将Stage中间结果输出到HDFS中,当发生节点运行故障时,可有其他可用节点代替该故障节点运行。

{7} 数据分片的高度弹性(coalesce)

Spark进行数据分片时,默认将数据放在内存中,如果内存放不下,一部分会放在磁盘上进行保存。

RDD.scala的coalesce算子代码如下:

/**
 * Return a new RDD that is reduced into `numPartitions` partitions.
 *
 * This results in a narrow dependency, e.g. if you go from 1000 partitions
 * to 100 partitions, there will not be a shuffle, instead each of the 100
 * new partitions will claim 10 of the current partitions.
 *
 * However, if you're doing a drastic coalesce, e.g. to numPartitions = 1,
 * this may result in your computation taking place on fewer nodes than
 * you like (e.g. one node in the case of numPartitions = 1). To avoid this,
 * you can pass shuffle = true. This will add a shuffle step, but means the
 * current upstream partitions will be executed in parallel (per whatever
 * the current partitioning is).
 *
 * Note: With shuffle = true, you can actually coalesce to a larger number
 * of partitions. This is useful if you have a small number of partitions,
 * say 100, potentially with a few partitions being abnormally large. Calling
 * coalesce(1000, shuffle = true) will result in 1000 partitions with the
 * data distributed using a hash partitioner.
 */
def coalesce(numPartitions: Int, shuffle: Boolean = false,
             partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
            (implicit ord: Ordering[T] = null)
    : RDD[T] = withScope {
  require(numPartitions > 0, s"Number of partitions ($numPartitions) must be positive.")
  if (shuffle) {
    /** Distributes elements evenly across output partitions, starting from a random partition. */
    // 从随机分区开始,将元素均匀分布在输出分区上
    val distributePartition = (index: Int, items: Iterator[T]) => {
      var position = (new Random(index)).nextInt(numPartitions)
      items.map { t =>
        // Note that the hash code of the key will just be the key itself. The HashPartitioner
        // will mod it with the number of total partitions.
        // key的哈希码是key本身,HashPartitioner将它与总分区数进行取模运算
        position = position + 1
        (position, t)
      }
    } : Iterator[(Int, T)]
 
    // include a shuffle step so that our upstream tasks are still distributed
    // 包括一个shuffle步骤,使我们的上游任务仍然是分布式的
    new CoalescedRDD(
      new ShuffledRDD[Int, T, T](mapPartitionsWithIndex(distributePartition),
      new HashPartitioner(numPartitions)),
      numPartitions,
      partitionCoalescer).values
  } else {
    new CoalescedRDD(this, numPartitions, partitionCoalescer)
  }
}

例如,在计算的过程中,会产生很多的数据碎片,这时产生一个Partition可能会非常小,如果一个Partition非常小,每次都会消耗一个线程去处理,这时可能会降低它的处理效率,需要考虑把许多小的Partition合并成一个较大的Partition去处理,这样会提高效率。另外,有可能内存不是那么多,而每个Partition的数据Block比较大,这时需要考虑把Partition变成更小的数据分片,这样让Spark处理更多的批次,但是不会出现OOM

4. RDD的底层实现

RDD底层存储原理:其数据分布存储于多台机器上,事实上,每个RDD的数据都以Block的形式存储于多台机器上,每个Executor会启动一个BlockManagerSlave,并管理一部分Block;而Block的元数据由Driver节点上的BlockManagerMaster保存,BlockManagerSlave生成Block后向BlockManagerMaster注册该Block,BlockManagerMaster管理RDD与Block的关系,当RDD不再需要存储的时候,将向BlockManagerSlave发送指令删除相应的Block。

BlockManager管理RDD的物理分区,每个Block就是节点上对应的一个数据块,可以存储在内存或者磁盘上。而RDD中的Partition是一个逻辑数据块,对应相应的物理块Block。本质上,一个RDD在代码中相当于数据的一个元数据结构,存储着数据分区及其逻辑结构映射关系,存储着RDD之前的依赖转换关系。

BlockManager在每个节点上运行管理Block(Driver和Executors),它提供一个接口检索本地和远程的存储变量,如memory、disk、off-heap。使用BlockManager前必须先初始化。BlockManager.scala的部分源码如下所示:

private[spark] class BlockManager(
    executorId: String,
    rpcEnv: RpcEnv,
    val master: BlockManagerMaster,
    serializerManager: SerializerManager,
    val conf: SparkConf,
    memoryManager: MemoryManager,
    mapOutputTracker: MapOutputTracker,
    shuffleManager: ShuffleManager,
    val blockTransferService: BlockTransferService,
    securityManager: SecurityManager,
    numUsableCores: Int)
  extends BlockDataManager with BlockEvictionHandler with Logging {

BlockManagerMaster会持有整个Application的Block的位置、Block所占用的存储空间等元数据信息,在Spark的Driver的DAGScheduler中,就是通过这些信息来确认数据运行的本地性的。Spark支持重分区,数据通过Spark默认的或者用户自定义的分区器决定数据块分布在哪些节点。RDD的物理分区是由Block-Manager管理的,每个Block就是节点上对应的一个数据块,可以存储在内存或者磁盘。而RDD中的partition是一个逻辑数据块,对应相应的物理块Block。本质上,一个RDD在代码中相当于数据的一个元数据结构(一个RDD就是一组分区),存储着数据分区及Block、Node等的映射关系,以及其他元数据信息,存储着RDD之前的依赖转换关系。分区是一个逻辑概念,Transformation前后的新旧分区在物理上可能是同一块内存存储。

Spark通过读取外部数据创建RDD,或通过其他RDD执行确定的转换Transformation操作(如map、union和groubByKey)而创建,从而构成了线性依赖关系,或者说血统关系(Lineage),在数据分片丢失时可以从依赖关系中恢复自己独立的数据分片,对其他数据分片或计算机没有影响,基本没有检查点开销,使得实现容错的开销很低,失效时只需要重新计算RDD分区,就可以在不同节点上并行执行,而不需要回滚(Roll Back)整个程序。落后任务(即运行很慢的节点)是通过任务备份,重新调用执行进行处理的。

因为RDD本身支持基于工作集的运用,所以可以使Spark的RDD持久化(persist)到内存中,在并行计算中高效重用。多个查询时,我们就可以显性地将工作集中的数据缓存到内存中,为后续查询提供复用,这极大地提升了查询的速度。在Spark中,一个RDD就是一个分布式对象集合,每个RDD可分为多个片(Partitions),而分片可以在集群环境的不同节点上计算。

RDD作为泛型的抽象的数据结构,支持两种计算操作算子:Transformation(变换)与Action(行动)。且RDD的写操作是粗粒度的,读操作既可以是粗粒度的,也可以是细粒度的。RDD.scala的源码如下:

/**
 * Internally, each RDD is characterized by five main properties:
 * 每个RDD都有5个主要特性
 *  - A list of partitions    分区列表
 *  - A function for computing each split    每个分区都有一个计算函数
 *  - A list of dependencies on other RDDs    依赖于其他RDD的列表
 *  - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)    数据类型(key-value)的RDD分区器
 *  - Optionally, a list of preferred locations to compute each split on (e.g. block locations for    每个分区都有一个分区位置列表
 */
abstract class RDD[T: ClassTag](
    @transient private var _sc: SparkContext,
    @transient private var deps: Seq[Dependency[_]]
  ) extends Serializable with Logging {

其中,SparkContext是Spark功能的主要入口点,一个SparkContext代表一个集群连接,可以用其在集群中创建RDD、累加变量、广播变量等,在每一个可用的JVM中只有一个SparkContext,在创建一个新的SparkContext之前,必须先停止该JVM中可用的SparkContext,这种限制可能最终会被修改。SparkContext被实例化时需要一个SparkConf对象去描述应用的配置信息,在这个配置对象中设置的信息,会覆盖系统默认的配置。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值