Spark 源码分析

本文详细介绍了Spark的核心概念,包括Scala的Iterator和Option类型。深入探讨了Spark的高级概念,如Yarn模式运行机制、Master & Worker、作业执行原理和数据倾斜。此外,文章详细讲解了Spark的各种算子,如map、flatMap、filter及其使用案例,重点分析了mapPartitions、groupByKey、ShuffledRDD和reduceByKey等操作的源码和执行原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


文章内容输出来源:拉勾教育大数据训练营,一堆东西还没学会,希望以后一点一点把漏洞补上,虽然我老鸽王了

基础概念

记录一些自己在scala和spark速成学习中懵圈的概念

Scala Iterator(迭代器)

简介

  • Scala Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法。
  • 迭代器 it 的两个基本操作是 nexthasNext
  • 调用 it.next() 会返回迭代器的下一个元素,并且更新迭代器的状态。
  • 调用 it.hasNext() 用于检测集合中是否还有元素。

Scala的Option的类型

Option有两个子类别,Some和None。当程序回传Some的时候,代表这个函式成功地给了你一个String,而你可以透过get()函数拿到那个String,如果程序返回的是None,则代表没有字符串可以给你。
在这里插入图片描述

val capitals = Map("1"->"Paris", "2"->"Tokyo", "3"->"Beijing")
scala> capitals.get("1")
res0: Option[String] = Some(Paris)
scala> capitals.get("8")
res1: Option[String] = None

高级概念

Yarn模式运行机制(ing)

Master & Worker(ing)

作业执行原理(ing)

Shuffle详解(ing)

数据倾斜(ing)

算子

map

/**
   * 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))
  }

最关键的一行就是new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))

这里withScope涉及到的是UI,而clean只是起到了清除闭包中的不能序列化的变量,防⽌RDD在⽹络传输过程中反序列化失败

这里 U 和 T都是泛型,传入的是T,输出的是U

/**
 * An RDD that applies the provided function to every partition of the parent RDD.
 *
 * @param prev the parent RDD.
 * @param f The function used to map a tuple of (TaskContext, partition index, input iterator) to
 *          an output iterator.
 * @param preservesPartitioning Whether the input function preserves the partitioner, which should
 *                              be `false` unless `prev` is a pair RDD and the input function
 *                              doesn't modify the keys.
 * @param isFromBarrier Indicates whether this RDD is transformed from an RDDBarrier, a stage
 *                      containing at least one RDDBarrier shall be turned into a barrier stage.
 * @param isOrderSensitive whether or not the function is order-sensitive. If it's order
 *                         sensitive, it may return totally different result when the input order
 *                         is changed. Mostly stateful functions are order-sensitive.
 */
private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag](
    var prev: RDD[T],
  	// 这里帮我们解释了传入的pid是什么,是分区的区号,Context应该就是封装了相关信息的上下文,task运⾏的环境
    f: (TaskContext, Int, Iterator[T]) => Iterator[U],  // (TaskContext, partition index, iterator)
    preservesPartitioning: Boolean = false,
    isFromBarrier: Boolean = false,
    isOrderSensitive: Boolean = false)
  extends RDD[U](prev) {
   
   

  // 通过判断分区器的标识获得分区器或者是None,从字面上是父RDD的分区器,暂时不深究,总之就是要获得分区器
  override val partitioner = if (preservesPartitioning) firstParent[T].partitioner else None

  override def getPartitions: Array[Partition] = firstParent[T].partitions
	
  // compute方法调用了传进来的函数,对输入的RDD分区进行计算
  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
    }
  }
}

我看源代码比较少,乍一看有点迷,先看注释,然后自己写一下注释

可以看到,其实会调用compute函数,对每一个RDD分区执行我们设置的函数,RDD的数据放在了迭代器中

同时我们找一下firstParent指的具体是什么

/** Returns the first parent RDD */
  protected[spark] def firstParent[U: ClassTag]: RDD[U] = {
   
   
    dependencies.head.rdd.asInstanceOf[RDD[U]]
  }

其实就是返回the first parent RDD,我的理解是MapPartitionsRDD的父RDD其实就是我们本身执行map的RDD,所以相当于想要获取分区号和分区器,都会选择从原先的RDD中去找,可以看到内部第一个参数其实就是var prev: RDD[T]

这里最重要的我觉得是认识了MapPartitionsRDD:一种RDD,比如通过map操作生成的新RDD即为此种类型

flatMap

/**
 *  Return a new RDD by first applying a function to all elements of this
 *  RDD, and then flattening the results.
 */
