【Spark系列3】RDD源码解析实战

本文主要讲

1、什么是RDD

2、RDD是如何从数据中构建

一、什么是RDD?

RDD:弹性分布式数据集,Resillient Distributed Dataset的缩写。

个人理解:RDD是一个容错的、并行的数据结构,可以让用户显式的将数据存储到磁盘和内存中,并能控制数据的分区。同时RDD还提供一组丰富的API来操作它。本质上,RDD是一个只读的分区集合,一个RDD可以包含多个分区,每个分区就是一个dataset片段。RDD可以互相依赖

二、RDD是如何从数据中构建

2.1、RDD源码

Internally, each RDD is characterized by five main properties

  • A list of pattitions

  • A function for computing each split

  • A list of dependencies on each RDDs

  • optionally, a partitioner for key-value RDDs(e.g. to say that RDD is hash-partitioned)

  • optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)

RDD基本都有这5个特性:

1、每个RDD 都会有 一个分片列表。 就是可以被切分,和hadoop一样,能够被切分的数据才能并行计算

2、有一个函数计算每一个分片。这里是指下面会提到的compute函数

3、对其他RDD的依赖列表。依赖区分宽依赖和窄依赖

4、可选:key-value类型的RDD是根据hash来分区的,类似于mapreduce当中的partitioner接口,控制哪个key分到哪个reduce

5、可选:每一个分片的有效计算位置(preferred locations),比如HDFS的block的所在位置应该是优先计算的位置

2.2、宽窄依赖

如果一个RDD的每个分区最多只能被一个Child RDD的一个分区所使用, 则称之为窄依赖(Narrow dependency), 如果被多个Child RDD分区依赖, 则称之为宽依赖(wide dependency)

例如 map、filter是窄依赖, 而join、groupby是宽依赖

2.3、源码分析

RDD的5个特征会对应到源码中的 4个方法 和一个属性

RDD.scala是一个总的抽象,不同的子类会对下面的方法进行定制化的实现。比如compute方法,不同子类在实现的时候是不同的。

// 该方法只会被调用一次。由子类实现,返回这个RDD下的所有Partition
protected def getPartitions: Array[Partition]
​
// 该方法只会被调用一次。计算该RDD和父RDD的关系
protected def getDenpendencies: Seq[Dependency[_]] = deps
​
//对分区进行计算,返回一个可遍历的结果
def compute(split: Partition, context: TaskContext): Iterator[T]
​
//可选的,指定优先位置,输入参数是split分片,输出结果是一组优先的节点位置
protected def getPreferredLocations(split: Partition): Seq(String)= Nil
​
// 可选的,分区的方法,针对第4点,控制分区的计算规则
@transient val partitioner: Option[Partitioner] = None

拿官网上的workcount举例:

val textFile = sc.textFile("文件目录/test.txt")
val counts = textFile.flatMap(line => line.split(" "))
                 .filter(_.length >= 2)
                 .map(word => (word, 1))
                 .reduceByKey(_ + _)
counts.saveAsTextFile("hdfs://...")

这里涉及到几个RDD的转换

1、textfile是一个hadoopRDD经过map转换后的MapPartitionsRDD,

2、经过flatMap后仍然是一个MapPartitionsRDD

3、经过filter方法之后生成了一个新的MapPartitionRDD

4、经过map函数之后,继续是一个MapPartitionsRDD

5、经过最后一个reduceByKey编程了ShuffleRDD

文件分为一个part1,part2,part3经过spark读取之后就变成了HadoopRDD,再按上面流程理解即可

2.3.1、代码分析:SparkContext 类

本次只看textfile方法,注释上说明

Read a text file from HDFS, a local file system (available on all nodes), or any Hadoop-supported file system URI, and return it as an RDD of Strings.
​
读取text文本从hdfs上、本地文件系统,或者hadoop支持的文件系统URI中, 返回一个String类型的RDD

看代码:

hadoopFile最后返回的是一个HadoopRDD对象,然后经过map变换后,转换成MapPartitionsRDD,鱿鱼HadoopRDD没有重写map函数,所以调用的是父类的RDD的map

def textFile(path: String,
      minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
    assertNotStopped() // 忽略不看
    
    hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text], minPartitions)
      .map(pair => pair._2.toString).setName(path)
  }

看下hadoopFile方法

1、广播hadoop的配置文件

