Spark原理——逻辑执行图

逻辑执行图

  • 明确逻辑计划的边界

    在 Action 调用之前,会生成一系列的RDD,这些RDD之间的关系,其实就是整个逻辑计划

    val conf= new SparkConf().setMaster("local[6]").setAppName("wordCount_source")
    val sc= new SparkContext(conf)val textRDD=sc.parallelize(Seq("Hadoop Spark", "Hadoop Flume", "Spark Sqoop"))
    val splitRDD=textRDD.flatMap(_.split(" "))
    val tupleRDD=splitRDD.map((_, 1))
    val reduceRDD=tupleRDD.reduceByKey(_ + _)
    val strRDD=reduceRDD.map(item => s"${item._1},${item._2}")println(strRDD.toDebugString)
    strRDD.collect.foreach(item =>println(item))
    

    例如上述代码,如果生成逻辑计划的,会生成效如下一些RDD,这些RDD是相互关联的,这些RDD之间,其实本质上生成的就是一个 计算链

    在这里插入图片描述

    接下来, 采用迭代渐进式的方式, 一步一步的查看一下整体上的生成过程

  • RDD 如何生成

    • **textFile算子的背后**

      研究 RDD 的功能或者表现的时候, 其实本质上研究的就是 RDD中的五大属性, 因为 RDD透过五大属性来提供功能和表现, 所以如果要研究 textFile 这个算子, 应该从五大属性着手, 那么第一步就要看看生成的 RDD是什么类型的 RDD

      1. **textFile 生成的是 HadoopRDD**

        在这里插入图片描述

        在这里插入图片描述

        在这里插入图片描述

      2. **HadoopRDDPartitions对应了 HDFSBlocks**

        在这里插入图片描述

        其实本质上每个 HadoopRDDPartition都是对应了一个 HadoopBlock, 通过 InputFormat 来确定 Hadoop中的 Block 的位置和边界, 从而可以供一些算子使用

        在这里插入图片描述

      3. HadoopRDDcompute函数就是在读取 HDFS中的 Block

        本质上, compute 还是依然使用 InputFormat来读取 HDFS中对应分区的 Block

      4. textFile这个算子生成的其实是一个 MapPartitionsRDD

        textFile这个算子的作用是读取 HDFS上的文件, 但是 HadoopRDD中存放是一个元组, 其 Key是行号, 其 ValueHadoop中定义的 Text对象, 这一点和 MapReduce程序中的行为是一致的

        但是并不适合 Spark 的场景, 所以最终会通过一个 map算子, 将 (LineNum, Text)转为 String形式的一行一行的数据, 所以最终 textFile这个算子生成的 RDD 并不是 HadoopRDD, 而是一个 MapPartitionsRDD

    • map算子的背后

      在这里插入图片描述

      • map算子生成了 MapPartitionsRDD

         val conf= new SparkConf().setMaster("local[6]").setAppName("wordCount_source")
        val sc= new SparkContext(conf)val rdd=sc.parallelize(Seq("Hadoop Spark", "Hadoop Flume", "Spark Sqoop"))
        val rdd1=rdd.flatMap(_.split(" "))
        val rdd2=rdd1.map((_, 1))
        

        由源码可知, 当 val rdd2 = rdd1.map() 的时候, 其实生成的新 RDD 是 rdd2, rdd2 的类型是 MapPartitionsRDD, 每个 RDD 中的五大属性都会有一些不同, 由 map 算子生成的 RDD 中的计算函数, 本质上就是遍历对应分区的数据, 将每一个数据转成另外的形式

      • MapPartitionsRDD的计算函数是 collection.map( function )

        真正运行的集群中的处理单元是 Task, 每个 Task 对应一个 RDD 的分区, 所以 collection 对应一个 RDD 分区的所有数据, 而这个计算的含义就是将一个 RDD 的分区上所有数据当作一个集合, 通过这个 Scala 集合的 map 算子, 来执行一个转换操作, 其转换操作的函数就是传入 map 算子的 function

      • 传入 map算子的函数会被清理

        在这里插入图片描述

        这个清理主要是处理闭包中的依赖, 使得这个闭包可以被序列化发往不同的集群节点运行

    • flatMap算子的背后

      在这里插入图片描述

      flatMapmap算子其实本质上是一样的, 其步骤和生成的 RDD都是一样, 只是对于传入函数的处理不同, mapcollect.map( function )flatMapcollect.flatMap( function )

      从侧面印证了, 其实 Spark中的 flatMapScala基础中的 flatMap其实是一样的

      val conf= new SparkConf().setMaster("local[6]").setAppName("wordCount_source")
      val sc= new SparkContext(conf)val textRDD=sc.parallelize(Seq("Hadoop Spark", "Hadoop Flume", "Spark Sqoop"))
      val splitRDD=textRDD.flatMap(_.split(" "))
      val tupleRDD=splitRDD.map((_, 1))
      val reduceRDD=tupleRDD.reduceByKey(_ + _)
      val strRDD=reduceRDD.map(item => s"${item._1},${item._2}")// println(strRDD.toDebugString)
      strRDD.collect.foreach(item =>println(item))
      

      textRDDsplitRDDtupleRDD

      textRDDsplitRDD再到 tupleRDD的过程, 其实就是调用 mapflatMap算子生成新的 RDD的过程, 所以如下图所示, 就是这个阶段所生成的逻辑计划

  • RDD 之间有哪些依赖关系

    • 前置说明

      • 什么是RDD之间的依赖关系?

        在这里插入图片描述

        • 什么是关系(依赖关系)?

          从算子视角上来看,splitRDD 通过 map 算子得到了 tupleRDD ,所以 splitRDD 和 tupleRDD 之间的关系是 map, 但是仅仅这样说,会不够全面,从细节上来看,RDD只是数据和关于数据的计算,而具体执这种计算得出结果的是一个神秘的其它组件,所以,这两个 RDD 的关系可以表示为 splitRDD 的数据通过 map 操作,被传入 tupleRDD ,这是它们之间更细化的关系

          但是 RDD 这个概念本身并不是数据容器,数据真正应该存放的地方是 RDD 的分区,所以如果把视角放在数据这一层面上的话,直接讲这两个 RDD 之间有关系是不科学的,应该从这两个 RDD 的分区之间的关系来讨论它们之间的关系

        • 那这些分区之间是什么关系?

          如果仅仅说 splitRDD 和 tupleRDD 之间的话,那它们的分区之间就是一对一的关系

          但是 tupleRDD 到 reduceRDD 呢?tupleRDD 通过算子 reduceByKey 生成 reduceRDD ,而这个算子是一个 Shuffle 操作,Shuff1e 操作的两个 RDD 的分区之间并不是一对一,reduceByKey 的一个分区对应 tupleRDD 的多个分区

      • reduceByKey 算子会生成 ShuffledRDD

        reduceByKey 是由算子 combineByKey 来实现的,combineByKey 内部会创建 ShuffledRDD 返回,而整个reduceByKey操作大致如下过程

        在这里插入图片描述

        去掉两个 reducer 端分区,只留下一个的话,如下

        在这里插入图片描述

        所以, 对于 reduceByKey 这个 Shuffle 操作来说, reducer 端的一个分区, 会从多个 mapper 端的分区拿取数据, 是一个多对一的关系

        至此为止, 出现了两种分区见的关系了, 一种是一对一, 一种是多对一

    • 窄依赖

      窄依赖(NarrowDependency)

      假如 rddB = rddA.transform(…), 如果 rddB 中一个分区依赖 rddA 也就是其父 RDD 的少量分区, 这种 RDD 之间的依赖关系称之为窄依赖

      换句话说, 子 RDD 的每个分区依赖父 RDD 的少量个数的分区, 这种依赖关系称之为窄依赖

      @Test
      def narrowDependency(): Unit = {// 需求:求得两个 RDD 之间的笛卡尔积// 1. 生成 RDDval conf = new SparkConf().setMaster("local[6]").setAppName("cartesian")val sc = new SparkContext(conf)val rddA = sc.parallelize(Seq(1, 2, 3))val rddB = sc.parallelize(Seq("a", "b"))// 2. 计算val rddC = rdd1.cartesian(rdd2)// 3. 结果获取rddC.collect().foreach(print(_))sc.stop()
      }
      // 运行结果: (1,a) (1,b) (2,a) (2,b) (3,a) (3,b)
      
      • 上述代码的 cartesian 是求得两个集合的笛卡尔积
      • 上述代码的运行结果是 rddA 中每个元素和 rddB 中的所有元素结合, 最终的结果数量是两个 RDD 数量之和
      • rddC 有两个父 RDD, 分别为 rddA 和 rddB

      对于 cartesian 来说, 依赖关系如下

      在这里插入图片描述

      上述图形中清晰展示如下现象

      • rddC 中的分区数量是两个父 RDD 的分区数量之乘积
      • rddA 中每个分区对应 rddC 中的两个分区 (因为 rddB 中有两个分区), rddB 中的每个分区对应 rddC 中的三个分区 (因为 rddA 有三个分区)

      它们之间是窄依赖, 事实上在 cartesian 中也是 NarrowDependency 这个所有窄依赖的父类的唯一一次直接使用, 为什么呢?

      因为所有的分区之间是拷贝关系, 并不是 Shuffle 关系

      • rddC 中的每个分区并不是依赖多个父 RDD 中的多个分区
      • rddC 中每个分区的数量来自一个父 RDD 分区中的所有数据, 是一个 FullDependence, 所以数据可以直接从父 RDD 流动到子 RDD
      • 不存在一个父 RDD 中一部分数据分发过去, 另一部分分发给其它的 RDD
    • 宽依赖

      宽依赖(ShuffleDependency

      并没有所谓的宽依赖, 宽依赖应该称作为 ShuffleDependency

      在 ShuffleDependency 的类声明上如下写到

      Represents a dependency on the output of a shuffle stage.
      

      上面非常清楚的说道, 宽依赖就是 Shuffle 中的依赖关系, 换句话说, 只有 Shuffle 产生的地方才是宽依赖

      那么宽窄依赖的判断依据就非常简单明确了, 是否有 Shuffle ?

      举个 reduceByKey 的例子, rddB = rddA.reduceByKey( (curr, agg) ⇒ curr + agg ) 会产生如下的依赖关系

      在这里插入图片描述

      在这里插入图片描述

      • rddB 的每个分区都几乎依赖 rddA 的所有分区
      • 对于 rddA 中的一个分区来说, 其将一部分分发给 rddB 的 p1, 另外一部分分发给 rddB 的 p2, 这不是数据流动, 而是分发
    • 如何分辨宽窄依赖 ?

      其实分辨宽窄依赖的本身就是在分辨父子 RDD 之间是否有 Shuffle, 大致有以下的方法

      • 如果是 Shuffle, 两个 RDD 的分区之间不是单纯的数据流动, 而是分发和复制
      • 一般 Shuffle 的子 RDD 的每个分区会依赖父 RDD 的多个分区

      先看是否一对一>是就是窄依赖,如果不是一对一,是多对一>不能确定,再继续判断

      但是这样判断其实不准确, 如果想分辨某个算子是否是窄依赖, 或者是否是宽依赖, 则还是要取决于具体的算子, 例如想看 cartesian 生成的是宽依赖还是窄依赖, 可以通过如下步骤

      1. 查看 map 算子生成的 RDD

        在这里插入图片描述

        在这里插入图片描述

        在这里插入图片描述

      2. 进去 RDD 查看 getDependence 方法

        在这里插入图片描述

        在这里插入图片描述

        在这里插入图片描述

        在这里插入图片描述
        724024159.png?origin_url=Untitled%2520180.png&pos_id=img-mqipe2i4-1704890196048)

    • 常见的窄依赖类型

      Dependency.scala 源码有。

      在这里插入图片描述

      • 一对一窄依赖

        其实 RDD 中默认的是 OneToOneDependency, 后被不同的 RDD 子类指定为其它的依赖类型, 常见的一对一依赖是 map 算子所产生的依赖, 例如 rddB = rddA.map(…)

        在这里插入图片描述

        • 每个分区之间一一对应, 所以叫做一对一窄依赖
      • Range 窄依赖

        Range 窄依赖其实也是一对一窄依赖, 但是保留了中间的分隔信息, 可以通过某个分区获取其父分区, 目前只有一个算子生成这种窄依赖, 就是 union 算子, 例如 rddC = rddA.union(rddB)

        在这里插入图片描述

        • rddC 其实就是 rddA 拼接 rddB 生成的, 所以 rddC 的 p5 和 p6 就是 rddB 的 p1 和 p2
        • 所以需要有方式获取到 rddC 的 p5 其父分区是谁, 于是就需要记录一下边界, 其它部分和一对一窄依赖一样
      • 多对一窄依赖

        多对一窄依赖其图形和 Shuffle 依赖非常相似, 所以在遇到的时候, 要注意其 RDD 之间是否有 Shuffle 过程, 比较容易让人困惑, 常见的多对一依赖就是重分区算子 coalesce, 例如 rddB = rddA.coalesce(2, shuffle = false), 但同时也要注意, 如果 shuffle = true 那就是完全不同的情况了

        在这里插入图片描述

        • 因为没有 Shuffle, 所以这是一个窄依赖
      • 再谈宽窄依赖的区别

        • 宽窄依赖的区别非常重要, 因为涉及了一件非常重要的事情: 如何计算 RDD ?
        • 宽窄依赖的核心区别是: 窄依赖的 RDD 可以放在一个 Task 中运行

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