def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] = withScope {
   
   
  val cleanF = sc.clean(f)
  new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.flatMap(cleanF))
}

字面意思上我喜欢理解为先map,再flat,整体看上去和map一样,就是map换成了flatMap

filter

/**
 * Return a new RDD containing only the elements that satisfy a predicate.
 */
def filter(f: T => Boolean): RDD[T] = withScope {
   
   
  val cleanF = sc.clean(f)
  new MapPartitionsRDD[T, T](
    this,
    (context, pid, iter) => iter.filter(cleanF),
    preservesPartitioning = true)
}

和上两个算子稍有不同的就是,他的preservesPartitioning一定要是true,一定要保留分区器

案例1:

需求1:不使用 map 算子,对rdd(1 to 10) 中每个元素加1,最后求和

需求2:不使用 map 算子,对rdd(1 to 10) 中每个元素加1,最后做字符串加

package org.apache.spark.rdd

package com.xiaoyuyu.test

import org.apache.spark.{
   
   SparkConf, SparkContext}

/**
 * @Description: 源码分析
 * @Author: Xiaoyuyu
 * @CreateDate: 2021/3/11 7:45 下午
 */

object Test {
   
   

  def main(args: Array[String]): Unit = {
   
   

    val conf: SparkConf = new SparkConf().setAppName(this.getClass.getCanonicalName).setMaster("local[*]")
    val sc = new SparkContext(conf)
    sc.setLogLevel("WARN")

    val arr: Array[Int] = (1 to 10).toArray
    val value: RDD[Int] = sc.makeRDD(arr)

//    需求1:不使用 map 算子,对rdd(1 to 10) 中每个元素加1,最后求和
    val value1 = new MapPartitionsRDD[Int, Int](value, (_, _, iter) => iter.map(_ + 1))
    value1.foreach(println(_))
//    需求2:不使用 map 算子,对rdd(1 to 10) 中每个元素加1,最后做字符串加
    val str: String = new MapPartitionsRDD[String, Int](value, (_, _, iter) => iter.map(x => (x + 1).toString))
      .reduce(_ + _)
    println(str)
    sc.stop()
  }
}
案例2

需求:不使用 filter 算子,实现filter功能,不保留/保留 父RDD的分区器

//    需求:不使用 filter 算子,实现filter功能,不保留/保留 父RDD的分区器
    val value2 = new MapPartitionsRDD[Int, Int](value,
      (_, _, iter) => iter.filter(_ > 2),
      preservesPartitioning = false)
    value2.foreach(println(_))

mapPartitions

这个作为一个小白用的有点少,只知道他是什么功能,先看一下怎么用

/**
   * Return a new RDD by applying a function to each partition of this RDD.
   *
   * `preservesPartitioning` indicates whether the input function preserves the partitioner, which
   * should be `false` unless this is a pair RDD and the input function doesn't modify the keys.
   */
  def mapPartitions[U: ClassTag](
      f: Iterator[T] => Iterator[U],
      preservesPartitioning: Boolean = false): RDD[U] = withScope {
   
   
    val cleanedF = sc.clean(f)
    new MapPartitionsRDD(
      this,
      (context: TaskContext, index: Int, iter: Iterator[T]) => cleanedF(iter),
      preservesPartitioning)
  }

可以看到分区器必须设置为false,底层依然还是一个MapPartitionsRDD,而且可以看到函数f调用和返回的都是Iterator

(context: TaskContext, index: Int, iter: Iterator[T]) => cleanedF(iter),这一行与之前的算子有不同,也就是f在用法上有些许不同,但是暂时不深究,但我们至少知道了函数怎么用,例子如下

val value3: RDD[Int] = value.mapPartitions(
      x => {
   
   
        val x1: Iterator[Int] = x
        val ints: Iterator[Int] = x1.map(_ + 1)
        ints
      }
    )

我特地标注了类型,可以知道操作的每一个分区的载体其实是Iterator[T]

  • f(index, iter)=> 将函数作用在迭代器上 => mapPartitions的实现
  • iter.map(cleanF) => 将函数作用在每个元素上 => map的实现

我们还可以看到许多相关的内部函数,我们无法使用,区别就是没用对函数clean过,如下,了解一下即可

在这里插入图片描述

案例1

最后自己实现一下不用mapPartitions的每个元素加一