2、设置文件的输入格式之类的,也决定的文件的读取方式

3、new HadoopRDD,并返回

def hadoopFile[K, V](path: String,
      inputFormatClass: Class[_ <: InputFormat[K, V]],
      keyClass: Class[K],
      valueClass: Class[V],
      minPartitions: Int = defaultMinPartitions): RDD[(K, V)] = withScope {
    assertNotStopped()
​
    // 做一些校验
    FileSystem.getLocal(hadoopConfiguration)
​
    // A Hadoop configuration can be about 10 KiB, which is pretty big, so broadcast it.
    val confBroadcast = broadcast(new SerializableConfiguration(hadoopConfiguration))
    val setInputPathsFunc = (jobConf: JobConf) => FileInputFormat.setInputPaths(jobConf, path)
    new HadoopRDD(
      this,
      confBroadcast,
      Some(setInputPathsFunc),
      inputFormatClass,
      keyClass,
      valueClass,
      minPartitions).setName(path)
  }

2.3.2、源码分析:HadoopRDD类

先看注释

An RDD that provides core functionality for reading data stored in Hadoop (e.g., files in HDFS, sources in HBase, or S3), using the older MapReduce API (org.apache.hadoop.mapred).

看注释可以知道,HadoopRDD是一个专为Hadoop(HDFS、Hbase、S3)设计的RDD。使用的是以前的MapReduce 的API来读取的。

HadoopRDD extends RDD[(K, V)] 重写了RDD中的三个方法

override def compute(theSplit: Partition, context: TaskContext): InterruptibleIterator[(K, V)] = {}
​
override def getPartitions: Array[Partition] = {}
​
override def getPreferredLocations(split: Partition): Seq[String] = {}

分别来看一下

HadoopRDD#getPartitions

1、读取配置文件

2、通过inputFormat自带的getSplits方法来计算分片,获取所有的Splits

3、创建HadoopPartition的List并返回

这里是不是可以理解,Hadoop中的一个分片,就对应到Spark中的一个Partition

override def getPartitions: Array[Partition] = {
  val jobConf = getJobConf()
    // add the credentials here as this can be called before SparkContext initialized
    SparkHadoopUtil.get.addCredentials(jobConf)
    try {
      // 通过配置的文件读取方式获取所有的Splits
      val allInputSplits = getInputFormat(jobConf).getSplits(jobConf, minPartitions)
      val inputSplits = if (ignoreEmptySplits) {
        allInputSplits.filter(_.getLength > 0)
      } else {
        allInputSplits
      }
      // 创建Partition的List
      val array = new Array[Partition](inputSplits.size)
      for (i <- 0 until inputSplits.size) {
        // 创建HadoopPartition
        array(i) = new HadoopPartition(id, i, inputSplits(i))
      }
      array
    } catch {
      异常处理
    }
}

HadoopRDD#compute

compute的作用主要是 根据输入的partition信息生成一个InterruptibleIterator。

iter中的逻辑主要是

1、把Partition转成HadoopPartition,通过InputSplit创建一个RecordReader

2、重写Iterator的getNext方法,通过创建的reader调用next方法读取下一个值

compute方法通过Partition来获取Iterator接口,以遍历Partition的数据

override def compute(theSplit: Partition, context: TaskContext): InterruptibleIterator[(K, V)] = {
    val iter = new NextIterator[(K, V)] {...}
    new InterruptibleIterator[(K, V)](context, iter)
  }
 override def compute(theSplit: Partition, context: TaskContext): InterruptibleIterator[(K, V)] = {
​
 val iter = new NextIterator[(K, V)] {
​
      //将compute的输入theSplit,转换为HadoopPartition
      val split = theSplit.asInstanceOf[HadoopPartition]
      ......
      //c重写getNext方法
      override def getNext(): (K, V) = {
        try {
          finished = !reader.next(key, value)
        } catch {
          case _: EOFException if ignoreCorruptFiles => finished = true
        }
        if (!finished) {
          inputMetrics.incRecordsRead(1)
        }
        (key, value)
      }
     }
}

HadoopRDD#getPreferredLocations

getPreferredLocations方法比较简单,直接调用SplitInfoReflections下的inputSplitWithLocationInfo方法获得所在的位置。

