一篇文章认识spark框架和核心数据结构RDD

文章详细介绍了Spark的运行框架,包括主从结构、Driver和Executor的角色。讨论了核心概念如Executor的资源分配,以及并行度的概念。还深入讲解了Spark在Yarn上的提交模式,包括YarnClient和YarnCluster的区别。此外,文章阐述了RDD作为核心数据结构的特性,以及如何创建和操作RDD进行分布式计算。

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

2 Spark运行架构

2.1 运行框架

​ Spark框架的核心是一个计算引擎,采用了标准的主从结构,也就是master-worker结构。如下图所示,Driver 表示master,负责集群的作业任务调度,而executor就是worker,负责实际任务的执行

在这里插入图片描述

2.2 核心组件

2.2.1 Driver

​ Spark的驱动节点,接收任务的接口,Spark作业执行时主要负责:

  • 在Executor之间安排并调度任务
  • 跟踪Executor情况

2.2.2 Executor

​ Spark Executor 是集群中工作节点(Worker)中的一个 JVM 进程,负责在 Spark 作业中运行具体任务(Task),任务彼此之间相互独立。Spark 应用启动时,Executor 节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有 Executor 节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他 Executor 节点上继续运行。

2.3 核心概念

2.3.1 executor 与 core

​ Executor本质上是一个进程,是Spark专门用于执行任务的节点,在启动时可以给其分配资源,所谓的资源就是工作节点Executor的内存大小以及虚拟CPU核数。

参数说明
–num-executor配置工作节点的个数
–executor-memory配置executor的内存大小
–executor-cores每个进程的虚拟cpu核数

2.3.2 并行度

在分布式计算框架中一般都是多个任务同时执行,由于任务分布在不同的计算节点进行计算,所以能够真正地实现多任务并行执行,这里我们将 整个集群并行执行任务的数量称之为并行度。并行度 = executor * cores(当然不能操作计算机最大情况)

  • 并行:多个任务同时在多个进程上进行
  • 并发:多个任务同时申请在同一个进程上执行

2.4 提交模式(yarn)

​ 一般的提交流程如下图所示,但具体还有所不同,Spark 应用程序提交到 Yarn 环境中执行的时候,一般会有两种部署执行的方式:Client 和 Cluster。两种模式主要区别在于:Driver 程序的运行节点位置

2.4.1 Yarn架构

  • Yarn是一个资源调度的工具,在多个任务执行时,集群的资源分配,其组成角色可以分为:

    • resource manager:整个集群的资源管理者
    • NodeManager:当个服务器的资源管理者
    • ApplicationMaster:单个任务的管理者
    • container:容器,存放资源
  • 既然是集群,那就是有个总的管理者:resource manager,他管理着整个集群的资源,所有资源都需要通过他的调控,各个服务器也有自己的管理者:NodeManager,管理该服务器的资源。所谓资源调度,就是在执行多个任务时的资源分配,既然有任务,那就得有一个角色来负责处理这个任务,那就是 ApplicationMaster,他管理单个任务,既然资源能分配,那就得有东西来储存你分配的资源,因此就有 container(容器) 来储存资源。

  • 合作流程:假设现在有job请求,那么resource Manager会首先收到情况,然后在合适的nodemanager上创建一个ApplicationMaster来接待它,ApplicationMaste首先对job的情况进行了解,明白了job需要的资源数量后,告诉resource manager情况,resource manager,会根据job的情况在合适的nodemanager上分配一个container,然后application会在该container上完成job任务。

    (可以想象成一个顾客买东西的时候)

图片来源与于尚硅谷教堂在这里插入图片描述

2.4.2 Yarn Client

在这里插入图片描述

​ Client 模式将用于监控和调度的 Driver 模块在客户端执行,而不是在 Yarn 中,所以一般用于测试。

  • Driver 在任务提交的本地机器上运行
  • Driver 启动后会和 ResourceManager 通讯申请启动 ApplicationMaster
  • ResourceManager 分配 container,在合适的 NodeManager 上启动 ApplicationMaster,负责向 ResourceManager 申请 Executor 内存
  • ResourceManager 接到 ApplicationMaster 的资源申请后会分配 container,然后 ApplicationMaster 在资源分配指定的 NodeManager 上启动 Executor 进程
  • Executor 进程启动后会向 Driver 反向注册,Executor 全部注册完成后 Driver 开始执行 main 函数
  • 之后执行到 Action 算子时,触发一个 Job,并根据宽依赖开始划分 stage,每个 stage 生 成对应的 TaskSet,之后将 task 分发到各个 Executor 上执行。

2.4.3 Yarn cluster

