Spark入门:概述、核心架构、RDD编程

1. 概述

1.1 什么是spark

Spark 是一种基于内存的快速、通用、可扩展的大数据分析计算引擎。

1.2 Spark与Hadoop的对比

SparkHadoop
Apache时间点2013年2008年
语言ScalaJava
主要功能数据计算分布式计算+分布式存储
数据通信模式内存硬盘

1.3 Spark核心模块

在这里插入图片描述

  • Spark Core 提供最基础最核心的功能,其他功能都是基于Spark Core进行拓展。
  • Spark SQL是Spark用于操作结构化数据的组件。通过Spark SQL,用户可以使用SQL或者Apache Hive版本的HQL来查询数据
  • Spark Streaming是Spark平台上针对实时数据进行流式计算的组件,提供丰富的处理流数据的API。
  • Spark MLlib是一个机器学习算法库。MLlib不仅提供了模型评估、数据导入等额外的功能,还提供了一些更底层的机器学习原语。
  • Spark GraphX是面向图计算的框架和算法库

2. Spark运行架构

2.1 运行架构

在这里插入图片描述

2.2核心组件

  • Driver

    Spark Driver节点负责执行Spark程序中的main方法,负责实际代码的执行工作。

    主要工作有:

    • 将用户的程序转化为作业(Job)
    • 在Executor之间调度任务(Task)
    • 跟踪Executor的执行情况
    • 通过UI展示查询运行情况
  • Executor

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

    主要功能有:

    • 负责运行Spark应用的任务,并将结果返回给驱动器。
    • 它们通过自身的块管理器(Block Manager)为用户程序中要求的RDD提供内存式存储。RDD是直接缓存在Executor进程内的,因此任务可以充分利用缓存数据加速运算。

    Master & Worker

    Spark集群的独立部署环境中,不需要依赖于其他的资源调度框架。其自身就实现了资源调度的功能。Master进程主要负责资源的调度和分配,并进行集群的监控,类似于YARN环境中的RM。Worker进程运行在集群中每台机器上,由Master分配资源对数据进行并行的处理和计算,类似于Yarn环境中的NM。

2.3 核心概念

  • Executor与Core

    Spark Executor是集群中运行在工作节点(Worker)中的一个JVM进程,是整个集群中的专门用于计算的节点。在提交的应用中,可以提供参数指定计算节点的个数,以及对应的资源。这里的资源一般指分配的内存大小和虚拟CPU核(Core)数量。

在这里插入图片描述

  • 并行度

    分布式系统一般都是多个程序同时执行,这里的同时执行指的是并行而不是并发。整个集群并行执行的任务量可以称为并行度。

  • 有向无环图

    有向无环图是有点和线临时组成的拓扑图形,该图形具有方向,不会形成闭环。

2.4 任务提交流程

提交流程是指开发人员将Spark应用通过Spark客户端提交到集群执行的过程。

在这里插入图片描述

Spark应用提交到YARN环境中执行的时候,一般有两种部署模式——Client和Cluster,两种模式主要的区别在于Driver程序的运行节点的位置。

  • Client模式

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

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

    1. 任务提交后首先和ResourceManager申请启动ApplicationMaster
    2. ResourceManager分配container,在合适的NodeManager上启动ApplicationMaster,此时的ApplicationMaster就是Driver
    3. Driver启动后向ResourceManager申请Executor内存。
    4. ResourceManager接到Driver的资源申请后会分配container,然后Driver在资源分配指定的NodeManager上启动Executor进程。
    5. Executor进程启动以后会向Driver反向注册,Executor全部注册完成以后Driver开始执行main函数。
    6. 之后执行到action算子时,触发一个Job,并根据宽依赖开始划分stage,每个stage生成对应的TaskSet,之后将task分发到各个Executor上执行。

3.Spark核心编程

Spark三大数据结构

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

3.1 RDD

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

  • 弹性:

    • 存储的弹性,内存和磁盘的自动切换
    • 容错的弹性:数据丢失可自动恢复
    • 计算的弹性:计算出错重试机制
    • 分片的弹性:可根据需要重新分片
  • 分布式:数据存储在大数据集群不同节点上

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

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

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

  • 可分区、并行计算

核心属性

  • 分区列表:RDD数据结构中存在分区列表,用于并行计算
  • 分区计算函数:Spark在计算的时候,是用分区函数对每一个分区进行计算
  • RDD之间的依赖关系:RDD是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个RDD建立依赖关系
  • 分区器:当数据为KV类型时,可以通过设定的分区器进行自定义数据的分区
  • 首选位置:计算数据时,可以根据计算节点的状态选择不同的节点位置进行计算