override def getPreferredLocations(split: Partition): Seq[String] = {
  val hsplit = split.asInstanceOf[HadoopPartition].inputSplit.value
  val locs: Option[Seq[String]] = HadoopRDD.SPLIT_INFO_REFLECTIONS match {
    case Some(c) =>
      try {
        val lsplit = c.inputSplitWithLocationInfo.cast(hsplit)
        val infos = c.getLocationInfo.invoke(lsplit).asInstanceOf[Array[AnyRef]]
        Some(HadoopRDD.convertSplitLocationInfo(infos))
      } catch {
        case e: Exception =>
          logDebug("Failed to use InputSplitWithLocations.", e)
          None
      }
    case None => None
  }
  locs.getOrElse(hsplit.getLocations.filter(_ != "localhost"))
}

2.3.3、源码分析:MapHadoopRDD类
An RDD that applies the provided function to every partition of the parent RDD.

经过RDD提供的function处理后的 父RDD 将会变成MapHadoopRDD

MapHadoopRDD重写了父类的partitioner、getPartitions和compute方法

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

在partitioner、getPartitions、compute中都用到了一个firstParent函数,可以看到,在MapPartition中并没有重写partitioner和getPartitions方法,只是从firstParent中取了出来

再看下firstParent是干什么的,其实就是取的父依赖

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

再看一下MapPartitionsRDD继承的RDD,它继承的是RDD[U] (prev),这里的prev指的是我们的HadoopRDD,也就是说HadoopRDD变成了我们这个MapPartitionRDD的OneToOneDependency依赖,OneToOneDependency是窄依赖

def this(@transient oneParent: RDD[_]) =
    this(oneParent.context , List(new OneToOneDependency(oneParent)))

再来看map方法

/**
 * Return a new RDD by applying a function to all elements of this RDD.
 * 通过将函数应用于新RDD的所有元素,返回新的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))
}

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

filter方法

/**
  * Return a new RDD containing only the elements that satisfy a predicate.
  * 返回仅包含满足表达式 的元素的新RDD。
  */
 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)
 }

观察代码发现,他们返回的都是MapPartitionsRDD对象,不同的仅仅是传入的function不同而已,经过前面的分析,这些都是窄依赖

注意:这里我们可以明白了MapPartitionsRDD的compute方法的作用了:

1、在没有依赖的条件下,根据分片的信息生成遍历数据的iterable接口

2、在有前置依赖的条件下,在父RDD的iterable接口上给遍历每个元素的时候再套上一个方法

2.3.4、源码分析:PairRDDFunctions 类

接下来,该reduceByKey操作了。它在PairRDDFunctions里面

reduceByKey稍微复杂一点,因为这里有一个同相同key的内容聚合的一个过程,它调用的是combineByKey方法。

/**
   * Merge the values for each key using an associative reduce function. This will also perform
   * the merging locally on each mapper before sending results to a reducer, similarly to a
   * "combiner" in MapReduce.
   */
  def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = self.withScope {
    combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
  }
​
    /**
   * Generic function to combine the elements for each key using a custom set of aggregation
   泛型函数,将每个key的元素 通过自定义的聚合 来组合到一起
   * functions. Turns an RDD[(K, V)] into a result of type RDD[(K, C)], for a "combined type" C
   *
   * Users provide three functions:
   *
   *  - `createCombiner`, which turns a V into a C (e.g., creates a one-element list)
   *  - `mergeValue`, to merge a V into a C (e.g., adds it to the end of a list)
   *  - `mergeCombiners`, to combine two C's into a single one.
   *
   * In addition, users can control the partitioning of the output RDD, and whether to perform
   * map-side aggregation (if a mapper can produce multiple items with the same key).
   *
   * @note V and C can be different -- for example, one might group an RDD of type
   * (Int, Int) into an RDD of type (Int, Seq[Int]).
   */
  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
    // 判断keyclass是不是array类型,如果是array并且在两种情况下throw exception。
    if (keyClass.isArray) {
      if (mapSideCombine) {
        throw SparkCoreErrors.cannotUseMapSideCombiningWithArrayKeyError()
      }
      if (partitioner.isInstanceOf[HashPartitioner]) {
        throw SparkCoreErrors.hashPartitionerCannotPartitionArrayKeyError()
      }
    }
    val aggregator = new Aggregator[K, V, C](
      self.context.clean(createCombiner),
      self.context.clean(mergeValue),
      self.context.clean(mergeCombiners))
    //虽然不太明白,但是此处基本上一直是false,感兴趣的看后面的参考文章
    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)
    }
  }