​ Cluster 模式将用于监控和调度的 Driver 模块启动在 **Yarn 集群资源中执行。**一般应用于 实际生产环境。

  • 在 YARN Cluster 模式下,任务提交后会和 ResourceManager 通讯申请启动 ApplicationMaster
  • 随后 ResourceManager 分配 container,在合适的 NodeManager 上启动 ApplicationMaster, 此时的 ApplicationMaster 就是 Driver。
  • Driver 启动后向 ResourceManager 申请 Executor 内存,ResourceManager 接到 ApplicationMaster 的资源申请后会分配 container,然后在合适的 NodeManager 上启动 Executor 进程
  • Executor 进程启动后会向 Driver 反向注册,Executor 全部注册完成后 Driver 开始执行 main 函数
  • 之后执行到 Action 算子时,触发一个 Job,并根据宽依赖开始划分 stage,每个 stage 生 成对应的 TaskSet,之后将 task 分发到各个 Executor 上执行。

2.5 分布式计算

​ 当Driver接收到job任务时,其将根据任务的逻辑对数据进行分块,并将逻辑和数据块进行封装成新的Task,然后传递给 Executor进行执行,执行完毕后的结果重新传递给Driver进行聚合,这就是所谓的分布式计算。注意:

  • Driver接收到的Jobs中,既包含逻辑,也包含数据。
  • 分块后的Task 也包含逻辑以及数据,只不过数据不是全部数据,而是job数据中的一部分

在这里插入图片描述

3 核心概念

​ Spark 计算框架为了能够进行高并发和高吞吐的数据处理,主要有三大数据结构,用于 处理不同的应用场景。三大数据结构分别是:

  • RDD,弹性分布式数据集
  • 累加器:分布式共享只写变量
  • 广播变量:分布式共享只读变量

3.1 RDD

3.1.1 什么是RDD

​ IO指input 和 output 过程,RDD IO 和正常IO及其相似。主要的IO方式有字节流IO,字符流IO

a) 字节流IO

​ 输入和输出都是以字节为单位进行操作的,因此称之为字节流IO(stream IO),过程以下方粒子所示。

在此代码中,每读一个字节就会输出一个字节,其过程就是 input-output -input 之间不断循环,每次input后将执行一次output后才会再一次进行input,读取效率较低

InputStream in  = new FIleInputStream("path")
    int i = -1
    while ((i = in.read()) ){
        println(i);
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传在这里插入图片描述

​ 为改善其读一个输出一个导致的效率低下,通常情况下会引入一个缓冲区,来暂时存放数据,见下方例子。

​ 见下图,所谓缓存区(Buff)作用就是将读取到的字节先放在Buff中,等Buff 中数据量达到阈值就将其输出,因此每次input不需要等待 output。(这也是所谓的批处理思想)

在这里插入图片描述

b) 字符流IO

​ 将字节转化为字符,再进行输出,就是所谓的字符流IO,其本质其实就是对字节流进行修饰。如下图为字节流IO的模型,其本质就是将字节流IO进行包装成字符流:当读入字节后、储存在InputStreamReader中,当组成一个字符后,将传递到Buff中进行缓存,达到缓存阈值后再输出于工作台

​ 如图所示,IO模式体现了装饰者设计模式:将最基础的类进行不断的封装,达到一个功能强大的类

在这里插入图片描述

在这里插入图片描述

c) RDD IO

​ RDD的IO模式体现装饰者,强大的功能都是通过不断封装原RDD(textFilie)得到的,如下图所示。注意:

  • RDD数据只有在调用collect()时才会真正执行代码逻辑,包装只是对逻辑进行包装。
  • RDD在传递过程中是保存数据的,而IO模式会保留数据

在这里插入图片描述

在这里插入图片描述

d) 什么是RDD

​ RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。spark中所有执行类都是RDD类。

  • 弹性 存储的弹性:内存与磁盘的自动切换;

    ⚫ 容错的弹性:数据丢失可以自动恢复;

    ⚫ 计算的弹性:计算出错重试机制;

    ⚫ 分片的弹性:可根据需要重新分片。

  • 分布式:数据存储在大数据集群不同节点上

  • 数据集:RDD 封装了计算逻辑,并不保存数据

  • 数据抽象:RDD 是一个抽象类,需要子类具体实现

  • 不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的 RDD,在 新的 RDD 里面封装计算逻辑

  • 可分区、并行计算

3.2 RDD编程基础

​ 在idea中需要提前连接环境,见下图

package RDD_build

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

object RDD_memory {

