Spark(一)

161 阅读12分钟

1. 介绍

Spark用于大规模数据处理的统一分析引擎,基于内存计算,提高了在大数据环境下数据处理的实时性,同时保证了高容错性和高可伸缩性,允许用户将Spark部署在大量硬件之上,形成集群。

主要分为:

  • Spark core:Spark 最基础与最核心的功能
  • Spark Sql:Spark 用来操作结构化数据的组件
  • Spark streaming:Spark 平台上针对实时数据进行流式计算的组件
  • Spark Graphx:Spark 面向图计算提供的框架与算法库
  • Spark MLlib:Spark 提供的一个机器学习算法库

2. 运行架构

Spark 框架的核心是一个计算引擎,整体来说,它采用了标准master-slave的结构

1651735520082.png

1. 核心组件:

  • Driver: 驱动器节点,执行Spark任务中的main方法,负责实际代码的执行工作,;

    • 用户程序转化为作业job
    • 在executor之调度任务并跟踪执行情况
  • Executor:工作节点(Worker)中的JVM进程,负责Spark作业中运行具体任务(Task),任务间独立

    • 负责运行Spark应用任务,并结果返回给Driver驱动器
    • 为RDD提供缓存需求,直接缓存在Executor的进程内
  • Master & Worker

    Spark独立部署的两个核心组件

    • Master:一个进程,负责资源的调度和分配,并进行集群的监控等职责(类似Yarn中的RM)
    • Worker:运行在集群中的服务器上,由Master分配资源对数据进行并行处理和计算,类似Yarn中的NM
  • ApplicationMaster

    Yarn集群应用程序中的组件,用于向资源调度器申请执行任务的资源容器Container,运行用户自己的程序任务job,监控整个任务的状态、处理任务失败等异常情况。

2. 核心概念

资源参数

  • --num-executors 配置Executor 的数量;
  • --executor-memory 配置每个Executor 的内存大小;
  • --executor-cores 配置每个Executor 的虚拟CPU core 数量;

并行度

整个集群并行执行任务的数量。

有向无环图(DAG)

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

3. 提交流程

以Yarn集群为例

  • 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 上执行。

注意

  • Yarn Client :在客户端上执行,不在Yarn集群中,所以一般用于测试;
  • Yarn Cluster :在Yarn 集群资源中执行,一般用以实际生产环境执行;

5.Spark Core

5.1 RDD

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

  • 弹性:(1)存储上:内存与磁盘的自动切换;(2)容错:数据丢失可以依赖血缘自动恢复;(3)计算:计算出错重试机制;(4)分片:可根据需要重新分片;
  • 分布式: 数据存储在大数据集群不同节点上;
  • 数据集: RDD 封装了计算逻辑,并不保存数据;
  • 数据抽象:RDD 是一个抽象类,需要子类具体实现;
  • 不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的RDD,在 新的RDD 里面封装计算逻辑;
  • 可分区、并行计算

5.2 核心属性:

* Internally, each RDD is characterized by five main properties:
*  - A list of partitions
// 分区的列表,由partition组成
*  - A function for computing each split/partition
// 函数作用于RDD的每个分区
*  - A list of dependencies on other RDDs
// 依赖关系
*  - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
// 可选项:key-value的一个partition默认是使用hash分区的
*  - Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
// 可选:每一分片的优先计算位置,比如HDFS的block的所在位置应该是优先计算的位置. 移动数据不如移动计算
  • 分区列表: RDD中存在分区列表,用于执行任务时 并行计算

  • 分区计算函数:当数据为KV 类型数据时,可以通过设定分区器自定义数据的分区

  • 最佳位置:根据计算节点的状态选择不同的节点位置进行计算

    这三个属性其实说的就是数据集在哪,在哪计算更合适,如何分区

  • 计算函数:Spark执行计算时,使用分区函数对每一个分区进行计算

  • 依赖关系:RDD 是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个RDD 建立依赖关系

    这两个属性其实说的是数据集怎么来的

5.3 RDD创建

  • 从集合中创建,parallelize 和makeRDD

    # parallelize
    val rdd1 = sc.parallelize(1 to 10,3)
    # makeRDD
    val rdd2 = sc.makeRDD(1 to 10,3)
    
  • 从外部存储(文件)创建RDD sparkContext.textFile("input")

  • 运算转换