2.3.5、源码分析:ShuffledRDD类

看上面代码最后传入了self和partitioner ,并set了三个值,shuffled过程暂时不做解析。这里看下ShuffledRDD的依赖关系(getDependencies方法),它是一个宽依赖

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

总结:我们讲了RDD的基本组成结构,也通过一个wordcount程序举例来说明代码是如果运行的,希望大家可以从源码入手,学习spark,共勉!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/355078.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

嵌入式Linux系统引导过程中的设备驱动加载

大家好&#xff0c;今天给大家介绍**嵌入式Linux系统引导过程中的设备驱动加载&#xff0c;**文章末尾附有分享大家一个资料包&#xff0c;差不多150多G。里面学习内容、面经、项目都比较新也比较全&#xff01;可进群免费领取。 **在嵌入式Linux系统的引导过程中&#xff0c…

杨占华主任:冠心病,中医好还是西医好

冠心病是一种慢性疾病&#xff0c;治疗时间相对较长&#xff0c;恢复缓慢。一般来说&#xff0c;冠心病患者也面临着巨大的心理压力。选择中药治疗好还是西药治疗好&#xff1f; 冠心病的西医治疗主要包括药物治疗、冠状动脉支架植入和冠状动脉搭桥术。 中医治疗冠心病的主要方…

Open CASCADE学习|读取STEP文件并显示

STEP文件是基于ISO 10303标准创建的三维模型数据交换文件&#xff0c;也称为产品模型数据交换标准&#xff08;Standard Exchange of Product data model&#xff09;。这种文件格式旨在提供一个不依赖具体系统的中性机制&#xff0c;实现产品数据的交换和共享。 STEP文件是一…

OJ_键盘输入问题

题干 c语言实现 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<vector> #include<map> using namespace std;int main() {//记录每个字母需要花费多少时间map<char, int> inputTime {{a,1},{b,2},{c,3},{d,1},{e,2},{f,3},{g,1},{h,2…

RK3568平台开发系列讲解(Linux系统篇)中断线程化

🚀返回专栏总目录 文章目录 一、什么是中断线程化二、中断线程化接口函数三、使用案例沉淀、分享、成长,让自己和他人都能有所收获!😄 一、什么是中断线程化 在Linux中,中断线程化(Interrupt Thread)是一种处理中断的方式,它允许将中断处理程序执行的部分移动到一个单…

C51 单片机学习(二):定时器与中断系统

参考 51单片机入门教程 1. 定时器 1.1 定时器定义 51 单片机的定时器属于单片机的内部资源&#xff0c;其电路的连接和运转均在单片机内部完成 C51 单片机学习&#xff08;一&#xff09;&#xff1a;基础外设 讲的都是单片机的 IO 口控制的外设 1.2 定时器作用 用于计时系…

Java算法---递归算法基础介绍

目录 一、递归算法 二、递归算法的典型例子 &#xff08;1&#xff09;阶乘 &#xff08;2&#xff09;二分查找 &#xff08;3&#xff09;冒泡排序 &#xff08;4&#xff09;插入排序 一、递归算法 计算机科学中&#xff0c;递归是一种解决计算问题的方法。其中解决方案…

yum指令——Linux的软件包管理器

. 个人主页&#xff1a;晓风飞 专栏&#xff1a;数据结构|Linux|C语言 路漫漫其修远兮&#xff0c;吾将上下而求索 文章目录 什么是软件包yum指令1.yum 是什么&#xff1f;2.Linux系统&#xff08;Centos&#xff09;的生态 3.yum的相关操作安装卸载yum的相关操作小结 软件源安…

Python判断语句——布尔类型和比较运算符

一、引言 在Python编程语言中&#xff0c;布尔类型和比较运算符是基础而又至关重要的概念。它们是构建逻辑和决策的核心要素&#xff0c;让程序能够理解“真”与“假”、以及各种数量和值之间的关系。理解并掌握这些概念&#xff0c;对于编写高效、准确的代码至关重要。在本文…

OpenHarmony—仅允许在表达式中使用typeof运算符

规则&#xff1a;arkts-no-type-query 级别&#xff1a;错误 ArkTS仅支持在表达式中使用typeof运算符&#xff0c;不允许使用typeof作为类型。 TypeScript let n1 42; let s1 foo; console.log(typeof n1); // number console.log(typeof s1); // string let n2: typeof …