val value4 = new MapPartitionsRDD(
      value,
      (_, _, iter: Iterator[Int]) => {
   
   
//         iter.map(_+1)
        /*val iter2: RDD[Int] = sc.makeRDD(iter.toList)
        val value5:RDD[Int] = new MapPartitionsRDD[Int, Int](iter2, (_, _, iter) => iter.map(_ + 1))
        val ints1: Array[Int] = value5.collect()
        ints1.toList.iterator*/
        val ints:ListBuffer[Int] = ListBuffer[Int]()
        while(iter.hasNext) {
   
   
          val i: Int = iter.next()
          ints += (i+1)
        }
        ints.iterator
      },
      false)

    value4.foreach(println(_))

mapPartitionsWithIndex

作为一个小白,必须诚实地说mapPartitions我至少知道是啥功能,这个算子我确实不知道咋用

/**
   * Return a new RDD by applying a function to each partition of this RDD, while tracking the index
   * of the original partition.
   *
   * `preservesPartitioning` indicates whether the input function preserves the partitioner, which
   * should be `false` unless this is a pair RDD and the input function doesn't modify the keys.
   */
  def mapPartitionsWithIndex[U: ClassTag](
      f: (Int, Iterator[T]) => Iterator[U],
      preservesPartitioning: Boolean = false): RDD[U] = withScope {
   
   
    val cleanedF = sc.clean(f)
    new MapPartitionsRDD(
      this,
      (context: TaskContext, index: Int, iter: Iterator[T]) => cleanedF(index, iter),
      preservesPartitioning)
  }

分区器标识还是必须要设置为false,while tracking the index of the original partition. 最后就是多了这句话

cleanedF(index, iter)这里也是不一样的,体现了对index的跟踪

mapPartitionsWithIndex既可以拿到分区的迭代器,又可以拿到分区编号。

def func(index:Int,iterator: Iterator[Int]):Iterator[Int] = {
   
   
      println(s"index:$index")
      val list: List[Int] = iterator.toList
      list.foreach(
        x => {
   
   
          val string: String = x.toString
          print(string+"\t")
        }
      )
      println()
      list.iterator
    }

    val value5: RDD[Int] = value.mapPartitionsWithIndex(func)
    value5.foreach(println(_))

其实就是给了我们操作index的机会,别的其实是一样的</

Apache Spark是一个分布式计算框架,它支持内存中的数据处理,大大提高了大数据批处理的速度,并且还能够用于实时流式处理、机器学习等场景。 ### 源码结构 Spark源码非常庞大而复杂,但可以大致分为以下几个模块: 1. **Core Module** 这是整个项目的基石,包含了任务调度系统(Scheduler)、依赖管理机制以及底层的RDD操作逻辑。理解这部分有助于掌握如何构建高效的任务分发及执行流程。 2. **SQL Module (Catalyst Optimizer)** Catalyst 是 Spark SQL 中的一个查询优化引擎。通过规则匹配的方式对输入表达式树进行转换,以此达到提高性能的目的。例如,自动选择最优物理计划或者简化某些特定模式下的表达式求值过程。 3. **Streaming Module** 实现了微批次架构来模拟连续的数据流入流出特性。开发者可以通过简单的API快速搭建起强大的实时应用;同时内部也包含了许多针对时间窗口运算的设计考量点。 4. **MLlib / GraphX Modules** MLlib 提供了一系列常用的机器学习算法库及其相关的工具函数;GraphX 则专注于图模型方面的研究工作,如PageRank 算法实现等。 5. **Other Components** 包含Shuffle Manager, Storage Layer, Metrics System等功能组件,在实际运行过程中起到辅助作用。 ### 关键设计原则 - **弹性分布式数据集(RDD)** RDD是一种只读的分区记录集合,它可以并行地存储在集群节点上并且允许用户施加各种变换(transformation),最终得到新的RDD实例。这种不可变性保证了容错性的简单性和效率最大化。 - **宽窄依赖关系(Wide/Narrow Dependencies)** 根据算子之间的连接方式将DAG划分成Stage阶段时所考虑的因素之一。“窄”表示每个parent partition仅对应少量child partitions,“宽”的情况反之亦然。这直接影响到shuffle文件大小和网络传输成本等问题。 - **延迟加载(Lazy Evaluation)** 当创建一个新RDD之后并不会立即开始计算,而是等到触发Action才会真正启动作业。这一策略不仅节省资源而且便于做更精细粒度的错误恢复措施。 为了深入探究其原理建议从官方文档入手逐步探索各个部分的核心功能接口定义,再结合社区贡献者的博客文章加深认识。此外,GitHub 上面也有很多基于不同版本解读整套体系的文章可供参考。 --
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值