利用HTML和CSS实现的浮动布局

代码如下 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><style>*{m…

使用 vsCode创建GO项目

最近回顾了一下go的使用&#xff1a;具体操作看下面的参考连接&#xff0c;下面只描述一些踩过的坑&#xff1a; 1. go安装配置 安装go->配置go环境变量 推荐官网下载&#xff0c;速度很快&#xff1b; 这里需要配置五个参数&#xff1a;GOPATH/GOROOT/Path、GO111MODULE/…

stable-diffusion 学习笔记

必看文档&#xff1a; 万字长篇&#xff01;超全Stable Diffusion AI绘画参数及原理详解 - 知乎 &#xff08;提示词&#xff09;语法控制 常用语法&#xff1a; 加权&#xff1a;() 或 {} 降权&#xff1a;[](word)//将括号内的提示词权重提高 1.1 倍 ((word))//将括号内的提示…

掌握Spring缓存-全面指南与最佳实践

第1章&#xff1a;引言 大家好&#xff0c;我是小黑&#xff0c;咱们今天来聊聊缓存&#xff0c;在Java和Spring里&#xff0c;缓存可是个大角色。咱们在网上购物&#xff0c;每次查看商品详情时&#xff0c;如果服务器都要去数据库里翻箱倒柜&#xff0c;那速度得慢成什么样&…

基于Java+SpringBoot+vue实现图书借阅和销售商城一体化系统

基于JavaSpringBootvue实现图书借阅和销售商城一体化系统 &#x1f345; 作者主页 央顺技术团队 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; &#x1f345; 查看下方微信号获取联系方式 承接各种定制系…

Vue3中使用自定义指令

一&#xff0c;自定义指令&#xff1a; 应用场景&#xff1a;禁用按钮多次点击 1.vue2 a. src/libs/preventClick.js import Vue from vue const preventClick Vue.directive(preventClick, {inserted: function (el, binding) {el.addEventListener(click, () > {if (!el…

Arm Generic Interrupt Controller v3 and v4(GICv3v4)学习(一)

提示 该博客主要为个人学习&#xff0c;通过阅读官网手册整理而来&#xff08;个人觉得阅读官网的英文文档非常有助于理解各个IP特性&#xff09;。若有不对之处请参考参考文档&#xff0c;以官网参考文档为准。 Arm Generic Interrupt Controller v3 and v4学习一共分为三章&…

剑指 Offer 37. 序列化二叉树

剑指 Offer 37. 序列化二叉树 剑指 Offer 37. 序列化二叉树 序列化是将一个数据结构或者对象转换为连续的比特位的操作&#xff0c;进而可以将转换后的数据存储在一个文件或者内存中&#xff0c;同时也可以通过网络传输到另一个计算机环境&#xff0c;采取相反方式重构得到原…

Linux 系统之部署 h5ai 目录列表程序

一、h5ai 介绍 1.1&#xff09;h5ai 简介 h5ai 是用于 HTTP Web 服务器的现代文件索引器&#xff0c;专注于您的文件。目录以吸引人的方式显示&#xff0c;浏览它们通过不同的视图、面包屑和树概述得到增强。最初 h5ai 是 HTML5 Apache Index 的首字母缩写&#xff0c;但现在它…

计算机导论05-计算机网络

文章目录 计算机网络基础计算机网络概述计算机网络的概念计算机网络的功能计算机网络的组成 计算机网络的发展计算机网络的类型 网络体系结构网络互联模型OSI/RM结构与功能TCP/IP结构模型TCP/IP与OSI/RM的比较 网络地址与分配IP地址构成子网的划分IPv6 传输介质与网络设备网络传…

视频转码实例:把MP4转为MKV视频,一键批量转换的操作方法

在数字媒体时代&#xff0c;视频格式的多样性是不可避免的。经常把MP4格式的视频转换为MKV格式。MKV格式有较高的音频和视频质量&#xff0c;能在其他设备或软件上播放视频。以下是云炫AI智剪如何把MP4视频转为MKV格式的一键批量转换操作方法。 已转码的mkv视频效果缩略图展示…

CSS3中多列布局详解

多列布局 概念&#xff1a;在CSS3之前&#xff0c;想要设计类似报纸那样的多列布局&#xff0c;有两种方式可以实现&#xff1a;一种是"浮动布局"&#xff0c;另一种是“定位布局”。 这两种方式都有缺点&#xff1a;浮动布局比较灵活&#xff0c;但不容易控制&…