Spark基础知识(个人总结)

    声明: 1. 本文为我的个人复习总结, 并那种从零基础开始普及知识 内容详细全面, 言辞官方的文章
              2. 由于是个人总结, 所以用最精简的话语来写文章
              3. 若有错误不当之处, 请指出

一、Spark概述:

Spark模块:

  1. Core
  2. SQL
  3. Streaming
  4. MLlib
  5. Graphx

Spark VS MapReduce:

  1. Spark比MapReduce更适合迭代式多任务计算:

    1. MapReduce多个作业间的数据通信是基于磁盘, 而Sparke多个作业间的数据通信是基于内存

    2. 一个MapReduce程序只有map+reduce, 而Spark程序可以有多个算子

  2. MapReduce是批处理, 而Spark是批处理 & 微批准实时处理

  3. Shuffle中Spark不用落盘, 而MapReduce要磁盘

  4. Spark有丰富的RDD算子, SQL操作, Streaming流处理, 还可以处理机器学习, 而MapReduce只有Mapper和Reducer

  5. Spark有容错机制可以切断血缘, 避免失败后从源头数据开始全部重新计算

Spark 不能完全替代 MapReduce, 因为内存不充足时, Spark就无法工作了

架构:

  1. Driver

    是程序的入口, 是任务的调度者

    功能:

    1. 将用户程序转化为Job作业
    2. 调度Task, 分配哪个Task由哪个Executor执行
    3. 向Yarn申请Container资源
    4. 监控Executor的执行情况
  2. Executor

    执行Task

对于Standalone独立部署模式, Master(相当于ResourceManager)和Worker(相当于NodeManager)来负责资源的管理调度

整个集群并行执行任务的数量称之为并行度

DAG 有向无环图, 是高度抽象后的 单向无闭环的任务流程图, 用于表示程序的拓扑结构

向Yarn提交Job:

  1. Yarn Client模式(用于测试)

    Driver模块的计算运行在本地

  2. Yarn Cluster模式(生产环境)

    Driver模块的计算运行在Yarn

    1. 客户端向ResourceManager申请启动Driver(ApplicationMaster)

    2. ResourceManager分配Container, 在合适的NodeManager上启动Driver(ApplicationMaster)

    3. Driver(ApplicationMaster)向ResourceManager申请Executor需要的内存; ResourceManager进行分配Container, 然后在合适的NodeManager上启动Executor

    4. Executor进程启动后会向Driver反向注册, 当所有Executor全部注册完成后Driver开始执行main函数

    5. 执行到Action算子时触发一个Job, 并根据宽依赖划分stage, 并生成对应的TaskSet, 之后将Task分配给Executor执行

二、Spark-Core:

概述:

三大数据结构:

  1. RDD 弹性分布式数据集
  2. 累加器 分布式共享只写变量
  3. 广播变量 分布式共享只读变量

RDD算子的特点:

  1. 弹性

    1. 容错的弹性, 有持久化机制, 数据丢失后可以自动恢复; 且可以切断血缘避免对父级的依赖, 减少重复计算

    2. 计算的弹性, 计算失败后自动重试

    3. 存储的弹性: 自动切换 内存和磁盘 去存储数据

    4. 分片的弹性:可根据需要重新分片

      先将数据集分片, 然后将各个分片放到各个分区

  2. 分布式

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

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

  5. 不可变:RDD是不可变的, 要想改变只能产生新的RDD

  6. 可分区、各分区间是并行计算的

算子类型:

  1. 转换(Transform)算子, 并不会触发Job的执行

    1. Value类型
    2. 双Value类型
    3. Key-Value类型
  2. 行动(Action)算子, 真正触发Job的执行

一、RDD算子:

算子以外的代码都是在Driver端执行, 算子里面的代码都是在Executor端执行

转换算子:

单Value类型:

  1. map

    每条数据为单位将数据发到Executor端

    rdd.map(num => num * 2)
    
  2. mapPartitions

    每个分区为单位将数据发到Executor端

    rdd.mapPartitions(datas => datas.filter(_%2==0))
    
  3. mapPartitionsWithIndex

    在mapPartitions基础上多了一个参数index, 即当前分区的索引序号

    rdd.mapPartitions((index,datas) => datas.filter(index==0))
    
  4. flatMap

    扁平化处理, 输入参数循环下来有多个List, 而输出结果只有一个List

    val dataRDD = sparkContext.makeRDD(List(
        List(1,2),List(3,4)
    ),1)
    // 计算结果 1,2,3,4
    val dataRDD1 = dataRDD.flatMap(
        list => list
    )
    
  5. glom

    将同一个分区的数据转换为同类型的数组

  6. groupBy

    会产生Shuffle, 数据被打乱分配到各个分区

    一个组的数据在一个分区中, 一个分区中可以有多个组

  7. filter

    返回true/false来进行过滤

    有些分区的数据过滤掉太多或太少的话, 可能会导致该分区发生数据倾斜

  8. sample

    根据一些规则进行随机抽取元素

    1. 抽取数据不放回(伯努利算法)

    2. 抽取数据放回(泊松算法)

  9. distinct

    去重

  10. coalesce

    缩减分区数量

    会产生Shuffle

    底层调的是repartition

  11. repartition

    扩大分区数量

    会产生Shuffle

  12. sortBy

    会产生Shuffle

    // 参数1 返回值是分区字段
    // 参数2 是否升序
    // 参数3 分区数量
    dataRDD.sortBy(str=>str.subString(0,5), false, 4)
    

双Value类型:

  1. intersection

    对源RDD和参数RDD求交集后返回一个新的RDD

    数据类型得相同

    dataRDD1.intersection(dataRDD2)
    
  2. union

    数据类型得相同

    对源RDD和参数RDD求并集后返回一个新的RDD

    dataRDD1.union(dataRDD2)
    
  3. subtract

    数据类型得相同

    差集, dataRDD1-公共元素

    val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4))
    val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6))
    // 结果为1,2
    dataRDD1.subtract(dataRDD2)
    
  4. zip

    将两个RDD中的元素, 以键值对的形式进行合并

    数据类型可以不同

    两个RDD的分区数量得相等, 而且每个分区的数据个数也得相等才行, 否则会报错

Key-Value类型:

  1. partitionBy

    将数据按照指定Partitioner重新进行分区: partitionBy(partitioner: Partitioner)

  2. groupByKey

  3. reduceByKey

  4. aggregateByKey

    有每个分区的初始值(不算元素个数)

    将数据进行分区内的计算和分区间的计算

    // 每个分区内初始值(不算元素个数) & 分区内的计算规则 & 分区间的计算规则
    dataRDD.aggregateByKey(0)(_+_ , _+_)
    
  5. foldByKey

    aggregateByKey分区内的计算规则和分区间的计算计算规则相同时, 可以简化为foldByKey

    dataRDD.foldByKey(0)(_+_ )
    
  6. combineByKey

    没有每个分区的初始值,

    第一个参数表示将分区内的第一个数据转换结构, 第二个参数为分区内的计算规则, 第三个参数为分区间的计算规则

    // 求每个key的平均值
    val list: List[(String, Int)] = List(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98))
    val input: RDD[(String, Int)] = sc.makeRDD(list, 2)
    val combineRdd: RDD[(String, (Int, Int))] = input.combineByKey(
       v => (v, 1),    
       (acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1),    
       (acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2)
       )
    // 再进行除操作
    
  7. sortByKey

    参数true/false代表是否升序排序

  8. join

    将(K,V) 组合 (K,W) 形成 (K,(V,W)), 二者的K类型得相同

  9. leftOuterJoin

    按key左外连接

  10. cogroup

    将(K,V) 组合 (K,W) 形成 (K,(Iterable,Iterable)), 二者的K类型得相同

行动算子:

  1. reduce

  2. collect

    收集数据到Driver

  3. count

    统计RDD内元素的个数

  4. first

  5. take

  6. takeOrdered

    返回该RDD排序后的前n个元素

  7. aggregate

    分区内聚合计算要用到初始值, 分区间聚合计算也要用到初始值

  8. fold

    aggregate分区内的计算规则和分区间的计算计算规则相同时, 可以简化为fold

  9. countByKey

    统计每种key的个数

  10. countByValue

    统计每个元素value出现的个数, 这个value不是键值对的value, 而是单个元素的value

  11. save相关算子

    rdd.saveAsTextFile("textFile")rdd.saveAsObjectFile("objectFile")rdd.saveAsSequenceFile("sequenceFile")
    
  12. foreach

    分布式遍历RDD中的每一个元素

会导致Shuffle的算子:

  1. repartition操作:repartition、repartitionAndSortWithinPartitions、coalesce等
  2. byKey操作: reduceByKey、groupByKey、sortByKey等
  3. join操作: join、cogroup

大对比:

map VS mapPartitions:

  1. 数据处理角度:
    map是分区内一个数据一个数据的执行, 而mapPartitions是以分区为单位进行批处理操作

  2. 功能的角度
    map是一对一, 处理后数据不会增加也不会减少

    mapPartitions是一个集合对一个集合, 集合里可以增加或减少数据

  3. 性能的角度
    mapPartitions类似于批处理, 所以性能较高;

    但是mapPartitions会长时间占用内存;

    所以内存不足时使用map, 充足时使用mapPartitions

groupByKey VS reduceByKey:

  1. 功能上: groupByKey是分组, reduceByKey是分组后聚合
  2. 从shuffle的角度: 二者都存在Shuffle;
    • 但是reduceByKey可以在Shuffle前对分区内相同key的数据进行预聚合, 从而减少落盘的数据量
    • 而groupByKey只是进行分组, 不存在数据量减少的问题, 从而不会减少Shuffle落盘的数据量

reduceByKey VS foldByKey VS aggregateByKey VS aggregate VS combineByKey:

  1. reduceByKey: 各个数据进行聚合, 没有分区内初始值, 分区内和分区间计算规则相同
  2. aggregateByKey: 分区内有初始值, 分区内和分区间计算规则不同
  3. foldByKey: 分区内有初始值, 分区内和分区间计算规则相同
  4. aggregate: 分区内聚合计算要用到初始值, 分区间聚合计算也要用到初始值, 分区内和分区间计算规则不同
  5. combineByKey: 将分区内的第一个数据转换数据结构, 分区内和分区间计算规则不相同

序列化:

分布式计算中, Driver要往Executor端发数据, 所以数据要支持序列化(算子内经常会用到算子外的数据, 闭包检测)

依赖关系:

RDD的Lineage(血统)会记录RDD间的元数据信息和转换行为, 当该RDD的部分分区数据丢失时 可以根据这些信息来恢复数据重新计算

多个RDD间可能有血缘依赖, 后者RDD恢复数据时, 也需要前者RDD重新计算

窄依赖: 一个父(上游)RDD的Partition最多被子(下游)RDD的一个Partition使用, 像独生子女

宽依赖: 一个父(上游)RDD的Partition可以被子(下游)RDD的多个Partition使用(会产生Shuffle), 像多生子女; 又称Shuffle依赖

RDD 任务划分:

  1. Application:初始化一个SparkContext即生成一个Application

  2. Job:一个Action算子就会生成一个Job

  3. Stage:Stage个数等于产生宽依赖(ShuffleDependency)的RDD个数+1(ResultStage)

    即每一次Shuffle后, 都会新起一个Stage

  4. Task:一个Stage阶段中最后一个RDD的分区个数就是Task的个数

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

Shuffle:

将上游各分区的数据打乱后 分到下游的各个分区, 即宽依赖

Shuffle要落盘, 因为得等待所有上游分区数据都到齐才能进行下一步操作, 所以Shuffle很耗时

窄依赖的话就不必等待所有分区数据全都到齐了, 故窄依赖不会引起Shuffle

持久化:

Cache缓存:

RDD通过cache( )方法将前面的计算结果临时缓存到内存

可以通过persist( )方法将其改为临时缓存到磁盘

  • 并不会立刻执行, 而是遇到Action算子时才执行

  • Cache操作不会切断血缘依赖

  • 因内存不足原因导致数据丢失时, 由于RDD的各个Partition是相对独立的, 所以只需要计算丢失的那部分Partition即可, 不必全部重新计算

  • Spark会自动对一些Shuffle操作的中间结果数据做持久化操作

    这是为了避免当有一个节点计算失败了, 导致任务还需要重新从起点进行计算, 重新执行耗时的Shuffle

缓存是临时存储

CheckPoint检查点:

将RDD计算的中间数据写到磁盘

  • 并不会立刻执行, 而是遇到Action算子时才执行
  • 由于血缘依赖过长会造成容错成本过高, 检查点可以切断血缘关系, 避免从头到尾全部重新计算

检查点是长期存储

建议在checkpoint( )前先使用.cache( ), 这样做持久化操作时 只需从Cache缓存中读取数据即可, 否则需要重新计算一次RDD进行持久化

缓存和检查点区别:

  1. Cache缓存只是将数据保存起来, 不切断血缘依赖; 而Checkpoint检查点切断血缘依赖
  2. Cache缓存将数据存储在内存, 可靠性低, 但可以使用persist指定到磁盘; 而Checkpoint将数据存储到磁盘, 可靠性高
  3. 缓存是临时存储, 检查点是长期存储

缓存和检查点相同的应用场景:

  1. 为了复用前面RDD计算的中间结果, 避免大量的重复计算
  2. 依赖过长时, 避免后面的RDD计算出错后要从最初的RDD开始全部重新计算一遍