5.4 并行度与分区

并行度(paralleism):在分布式计算框架中,一般都是多个任务同时执行,由于任务分布在不同的计算节点进行计算,所以能够真正实现多个任务并行执行,记住,这里是并行,而不是并发,这里我们将整个集群并行执行任务的数量,成为并行度。

spark中的并行度和分区之间是有关系的,rdd的每一个分区都是一个task,然后传送到对应的executor中进行计算。如果资源充足(executor core数=task数)并行度就等于分区数,如果(executor core数<task数)就是并发执行。

官方推荐: task数量,设置成spark Application 总cpu core数量的2到3倍 ,比如150个cpu core ,基本设置 task数量为 300~ 500

总结: spark根据分区数来决定task的个数,而task的个数和executor所拥有的core数来决定着spark的并行度,当task数多余core数时,就会产生并发操作。

5.5 RDD依赖

  • 血缘

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

    // RDD血缘关系
    scala> rdd2.toDebugString
    res3: String =
    (2) ShuffledRDD[4] at reduceByKey at <console>:25 []
     +-(2) MapPartitionsRDD[3] at map at <console>:24 []
        |  MapPartitionsRDD[2] at flatMap at <console>:24 []
        |  README.md MapPartitionsRDD[1] at textFile at <console>:24 []
        |  README.md HadoopRDD[0] at textFile at <console>:24 []// RDD依赖类型
    //  窄依赖(narrow dependency)
    scala> rdd1.dependencies
    res4: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@37efd738)
    // 宽依赖(wide dependency)
    scala> rdd2.dependencies
    res5: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.ShuffleDependency@4b37b02a)
    ​
    
  • 窄依赖:1个父RDD分区对应1个子RDD的分区;

    • 1个子RDD的分区对应于1个父RDD的分区,比如:map,filter,union等算子,子RDD每个分区的生成与父RDD的数据规模无关
    • 1个子RDD的分区对应于N个父RDD的分区,比如co-partioned join,子RDD每个分区的生成与父RDD的数据规模相关
  • 宽依赖:**1个父RDD分区对应多个子RDD分区**

    • 1个父RDD对应非全部多个子RDD分区,比如groupByKey,reduceByKey,sortByKey
    • 1个父RDD对应所有子RDD分区,比如未经协同划分的join

5.6 任务划分

RDD任务切分中间分为:Application、Job、Stage和Task;

  • Application:初始化一个SparkContext即生成一个Application
  • Job:一个Action算子就会生成一个Job
  • Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage
  • Task:Stage是一个TaskSet,将Stage划分的结果发送到不同的Executor执行即为一个Task

Application -> Job-> Stage-> Task 每一层都是 1对n 的关系

5.7 持久化

持久化使用方法:

  • persist(): 参数,尽量选择_SER结尾的级别,表示经过java序列化操作
  • cache() ,调用的是persist(MEMORY_ONLY)

数据将会在第一次 action 操作时进行计算,并缓存在节点的内存中

注意:在python中,存储的对象都是通过Pickle库序列化了的,所以是否选择序列化等级并不重要

删除缓存RDD.unpersist()

5.8 检查点

检查点是将RDD中间结果写入磁盘,调用setCheckpointDir设置检查点目录。

  • 背景:由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销;

  • 存储:Checkpoint的数据通常是存储在HDFS等容错、高可用的文件系统

  • 存储格式:二进制的文件

  • 触发:不会立即执行,等待action操作才会触发,但是检查点为了数据安全,会从血缘关系的最开始执行一遍

  • 使用方法:

    • SparkContext的setCheckPointDIR()方法,设置高可用文件系统、 HDFS
    • 然后对RDD调用checkpoint()方法

5.9 缓存与检查点的区别

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

5.10 共享变量