【极数系列】Flink集成DataSource读取集合数据(07)

文章目录 01 引言02 简介概述03 基于集合读取数据3.1 集合创建数据流3.2 迭代器创建数据流3.3 给定对象创建数据流3.4 迭代并行器创建数据流3.5 基于时间间隔创建数据流3.6 自定义数据流 04 源码实战demo4.1 pom.xml依赖4.2 创建集合数据流作业4.3 运行结果日志 01 引言 源码地…

基于Python 爬虫的房地产数据可视化分析与实现

摘要&#xff1a; 过去&#xff0c;不管是翻阅书籍&#xff0c;还是通过手机&#xff0c;电脑等从互联网上手动点击搜索信息&#xff0c;视野受限&#xff0c;信息面太过于狭窄&#xff0c;且数据量大而杂乱&#xff0c;爆炸式信息的更新速度是快速且不定时的。要想手动获取到海…

OpenAI、斯坦福大学提出Meta-Prompting,有效提升语言模型的性能

为了研究如何提高语言模型的性能&#xff0c;使其更充分有效地输出对于提问的回答&#xff0c;来自斯坦福和 OpenAI 的学者强强联手&#xff0c;通过提出一种名为元提示&#xff08;meta-prompting&#xff09;的方法来深入探索。元提示通过让单个语言模型&#xff08;如 GPT-4…

【JavaScript基础入门】04 JavaScript基础语法(二)

JavaScript基础语法&#xff08;二&#xff09; 目录 JavaScript基础语法&#xff08;二&#xff09;变量变量是什么声明变量变量类型动态类型注释 数字与运算符数字类型算术运算符操作运算符比较运算符逻辑运算符运算符的优先级 变量 变量是什么 在计算机中&#xff0c;数据…

练习12.6_横向射击_Python编程:从入门到实践(第3版)

编写一个游戏&#xff0c;将一艘飞船放在屏幕左侧&#xff0c;并允许玩家上下移动飞船。在玩家按空格键时&#xff0c; 让飞船发射一颗在屏幕中向右飞行的子弹&#xff0c;并在子弹从屏幕中消失后将其删除。 ship_shooting.py import pygame import sys from leftship impor…

RabbitMQ基础编程模型及详细使用

目录 RabbitMQ基础编程模型 引入依赖 创建连接&#xff0c;获取Channel 声明Exchange-可选 声明queue 声明Exchange与Queue的绑定关系-可选 Producer根据应用场景发送消息到queue Consumer消费消息 Consumer主要有两种消费方式 1、被动消费模式 2、主动消费模式 完成…

sqli-labs闯关

目录 1.安装靶场2.了解几个sql常用知识2.1联合查询union用法2.2MySQL中的通配符&#xff1a;2.3常用函数2.4数据分组 3.mysql中重要的数据库和表4.开始闯关4.1 Less-14.1.1 首先进行一次常规的注入4.1.2 深入解析 1.安装靶场 1.首先推荐使用github下载靶场源码 https://githu…

内网安全:PTH PTK PTT

目录 实验所用网络拓朴图 网络环境说明​​​​​​​ LM认证 NTLM认证 NTLM Hash Kerberos认证 TGT票据 服务票据 Windows系统密码存储 域控制器 - 用户登录 域用户 本地用户 域用户和本地管理员 用户登录 Mimikatz抓取密码来源 域内一台主机上可以得到非本地用…

js实现贪吃蛇

文章目录 实现方法_11实现效果2 实现步骤2.1 移动场地2.2 游戏难度2.3 造蛇和食物2.4 蛇的移动2.5 产生食物的随机位置 3 全部代码 实现方法_21 实现效果2实现想法2.1 蛇的存储 实现方法_1 1实现效果 2 实现步骤 html部分忽略&#xff0c;布局写的太辣眼了 2.1 移动场地 用的表…

遥感的CCDC连续变化监测的qgis插件

简介 今天我逛GitHub的时候&#xff0c;看到一个比较有意思的插件:CCD-Plugin&#xff0c;记录一下。 CCD-Plugin是一个qgis插件&#xff0c;它使用 Google Earth Engine 获取 Landsat 或 Sentinel2 数据集&#xff0c;并运行连续变化检测 (CCDC) 算法来分析给定点的多年时间序…