分区器:

只有Key-Value类型的RDD才有分区器,非Key-Value类型的RDD都分到None分区

  1. Hash分区(默认) hash(key)%分区数量
  2. Range分区 将一定范围内的数据分到一个分区中, 并且尽量使每个分区数据均匀, 分区内数据是有序的
  3. 自定义分区器

使用文件进行数据的读取和保存

文件格式:

  1. text
  2. csv
  3. sequence(二进制文件)
  4. Object(对象的序列化文件)

文件系统:

  1. HDFS
  2. HBase
  3. 本地磁盘

二、累加器:

为什么要有累加器?

各个Executor端计算的结果数据并不会影响到Driver端最终结果, 所以需要累加器

累加器的作用:

累加器用来把各个Executor端计算的结果数据聚合到Driver端

三、广播变量:

为什么要有广播变量?

Driver向Executor端的每个Task都发一份数据, 开销太大

广播变量的作用:

不需要给Executor端的每个Task都发一份数据, 而是只给Executor节点发一份数据即可

三、Spark-SQL:

HiveOnSpark:

计算引擎是是Spark, 语法是HiveSQL

SparkOnHive:

计算引擎是是Spark, 语法是SparkSQL

DataFrame:

是一个二维表格, 有一个个字段; 是弱类型

DataSet:

在DataFrame的基础上, 将字段映射为实体类的属性, 相当于多了表名; 是强类型

DataFrame=DataSet[ROW]

RDD & DataFrame & DataSet之间的转换:

Spark基础知识(个人总结)_第1张图片

数据存储格式:

SparkSQL默认读取和保存的文件格式为Parquet格式

四、Spark-Streaming:

Receiver:

其中一个Executor作Receiver接收数据

背压机制: 根据JobScheduler反馈作业的执行信息来动态调整Receiver数据接收率

状态:

状态就是一块内存, 如果要访问历史窗口(或批次)的数据时就需要用到状态, 把历史窗口(或批次)的数据处理结果值保存到状态里

无状态转化操作:

map, filter等

.transform(类似于RDD里的转换算子, 不会触发计算)转化为RDD进行操作

lineDStream.transform(rdd => {
      val words: RDD[String] = rdd.flatMap(_.split(" "))
      val wordAndOne: RDD[(String, Int)] = words.map((_, 1))
      val value: RDD[(String, Int)] = wordAndOne.reduceByKey(_ + _)
      value
})

有状态转化操作:

状态操作需要设置检查点, 因为要用检查点来存状态数据

updateStateByKey:

    // 定义更新状态方法,values为当前批次单词频度,state为之前批次单词频度
    val updateFunc = (values: Seq[Int], state: Option[Int]) => {
      val currentCount = values.foldLeft(0)(_ + _)
      val previousCount = state.getOrElse(0)
      Some(currentCount + previousCount)
    }
   pairs.updateStateByKey[Int](updateFunc)

Window操作:

  1. window

    开窗口, 窗口大小 & 滑动不长

  2. reduceByWindow

    窗口内做聚合

  3. reduceByKeyAndWindow

    窗口内按key做聚合

    pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b),Seconds(12), Seconds(6))
    
  4. reduceByKeyAndWindow

    有状态操作, 为了避免窗口重叠部分的值的重复计算, 采用减去旧窗口不包含重叠部分的值,

    pairs.reduceByKeyAndWindow(
      {(x, y) => x + y}, // 减去旧窗口不包含重叠部分的值
      {(x, y) => x - y},  // 增加新窗口不包含重叠部分的值
      Seconds(30),
      Seconds(10))
    
  5. countByWindow

    统计窗口内数据的数量

  6. countByValueAndWindow

    统计窗口内每个元素出现了多少次

DStream输出:

类似于RDD的行动算子, 触发计算

  1. print
  2. foreach
  3. foreachRDD
  4. saveAsTextFiles
  5. saveAsObjectFiles
  6. saveAsHadoopFiles

注意:

  1. Connection对象不能写在Driver层面, 因为Connection对象不能被序列化(安全起见), 而Driver发往Executor又非得把数据进行序列化

  2. 如果用foreach则每一条数据都使用一个Connection, 太浪费, 且最大连接数有限制

  3. 最好使用foreachPartition, 每个分区共用一个Connection

优雅关闭:

使用外部文件系统来控制内部程序关闭

//关闭时使用优雅关闭
sparkConf.set("spark.streaming.stopGracefullyOnShutdown", "true")

你可能感兴趣的:(大数据,spark,大数据)