执行原理

从计算角度来看,数据处理过程中需要计算资源(内存和CPU)和计算模型(逻辑)。执行时,需要将计算资源和计算模型进行协调和整合。

Spark框架在执行时,先申请资源,然后将应用程序的处理处理逻辑分解成一个一个的计算任务,然后将计算任务发到已经分配资源的计算节点上,按照指定的计算模型进行数据计算,最后返回计算结果给驱动进程。

RDD是Spark框架中用于数据处理的核心模型,Yarn环境中,RDD的工作原理

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

基础编程

  1. RDD的创建

    • 从集合创建RDD:parallelize()和makeRDD()
    • 从外部存储创建RDD:textFile()
    • 从已有RDD生成新的RDD
    • 直接使用new的方式构造RDD
  2. RDD并行度与分区

    • 默认情况下,Spark可以将一个作业切分为多个任务以后,发送给Executor节点并行计算,而能够并行计算的任务数量我们称之为并行度。这个数量可以在创建RDD的时候指定。

    • 读取内存数据时,数据可以按照并行度进行分区操作。

      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)
      }
      
    • 读取文件数据时,数据是按照Hadoop文件读取的规则进行切片分区,而切片规则和数据读取的规则有些差异,源码如下

      public InputSplit[] getSplits(JobConf job, int numSplits)
      throws IOException {
          long totalSize = 0; // compute total size
          for (FileStatus file: files) { // check we have valid files
              if (file.isDirectory()) {
                  throw new IOException("Not a file: "+ file.getPath());
              }
              totalSize += file.getLen();
          }
          long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
          long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
                                              FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);
          ...
          for (FileStatus file: files) {
              ...
              if (isSplitable(fs, path)) {
                  long blockSize = file.getBlockSize();
                  long splitSize = computeSplitSize(goalSize, minSize, blockSize);
                  ...
              }
              protected long computeSplitSize(long goalSize, long minSize,
                                              long blockSize) {
                  return Math.max(minSize, Math.min(goalSize, blockSize));
      }
      
  3. RDD转换算子

    RDD根据数据的处理方式不同,将算子整体上分成Value类型,双Value类型和Key-Value类型

    • Value类型

      1. map:将数据逐条的进行转换,这里可以是值的转换或者是类型的转换。转换前后RDD的数据量保持不变。

      2. mapPartitions: 将待处理的数据以分区为单位发送到计算节点进行处理。

      ​ map和mapPartition的区别在于,map中处理单位是RDD的每一条记录,而mapPartition中处理单位是每个数据分区的迭代器。

      ​ map操作不会减少和增加RDD的数据数量,而mapPartition操作返回的是一个迭代器,并没有对数据量进行限制。

      ​ mapPartition可能会导致内存溢出,而map不会。

      ​ mapPartition操作类似于批处理,性能比map更高,但是会长时间占用内存。

      3)mapPartitionWithIndex:将数据以分区为单位发送到计算节点进行处理,处理的时候可以获取到当前分区的索引。

      4)flatMap:将处理的数据进行扁平化后再进行映射处理,也成为扁平映射。

      5)glom:将同一个分区的元素合并到一个数组里去,合并后每个分区是一个独立的数组。

      6)groupBy:将数据按照指定的规则进行分组,默认分区不变。但是数据会被打乱重新组合(shuffle)

      7)filter:将数据根据指定的规则进行过滤,符合规则的数据保留,不符合规则的数据丢弃。当数据进行筛选后,分区不变,但是不同分区中数据量可能不同,出现数据倾斜。

      8)sample:从指定数据集抽取数据:伯努利算法(抽取不放回)和泊松算法(抽取后放回)

      9)distinct:数据去重。

      10)coalesce:根据数据量缩减分区,当spark程序中存在很多小任务时,可以使用该方法合并分区,减少分区数量和调度成本。

      11)repartition:该操作内部执行的是coalesce操作,shuffle默认为true。该操作可以将分区由少变多或者由多变少。因为无论如何都会执行shuffle过程。

      12)sortBy:通过指定处理函数后,对函数处理后的结果进行排序。排序前后分区数量保持不变,中间存在shuffle过程。

      13)intersection:对源RDD和参数RDD取交集后返回新的RDD

      14)union:对源RDD和参数RDD取并集后返回新的RDD

      15)subtract:以一个RDD为主,去掉两个RDD重复的元素,即求两个RDD的差集。

      16)zip:将两个RDD以键值对的形式合并,第一个RDD的值作为Key,第二个RDD的值作为Value。

      17)partitionBy:将数据按照指定的Partitioner重新进行分区。Spark默认的分区器是HashPartitioner。

      18)reduceByKey:可以按照相同的key,对value进行聚合

      19)groupByKey: 对源数据按照key进行分组,存在Shuffle过程。与reduceByKey的差异在于,reduceByKey会在本地对相同key进行combine,相比groupByKey的落盘数据量会少很多。

      20)aggregateByKey :将数据根据不同规则进行分区内计算和分区间计算

      21)foldByKey :当aggregateByKey 中分区内计算规则和分区间计算规则相同时,可以简化为foldByKey。

      22)combineByKey : 最通用的key-value 型 rdd 进行聚集操作的聚集函数 ,允许用户输入类型和返回值类型不一致。

      23)sortByKey:按照Key进行排序,该Key必须实现sorted接口

      24)join: 在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素连接在一起的(K,(V,W))的 RDD

      1. leftOuterJoin : 类似于SQL的左外连接,返回结果类似(K(V,[W]))

      26)cogroup:在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD

  4. RDD行动算子

    1. Reduce:聚集RDD中所有的元素,先聚合分区内的数据,再聚合分区间的数据
    2. collect:以Array的形式返回数据集中的所有元素
    3. count:返回RDD中元素的个数
    4. first:返回RDD中第一个元素
    5. take:返回一个由RDD的前N个元素组成的数组
    6. takeOrdered:返回该RDD排序后的前N个元素组成的数组
    7. aggregate:分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合。
    8. fold:aggregate的简化版操作
    9. countByKey :统计每种key的数量
    10. save 相关算子 :保存数据到文件
    11. foreach: 遍历RDD