  def main(args: Array[String]): Unit = {
    // TODO 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")  // 申请
    val sc = new SparkContext(sparkConf)    // 连接

    // TODO  任务
      command。。。。
    // TODO 关闭环境
    sc.stop()
  }

}

3.2.1 创建RDD

a) 从内存中创建
  • sc.makeRDD(seq):是sc.arallelize的包装,其实际调用的就是paralielize 方法。

  • sc.parallelize(seq)

    其中seq表示传入的数据类型(常见的数据类型都为Seq型)

package RDD_build

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

object RDD_memory {

  def main(args: Array[String]): Unit = {
    // TODO 准备环境
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")  // 申请
    val sc = new SparkContext(sparkConf)    // 连接

    // TODO  创建RDD
    // 集合中创建
    val seq = Seq[Int](1,2,3,4)
    val rdd1: RDD[Int] = sc.parallelize(seq)
    val rdd2: RDD[Int] = sc.makeRDD(seq)

    rdd1.collect().foreach(println)
    rdd2.collect().foreach(println)
    // TODO 关闭环境
    sc.stop()
  }
}
b) 从外部文件中创建
  • 方法:

    sc.textFile(“path”):以行为单位进行读取,所有文件中的每一行进行输出

    sc.wholeTextFile(“paht”):以文件为单位进行读取,返回元组(文件路径, 内容)

  • 注意:

    1. spark以当前项目所在路径为根目录,而不是以当前文件所在位置为根目录(与python相反)
    2. path可以是绝对路径,也可以是相对路径。
    3. path也可以指定文件夹,此时将读取目录中的全部文件
    4. path可以使用通配符,可以读取指定名称的文件
    5. path 也可以指定分布式储存系统中的文件,hdfs,hbase
    6. 当想知道内容来自于哪个文件时,可以使用第二种方法。
val rdd: RDD[String] = sc.textFile("data/1.txt")  
val rdd1: RDD[String] = sc.textFile("data/")  // 指定文件夹
val rdd2: RDD[String] = sc.textFile("data/1*")  // 通配符
val rdd2: RDD[String] = sc.textFile("hdfs://nod1:9000/1.tx1")  // hdfs文件
val rdd1: RDD[String] = sc.textFile("data")
val rdd2: RDD[(String, String)] = sc.wholeTextFiles("data")
rdd1.collect().foreach(println)
rdd2.collect().foreach(println)
// 结果如下图

在这里插入图片描述

3.2.1 分区与储存策略

  • 所谓分区策略是指:指定分区的个数时,实际产生的分区个数
  • 储存策略:已知分区个数了,怎么将数据分配到各个分区中去

指定分区个数

  • 方法

    sc.makeRDD(seq, numSlices):numSlices指定内存数据的分区个数

    sc.textFile(seq, minPartitions):minPartitions指定文件读取时的分区个数

    **rdd.saveAsTextFile(“path”)**方法查看核数。

    sparkConf.set(“spark.default.parallelism”, “5”) 配置核数

  • 实例如下

sparkConf.set("spark.default.parallelism", "5")	// 指定默认核数
// RDD的并行度 与 分区
val seq = Seq[Int](1, 2,3)
val rdd: RDD[Int] = sc.makeRDD(seq, 2)	// 分为两区
rdd.saveAsTextFile("output")			// 查看分区个数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eVYQLtEz-1683266209116)(…/img/spark/image-20230322163916268.png)]

3.2.2 内存数据的分区储存策略

a) 分区策略
  • 对于内存数据,指定多少个分区就产生多少个分区(文件数据会根据大小产生)
b) 储存策略
  • 若 A = List(1,2, 3, 4, 5),即可见A的长度为5,将其分为 3 块,则其得到的各个分区数据情况为

  • 第一块数据:1

    第二块数据:23

    第三块数据:45

  • 分区策略,通过数据长度和分块数量计算元组( start, end),元组表示各个分区的索引,计算过程:

    0 * 5 / 3 = 0; 1 * 5/ 3 = 1 ==>(0, 1)

    1 * 5 / 3 = 1; 2 * 5 / 3= 3 ==> (1, 3)

    2 * 5 / 3 = 3; 3 * 5 / 3 = 5 ==> (3, 5)

  • 索引取前不取后,因此结果如上所示,源码如下:

def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = { 
    (0 until numSlices).iterator.map { i => 
        val start = ((i * length) / numSlices).toInt 
        val end = (((i + 1) * length) / numSlices).toInt 
        (start, end)
    }
 /*
  因此若传入数据为 List(1,2,3,4,5)长度为5, 分区为 3个的话,将得到三个(start, end)元组
  (0, 1) (1, 3) (3, 5)
  也就是三个分区数据的对应索引,取前不取后,
  (0, 1) 表示第1个元素,
  (1, 3) 表示第2、3个元素,
  (3, 5) 表示第4、5个元素
  */

3.2.3 文件数据的分区储存策略

a) 分区策略
  • 文件数据的读取方式为:sc.textFile(path, minPartitions)minPartitions:表示最小分区数量,也就是人为指定的分区,但实际分区可能会大于minPartitions,这就是文件数据的分区策略造成的。(当没有指定时,默认情况minPartitons = min(defaultParallelism, 2) )

  • 假设读取文件 A 的字节长度为len(A) = 7,人为指定分区个数为 minPartitions = 2;则实际分区个数将会是3。

    Spark分区策略其实是采取Hadoop中*“1.1”分区模式,在Hadoop分块模式中,当字节数据不能整除时,若剩余字节数不超过每块数据量的0.1,那么其将于最后一块数据合并(最大能达到1.1倍),若超过了0.1,则重新归为一块**。*

    因此 7 / 2 = 3…1, 1 / 3 > 0.1,重新生成一块,因此文件A将会被分为三个区。

b) 数据储存策略
  • spark数据读取方式和Hadoop 一样,当读取一个文件时,以行为单位进行读取,储存数据时,行不会被拆分,也就是说同一个行只会储存在同一个块,不会储存到另外的一个块中去。(当一次性读取多个文件时,以文件作为一个单位,同一个文件不会被储存到多个块中去)。

  • spark以偏移量的方式判断每一行(或文件)所应该分到的块位置。同时保持同一行数据不会被拆分。

    偏移量 = 字节长度 / 指定分块数(并不是实际分块数)

    每个分区给予相同的偏移量(但是数据长度不一定相同,见下文)

  • 沿用上方例子情况,对长度为 7 的文件A进行分区,指定分区个数为2

    假设文件A中的数据情况为:1/n2/n3(也就是三行数据,每一行一个数字)

    因此 len(A) = 7 (换行符也算)

    所以 偏移量 = 7 / 2 = 3

    所以根据偏移量指定分区情况为

    ​ 第一区偏移量:[0, 3] // 偏移量为03的字节,也就是第14个字节应该属于分区1

    ​ 第二区偏移量:[3, 6]

    ​ 第三区偏移量:[6, 9]

    因此由上可知,第14个字节应该属于分区一,因此文件A中的**数字1及后面换行符**归属于分区一;**第4个字节为数字2,所以数字2也归属于分区一,不仅如此,第46个字节为一行,不可拆分,需归为同一分区**,因此数字2及后面换行符归属于第二个分区。同理,数字3为第7个字节,也就是字节数6,因此归属于第二个分区,而第三个分区却什么都没有!

    综上所诉,文件A的数据储存情况为:

    ​ 第一分区:1/n2/n

    ​ 第二分区:3

    ​ 第三分区:空

  • 如果数据源为多个文件,那么计算分区时文件为单位进行分区

3.3 并行计算演示

Spark通过将数据及其任务逻辑封装到不同的RDD块中去,进而发送的各个线程去同时进行,各个线程之间并不干扰,这就是并行计算,让我们来直观感受一下并行结算。

  • 如下方代码所示,numSlices=1,也就是指定分块个数为1时,表示没有进行并行计算。当numSlices>1时,表示进行多个分块,也就是并行计算。

  • 当没有进行并行计算时,输出结果如左图所示,当进行并行计算时,结果如右图所示。分析结果:

    1. 由计算结果可以看出,RDD转化算子在进行计算时,是逐个元素进行转化,只有第一个元素全部执行完毕才会执行下一个元素。

    2. 见左图,没有进行并行计算,也就是只有一个块时,计算顺序为元素中的排列顺序。

      见右图,当进行并行计算,多个块同时进行计算时,块内元素按顺序进行计算;而各个块同时进行计算,因此谁快谁慢无法确定,最后会出现乱序的情况。

在这里插入图片描述
在这里插入图片描述

package value_operator

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

object RDD_map1 {

  def main(args: Array[String]): Unit = {
    val sparkconf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("map1")
    val sc = new SparkContext(sparkconf)

    var list = List(1, 2, 3, 4)
    val Rdd: RDD[Int] = sc.makeRDD(list, numSlices=1)   // 分为一块时,没有进行并行计算
    val Rdd1: RDD[Unit] = Rdd.map(x => {
      println("*******" + x)
    })
    val Rdd2: RDD[Unit] = Rdd1.map(x => {
      println("########" + x)
    })
    Rdd2.collect()

    sc.stop()
  }
}

部分图片来自于视频
尚硅谷大数据Spark教程从入门到精通

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值