一般情况下,当一个传递给Spark操作(例如map和reduce)的函数在远程节点上面运行时,Spark操作实际上操作的是这个函数所用变量的一个独立副本。这些变量被复制到每台机器上,并且这些变量在远程机器上 的所有更新都不会传递回驱动程序。通常跨任务的读写变量是低效的,但是,Spark还是为两种常见的使用模式提供了两种有限的共享变量:广播变量(broadcast variable)和累加器(accumulator)。

  • 广播变量

    • 释义:广播变量允许程序员缓存一个只读的变量在每台机器上面,而不是每个任务保存一份拷贝

    • 使用方法:SparkContext.broadcast(v),v是需要广播的变量,且广播之后不能被修改

      // 找出key相等的值
      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 = sc.broadcast(list)
      val resultRDD = rdd1.map {
       case (key, num) => {
       var num2 = 0
       for ((k, v) <- broadcast.value) {
       if (k == key)  num2 = v
       }
       (key, (num, num2))
       }
      }
      ​
      resultRDD.collect
      res6: Array[(String, (Int, Int))] = Array((a,(1,4)), (b,(2,5)), (c,(3,6)), (d,(4,7)))
      
  • 累加器

    • 释义:支持在所有不同节点之间进行累加计算,可自定义

    • 使用方法:SparkContext.accumulator(v),v:初始变量

      scala> val rdd1 = sc.makeRDD(List(1,2,3,4,5,6))
      scala> var sum = sc.longAccumulator("sum")
      scala> rdd1.foreach(num => sum.add(num))
      scala> println("sum = " + sum.value)
      sum = 21
      

5.11 疑惑

1.MR的shuflle与spark的shulle的区别?

  • MR的shuffle:

1651131542955.png

  • Map端的shuffle,主要是Partition、Collector、Sort、Spill、Merge几个阶段 Reduce端的shuffle,主要是Copy、Merge、Reduce几个阶段

存在全局排序(reduce task 是根据key去处理数据的),合并排序。先进行小范围排序,最后再大范围排序。最后的复杂度为O(nlog(n)),比普通排序复杂度O(n的平方)快

  • Spark的shuffle

    MapReduce Shuffle基础上进行的调优,主要针对 排序、合并逻辑做了一些优化 , Spark中Shuffle write相当于MapReduce 的map,Shuffle read相当于MapReduce 的reduce。负责shuffle过程的主要是ShuffleManager,主要有 HashShuffleManager和SortShuffleManager 。

    • HashShuffle:分为普通机制和合并机制 :(1)普通机制:会产生MR(Map个数Reduce个数)个据量小文件,产生大量性能低下的IO操作,易发生OOM;(2)合并机制:复用buffer,同分区文件合并,文件数量下降到coreR(core个数Reduce个数据)依然会产生大量小文件
    • SortShuffle:分普通机制和bypass机制: 普通机制在内存数据结构(默认为5M)完成排序,会产生2M个磁盘小文件 。 而当shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值。或者算子不是聚合类的shuffle算子(比如reduceByKey)的时候会触发SortShuffle的bypass机制,SortShuffle的bypass机制不会进行排序,极大的提高了其性能。
  • 异同点

    • 功能上 : 差别不大, 都是对Map端的数据进行分区,要么聚合排序,要么不聚合排序,然后Reduce端或者下一个调度阶段进行拉取数据,完成map端到reduce端的数据传输功能
    • 方案上: MR的shuffle是基于合并排序的思想,在数据进入reduce端之前,都会进行sort,为了方便后续的reduce端的全局排序 , 而Spark的shuffle是可选择的聚合,特别是1.2之后,需要通过调用特定的算子才会触发排序聚合的功能。
    • 流程 : MR的Map端和Reduce区分非常明显,两块涉及到操作也是各司其职, park的RDD是内存级的数据转换,不落盘,所以没有明确的划分,只是区分不同的调度阶段,不同的算子模型。
    • 数据拉取 : MR的reduce是直接拉去Map端的分区数据,而Spark是根据索引读取,而且是在action触发的时候才会拉去数据 。
  • 参考:MapReduce Shuffle 和 Spark Shuffle Spark Shuffle 详解

2 Spark与Hadoop差异

Spark是在借鉴了MapReduce之上发展而来的,继承了其分布式并行计算的优点并改进了MapReduce明显的缺陷 。

  • Spark把中间数据放到内存中,DAG图的分布式并行计算的编程框架,减少了迭代过程中数据的落地 ,迭代运算效率高
  • Spark容错性高: Spark引进了弹性分布式数据集RDD, 如果数据集一部分丢失,则可以根据血缘进行重建,也可以通过CheckPoint来实现容错。
  • 通用: 包含了Spark Core、Spark SQL、Spark Streaming、MLLib和GraphX等组件 。此外算子丰富,分为转化和动作算子,表达能力强。