RDD序列化

对于Spark应用来说,算子内部的代码在Executor上执行,算子以外的代码在Driver上面执行。算子以内的代码需要用到算子以内的数据时,就会形成闭包效果。如果算子以外的数据无法序列化,也就意味着无法传递给Executor端执行,从而导致错误。所以在执行任务之前,需要进行闭包检测。

RDD依赖关系

  1. RDD血缘关系

    RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD丢失部分分区数据时,可以根据这些信息重新计算和恢复丢失的数据分区。

  2. RDD依赖关系:指的是相邻RDD之间的依赖关系

  3. RDD窄依赖:窄依赖表示每一个父RDD的Partition最多被子RDD的一个Partition使用。

  4. RDD宽依赖:一个父RDD的Partition会被多个子RDD的Partition依赖,会引起shuffle。

  5. RDD阶段划分:DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。例如, DAG 记录了 RDD 的转换过程和任务的阶段。

  6. RDD任务划分,RDD任务切分中间分为:Application、Job、Stage、Task

    • Application:初始化一个SparkContext即生成一个Application
    • Job:一个Action算子就会生成一个Job
    • Stage:Stage等于宽依赖(ShuffleDependency)的个数+1;
    • Task: 一个stage阶段中,最后一个RDD的分区个数就是Task的个数。

在这里插入图片描述

RDD持久化

  1. RDD Cache缓存

    RDD 通过 Cache 或者 Persist 方法将前面的计算结果缓存,默认情况下会把数据以缓存在 JVM 的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 算子时,该 RDD 将会被缓存在计算节点的内存中,并供后面重用。

    缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除, RDD 的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。

    通过基于 RDD 的一系列转换,丢失的数据会被重算,由于 RDD 的各个 Partition 是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部 Partition。
    Spark 会自动对一些 Shuffle 操作的中间数据做持久化操作(比如: reduceByKey)。这样做的目的是为了当一个节点 Shuffle 失败了避免重新计算整个输入。但是,在实际使用的时候,如果想重用数据,仍然建议调用 persist 或 cache。

  2. RDD CheckPoint 检查点

    所谓的检查点其实就是通过将 RDD 中间结果写入磁盘。由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题, 可以从检查点开始重做血缘,减少了开销。
    对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发。

  3. 缓存和检查点的区别

    • Cache 缓存只是将数据保存起来,不切断血缘依赖。 Checkpoint 检查点切断血缘依赖。
    • Cache 缓存的数据通常存储在磁盘、内存等地方,可靠性低。 Checkpoint 的数据通常存
      储在 HDFS 等容错、高可用的文件系统,可靠性高。
    • 建议对 checkpoint()的 RDD 使用 Cache 缓存,这样 checkpoint 的 job 只需从 Cache 缓存
      中读取数据即可,否则需要再从头计算一次 RDD。

RDD分区器

Spark 目前支持 Hash 分区和 Range 分区,和用户自定义分区。 Hash 分区为当前的默认分区。分区器直接决定了 RDD 中分区的个数、 RDD 中每条数据经过 Shuffle 后进入哪个分区,进而决定了 Reduce 的个数。
只有 Key-Value 类型的 RDD 才有分区器,非 Key-Value 类型的 RDD 分区的值是 None
每个 RDD 的分区 ID 范围: 0 ~ (numPartitions - 1),决定这个值是属于那个分区的。

​ **Hash分区:**对每个Key,计算其HashCode,对分区数进行取余得到分区号。

​ **Range分区:**将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序。

RDD文件的读取与保存

Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。
文件格式分为: text 文件、 csv 文件、 sequence 文件以及 Object 文件;
**文件系统分为:**本地文件系统、 HDFS、 HBASE 以及数据库。

  • Text文件

    // 读取输入文件
    val inputRDD: RDD[String] = sc.textFile("input/1.txt")
    // 保存数据
    inputRDD.saveAsTextFile("output")
    
  • sequence文件

    SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件(FlatFile)。

    在 SparkContext 中,可以调用 sequenceFilekeyClass, valueClass

    // 保存数据为 SequenceFile
    dataRDD.saveAsSequenceFile("output")
    // 读取 SequenceFile 文件
    sc.sequenceFile[Int,Int]("output").collect().foreach(println)
    
  • object 对象文件

    对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。

    可以通过 objectFileT:ClassTag函数接收一个路径, 读取对象文件, 返回对应的 RDD, 也可以通过调用saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定类型。

    // 保存数据
    dataRDD.saveAsObjectFile("output")
    // 读取数据
    sc.objectFile[Int]("output").collect().foreach(println)
    

3.2 累加器

**原理:**累加器用来把 Executor 端变量信息聚合到 Driver 端。在 Driver 程序中定义的变量,在Executor 端的每个 Task 都会得到这个变量的一份新的副本,每个 task 更新这些副本的值后,传回 Driver 端进行 merge。

系统累加器

val rdd = sc.makeRDD(List(1,2,3,4,5))
// 声明累加器
var sum = sc.longAccumulator("sum");
rdd.foreach(
    num => {
    // 使用累加器
    sum.add(num)
}
)
// 获取累加器的值
println("sum = " + sum.value)

用户自定义累加器

// 自定义累加器
// 1. 继承 AccumulatorV2,并设定泛型
// 2. 重写累加器的抽象方法
class WordCountAccumulator extends AccumulatorV2[String, mutable.Map[String,
Long]]{
var map : mutable.Map[String, Long] = mutable.Map()
// 累加器是否为初始状态
override def isZero: Boolean = {
map.isEmpty
}
// 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = {
new WordCountAccumulator
}
// 重置累加器
override def reset(): Unit = {
map.clear()
}
// 向累加器中增加数据 (In)
override def add(word: String): Unit = {
// 查询 map 中是否存在相同的单词
// 如果有相同的单词,那么单词的数量加 1
// 如果没有相同的单词,那么在 map 中增加这个单词
map(word) = map.getOrElse(word, 0L) + 1L
}
// 合并累加器
override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]):
Unit = {
val map1 = map
val map2 = other.value
// 两个 Map 的合并
map = map1.foldLeft(map2)(
( innerMap, kv ) => {
innerMap(kv._1) = innerMap.getOrElse(kv._1, 0L) + kv._2
innerMap
}
)
}
// 返回累加器的结果 (Out)
override def value: mutable.Map[String, Long] = map
}

3.3 广播变量

广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,广播变量用起来都很顺手。在多个并行操作中使用同一个变量,但是 Spark 会为每个任务分别发送。

val rdd1 = sc.makeRDD(List( ("a",1), ("b", 2), ("c", 3), ("d", 4) ),4)
val list = List( ("a",4), ("b", 5), ("c", 6), ("d", 7) )
// 声明广播变量
val broadcast: Broadcast[List[(String, Int)]] = sc.broadcast(list)
val resultRDD: RDD[(String, (Int, Int))] = rdd1.map {
case (key, num) => {
var num2 = 0
// 使用广播变量
for ((k, v) <- broadcast.value) {
if (k == key) {
    num2 = v
    }
    }
    (key, (num, num2))
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值