Spark知识点总结

Spark基础

Spark优势

  1. 优秀的数据模型与丰富计算抽象
    Spark 借鉴了 MapReduce 思想发展而来,保留了其分布式并行计算的优点并改进了其明显的缺陷。
    让中间数据存储在内存中提高了运行速度、并提供丰富的操作数据的 API 提高了开发速度。

  2. 完善的生态圈-fullstack

  • Spark Core
    实现了Spark的基本功能,包含RDD、任务调度、内存管理、错误恢复、与存储系统交互等模块。

  • Spark SQL
    Spark用来操作结构化数据的程序包。通过Spark SQL,我们可以使用SQL操作数据。

  • Spark Streaming
    Spark提供的对实时数据进行流式计算的组件。提供了用来操作数据流的API。

  • Spark MLlib
    提供常见的机器学习(ML)功能的程序库。包括分类、回归、聚类、协同过滤等,还提供了模型评估、数据导入等额外的支持功能。

  • GraphX(图计算)
    Spark中用于图计算的API,性能良好,拥有丰富的功能和运算符,能在海量数据上自如地运行复杂的图算法。

  • 集群管理器
    Spark设计为可以高效地在一个计算节点到数千个计算节点之间伸缩计算。

  • Structured Streaming
    处理结构化流,统一了离线和实时的API。

Spark VS Hadoop

Hadoop Spark
类型 分布式基础平台,包含计算,存储,调度 分布式计算工具
场景 大规模数据集上的批处理 迭代计算,交互式计算,流计算
价格 对机器要求低,便宜 对内存有要求,相对较贵
编程范式 Map+Reduce,API较为底层,算法适应性差 RDD组成DAG有向无环图,API较为顶层,方便使用
数据存储结构 MapReduce中间计算结果存在HDFS磁盘上,延迟大 RDD中间运算结果存在内存中,延迟小
运行方式 Tash以进程方式维护,任务启动慢 Task以线程方式维护,任务启动快

注意:
尽管Spark相对于Hadoop而言具有较大优势,但Spark并不能完全替代Hadoop,Spark主要用于替代Hadoop中的MapReduce计算模型。
存储依然可以使用HDFS,但中间结果可以存放在内存中,调用可以使用Spark内置的,也可以使用更成功的调度系统Yarn。

实际上,Spark 已经很好地融入了 Hadoop 生态圈,并成为其中的重要一员,它可以借助于 YARN 实现资源调度管理,借助于 HDFS 实现分布式存储。
此外,Hadoop 可以使用廉价的、异构的机器来做分布式存储与计算,但是,Spark 对硬件的要求稍高一些,对内存与 CPU 有一定的要求。

Spark运行模式

  1. local本地模式(单机)---学习测试使用
    分为local单线程和local-cluster多线程。

  2. standalone独立集群模式---学习测试使用
    典型的Master/Slave模式。

  3. standalone-HA高可用模式---生产环境使用
    基于standalone模式,使用zk搭建高可用,避免Master是有单点故障的。

  4. on yarn集群模式---生产环境使用
    运行在yarn集群之上,由yarn负责资源管理,Spark负责任务调度和计算
    好处:计算资源按需伸缩,集群利用率高,共享底层存储,避免数据跨集群迁移。

  5. on mesos集群模式---国内使用较少
    运行在mesos资源管理器框架之上,由mesos负责资源管理,Spark负责任务调度和计算。

  6. on cloud集群模式---中小公司未来会更多使用云服务

Spark Core

RDD详解

  1. 为什么要有RDD?
    在许多迭代式算法(比如机器学习、图算法等)和交互式数据挖掘中,不同计算阶段之间会重用中间结果,即一个阶段的输出结果会作为下一个阶段的输入。
    但是,之前的 MapReduce 框架采用非循环式的数据流模型,把中间结果写入到 HDFS 中,带来了大量的数据复制、磁盘 IO 和序列化开销。且这些框架只能支持一些特定的计算模式(map/reduce),并没有提供一种通用的数据抽象。
    RDD 提供了一个抽象的数据模型,让我们不必担心底层数据的分布式特性,只需将具体的应用逻辑表达为一系列转换操作(函数),不同 RDD 之间的转换操作之间还可以形成依赖关系,进而实现管道化,从而避免了中间结果的存储,大大降低了数据复制、磁盘 IO 和序列化开销,并且还提供了更多的 API(map/reduec/filter/groupBy...)。

  2. RDD是什么?
    RDD叫做弹性分布式数据集,是Spark中最基本的数据抽象,代表一个不可变、可分区、里面的元素可并行计算的集合。
    Resilient :它是弹性的,RDD 中的数据可以保存在内存中或者磁盘里面;
    Distributed :它里面的元素是分布式存储的,可以用于分布式计算;
    Dataset: 它是一个集合,可以存放很多元素。

  3. RDD主要属性

  • A list of partitions
    一组分片(Partition)/一个分区(Partition)列表,即数据集的基本组成单位。
    对于 RDD 来说,每个分片都会被一个计算任务处理,分片数决定并行度。用户可以在创建 RDD 时指定 RDD 的分片个数,如果没有指定,那么就会采用默认值。

  • A function for computing each split
    一个函数会被作用在每一个分区。Spark 中 RDD 的计算是以分片为单位的,compute 函数会被作用到每个分区上。

  • A list of dependencies on other RDDs
    一个 RDD 会依赖于其他多个 RDD。RDD 的每次转换都会生成一个新的 RDD,所以 RDD 之间就会形成类似于流水线一样的前后依赖关系。
    在部分分区数据丢失时,Spark 可以通过这个依赖关系重新计算丢失的分区数据,而不是对 RDD 的所有分区进行重新计算。(Spark 的容错机制)

  • Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
    可选项,对于 KV 类型的 RDD 会有一个 Partitioner,即 RDD 的分区函数,默认为 HashPartitioner。

  • Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
    可选项,一个列表,存储存取每个 Partition 的优先位置(preferred location)。对于一个 HDFS 文件来说,这个列表保存的就是每个 Partition 所在的块的位置。按照"移动数据不如移动计算"的理念,Spark 在进行任务调度的时候,会尽可能选择那些存有数据的 worker 节点来进行任务计算。

总结
RDD 是一个数据集的表示,不仅表示了数据集,还表示了这个数据集从哪来,如何计算,主要属性包括:

  • 分区列表
  • 计算函数
  • 依赖关系
  • 分区函数(默认是 hash)
  • 最佳位置
    分区列表、分区函数、最佳位置,这三个属性其实说的就是数据集在哪,在哪计算更合适,如何分区;
    计算函数、依赖关系,这两个属性其实说的是数据集怎么来的。

RDD-API

  1. RDD的创建方式
  • 由外部存储系统的数据集创建,包括本地的文件系统,还有所有 Hadoop 支持的数据集,比如 HDFS、Cassandra、HBase 等
val rdd1 = sc.textFile("hdfs://node1:8020/wordcount/input/words.txt")
  • 通过已有的RDD经过算子转换生成新的RDD
val rdd2=rdd1.flatMap(_.split(" "))
  • 由一个已经存在的集合创建
val rdd3 = sc.parallelize(Array(1,2,3,4,5,6,7,8))或者
val rdd4 = sc.makeRDD(List(1,2,3,4,5,6,7,8))

2 RDD的算子分类
RDD的算子分为两类

  1. Transformation 转换操作
    返回一个新的RDD
  2. Action 动作操作
    返回值不是RDD(无返回值或返回其他的)

注意:
1、RDD 不实际存储真正要计算的数据,而是记录了数据的位置在哪里,数据的转换关系(调用了什么方法,传入什么函数)。
2、RDD 中的所有转换都是惰性求值/延迟执行的,也就是说并不会直接计算。只有当发生一个要求返回结果给 Driver 的 Action 动作时,这些转换才会真正运行。
3、之所以使用惰性求值/延迟执行,是因为这样可以在 Action 时对 RDD 操作形成 DAG 有向无环图进行 Stage 的划分和并行优化,这种设计让 Spark 更加有效率地运行。

  1. Transformation 转换算子
转换算子 含义
map(func) 返回一个新的 RDD,该 RDD 由每一个输入元素经过 func 函数转换后组成
filter(func) 返回一个新的 RDD,该 RDD 由经过 func 函数计算后返回值为 true 的输入元素组成
flatMap(func) 类似于 map,但是每一个输入元素可以被映射为 0 或多个输出元素(所以 func 应该返回一个序列,而不是单一元素)
mapPartitions(func) 类似于 map,但独立地在 RDD 的每一个分片上运行,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是 Iterator[T] => Iterator[U]
mapPartitionsWithIndex(func) 类似于 mapPartitions,但 func 带有一个整数参数表示分片的索引值,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是(Int, Interator[T]) => Iterator[U]
sample(withReplacement, fraction, seed) 根据 fraction 指定的比例对数据进行采样,可以选择是否使用随机数进行替换,seed 用于指定随机数生成器种子
union(otherDataset) 对源 RDD 和参数 RDD 求并集后返回一个新的 RDD
intersection(otherDataset) 对源 RDD 和参数 RDD 求交集后返回一个新的 RDD
distinct([numTasks])) 对源 RDD 进行去重后返回一个新的 RDD
groupByKey([numTasks]) 在一个(K,V)的 RDD 上调用,返回一个(K, Iterator[V])的 RDD
reduceByKey(func, [numTasks]) 在一个(K,V)的 RDD 上调用,返回一个(K,V)的 RDD,使用指定的 reduce 函数,将相同 key 的值聚合到一起,与 groupByKey 类似,reduce 任务的个数可以通过第二个可选的参数来设置
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) 对 PairRDD 中相同的 Key 值进行聚合操作,在聚合过程中同样使用了一个中立的初始值。和 aggregate 函数类似,aggregateByKey 返回值的类型不需要和 RDD 中 value 的类型一致
sortByKey([ascending], [numTasks]) 在一个(K,V)的 RDD 上调用,K 必须实现 Ordered 接口,返回一个按照 key 进行排序的(K,V)的 RDD
sortBy(func,[ascending], [numTasks]) 与 sortByKey 类似,但是更灵活
join(otherDataset, [numTasks]) 在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素对在一起的(K,(V,W))的 RDD
cogroup(otherDataset, [numTasks]) 在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD
cartesian(otherDataset) 笛卡尔积
pipe(command, [envVars]) 对 rdd 进行管道操作
coalesce(numPartitions) 减少 RDD 的分区数到指定值。在过滤大量数据之后,可以执行此操作
repartition(numPartitions) 重新给 RDD 分区
  1. Action 动作算子
动作算子 含义
reduce(func) 通过 func 函数聚集 RDD 中的所有元素,这个功能必须是可交换且可并联的
collect() 在驱动程序中,以数组的形式返回数据集的所有元素
count() 返回 RDD 的元素个数
first() 返回 RDD 的第一个元素(类似于 take(1))
take(n) 返回一个由数据集的前 n 个元素组成的数组
takeSample(withReplacement,num, [seed]) 返回一个数组,该数组由从数据集中随机采样的 num 个元素组成,可以选择是否用随机数替换不足的部分,seed 用于指定随机数生成器种子
takeOrdered(n, [ordering]) 返回自然顺序或者自定义顺序的前 n 个元素
saveAsTextFile(path) 将数据集的元素以 textfile 的形式保存到 HDFS 文件系统或者其他支持的文件系统,对于每个元素,Spark 将会调用 toString 方法,将它装换为文件中的文本
saveAsSequenceFile(path) 将数据集中的元素以 Hadoop sequencefile 的格式保存到指定的目录下,可以使 HDFS 或者其他 Hadoop 支持的文件系统
saveAsObjectFile(path) 将数据集的元素,以 Java 序列化的方式保存到指定的目录下
countByKey() 针对(K,V)类型的 RDD,返回一个(K,Int)的 map,表示每一个 key 对应的元素个数
foreach(func) 在数据集的每一个元素上,运行函数 func 进行更新
foreachPartition(func) 在数据集的每一个分区上,运行函数 func
  1. RDD 的持久化/缓存
    在实际开发中某些 RDD 的计算或转换可能会比较耗费时间,如果这些 RDD 后续还会频繁的被使用到,那么可以将这些 RDD 进行持久化/缓存,这样下次再使用到的时候就不用再重新计算了,提高了程序运行的效率。
val rdd1 = sc.textFile("hdfs://node01:8020/words.txt")
val rdd2 = rdd1.flatMap(x=>x.split(" ")).map((_,1)).reduceByKey(_+_)
rdd2.cache //缓存/持久化
rdd2.sortBy(_._2,false).collect//触发action,会去读取HDFS的文件,rdd2会真正执行持久化
rdd2.sortBy(_._2,false).collect//触发action,会去读缓存中的数据,执行速度会比之前快,因为rdd2已经持久化到内存中了

持久化/存储API详解

  • persist 方法和 cache 方法
    RDD 通过 persist 或 cache 方法可以将前面的计算结果缓存,但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 时,该 RDD 将会被缓存在计算节点的内存中,并供后面重用。

  • 存储级别
    默认的存储级别都是仅在内存存储一份,Spark 的存储级别还有好多种,存储级别在 object StorageLevel 中定义的。

持久化级别 说明
MORY_ONLY(默认) 将 RDD 以非序列化的 Java 对象存储在 JVM 中。如果没有足够的内存存储 RDD,则某些分区将不会被缓存,每次需要时都会重新计算。这是默认级别
MORY_AND_DISK(开发中可以使用这个) 将 RDD 以非序列化的 Java 对象存储在 JVM 中。如果数据在内存中放不下,则溢写到磁盘上.需要时则会从磁盘上读取
MEMORY_ONLY_SER (Java and Scala) 将 RDD 以序列化的 Java 对象(每个分区一个字节数组)的方式存储.这通常比非序列化对象(deserialized objects)更具空间效率,特别是在使用快速序列化的情况下,但是这种方式读取数据会消耗更多的 CPU
MEMORY_AND_DISK_SER (Java and Scala) 与 MEMORY_ONLY_SER 类似,但如果数据在内存中放不下,则溢写到磁盘上,而不是每次需要重新计算它们
DISK_ONLY 将 RDD 分区存储在磁盘上
MEMORY_ONLY_2, MEMORY_AND_DISK_2 等 与上面的储存级别相同,只不过将持久化数据存为两份,备份每个分区存储在两个集群节点上
OFF_HEAP(实验中) 与 MEMORY_ONLY_SER 类似,但将数据存储在堆外内存中。(即不是直接存储在 JVM 内存中)

总结:

  • RDD 持久化/缓存的目的是为了提高后续操作的速度
  • 缓存的级别有很多,默认只存在内存中,开发中使用 memory_and_disk
  • 只有执行 action 操作的时候才会真正将 RDD 数据进行持久化/缓存
  • 实际开发中如果某一个 RDD 后续会被频繁的使用,可以将该 RDD 进行持久化/缓存
  1. RDD容错机制
  • 持久化的局限
    持久化/缓存可以把数据放在内存中,虽然是快速的,但是也是最不可靠的;也可以把数据放在磁盘上,也不是完全可靠的!例如磁盘会损坏等。

  • 问题解决
    Checkpoint 的产生就是为了更加可靠的数据持久化,在 Checkpoint 的时候一般把数据放在在 HDFS 上,这就天然的借助了 HDFS 天生的高容错、高可靠来实现数据最大程度上的安全,实现了 RDD 的容错和高可用。

  • 用法

SparkContext.setCheckpointDir("目录") //HDFS的目录

RDD.checkpoint

总结:
开发中如何保证数据的安全性性及读取效率:可以对频繁使用且重要的数据,先做缓存/持久化,再做 checkpoint 操作。

持久化和 Checkpoint 的区别:

  • 位置
    Persist 和 Cache 只能保存在本地的磁盘和内存中(或者堆外内存--实验中) Checkpoint 可以保存数据到 HDFS 这类可靠的存储上。

  • 生命周期
    Cache 和 Persist 的 RDD 会在程序结束后会被清除或者手动调用 unpersist 方法 Checkpoint 的 RDD 在程序结束后依然存在,不会被删除。

  1. RDD 依赖关系
  1. 宽窄依赖
    两种依赖关系类型:RDD 和它依赖的父 RDD 的关系有两种不同的类型,即宽依赖(wide dependency/shuffle dependency)窄依赖(narrow dependency)
    窄依赖:父 RDD 的一个分区只会被子 RDD 的一个分区依赖
    宽依赖:父 RDD 的一个分区会被子 RDD 的多个分区依赖(涉及到 shuffle)

  2. 为什么要设计宽窄依赖
    对于窄依赖:
    窄依赖的多个分区可以并行计算;
    窄依赖的一个分区的数据如果丢失只需要重新计算对应的分区的数据就可以了。
    对于宽依赖:
    划分 Stage(阶段)的依据:对于宽依赖,必须等到上一阶段计算完成才能计算下一阶段。

  1. DAG 的生成和划分 Stage
  1. DAG 介绍
    DAG 是什么
    DAG(Directed Acyclic Graph 有向无环图)指的是数据转换执行的过程,有方向,无闭环(其实就是 RDD 执行的流程);
    原始的 RDD 通过一系列的转换操作就形成了 DAG 有向无环图,任务执行时,可以按照 DAG 的描述,执行真正的计算(数据被操作的一个过程)。
    DAG 的边界
    开始:通过 SparkContext 创建的 RDD;
    结束:触发 Action,一旦触发 Action 就形成了一个完整的 DAG。

  2. DAG 划分 Stage
    image

一个 Spark 程序可以有多个 DAG(有几个 Action,就有几个 DAG,上图最后只有一个 Action(图中未表现),那么就是一个 DAG)。

一个 DAG 可以有多个 Stage(根据宽依赖/shuffle 进行划分)。

同一个 Stage 可以有多个 Task 并行执行(task 数=分区数,如上图,Stage1 中有三个分区 P1、P2、P3,对应的也有三个 Task)。

可以看到这个 DAG 中只有 reduceByKey 操作是一个宽依赖,Spark 内核会以此为边界将其前后划分成不同的 Stage。

同时我们可以注意到,在图中 Stage1 中,从 textFile 到 flatMap 到 map 都是窄依赖,这几步操作可以形成一个流水线操作,通过 flatMap 操作生成的 partition 可以不用等待整个 RDD 计算结束,而是继续进行 map 操作,这样大大提高了计算的效率。

为什么要划分 Stage? --并行计算
一个复杂的业务逻辑如果有 shuffle,那么就意味着前面阶段产生结果后,才能执行下一个阶段,即下一个阶段的计算要依赖上一个阶段的数据。
那么我们按照 shuffle 进行划分(也就是按照宽依赖就行划分),就可以将一个 DAG 划分成多个 Stage/阶段,在同一个 Stage 中,会有多个算子操作,可以形成一个 pipeline 流水线,流水线内的多个平行的分区可以并行执行。

如何划分 DAG 的 stage?
对于窄依赖,partition 的转换处理在 stage 中完成计算,不划分(将窄依赖尽量放在在同一个 stage 中,可以实现流水线计算)。

对于宽依赖,由于有 shuffle 的存在,只能在父 RDD 处理完成后,才能开始接下来的计算,也就是说需要要划分 stage。

总结:

Spark 会根据 shuffle/宽依赖使用回溯算法来对 DAG 进行 Stage 划分,从后往前,遇到宽依赖就断开,遇到窄依赖就把当前的 RDD 加入到当前的 stage/阶段中

  1. RDD累加器和广播变量
    在默认情况下,当 Spark 在集群的多个不同节点的多个任务上并行运行一个函数时,它会把函数中涉及到的每个变量,在每个任务上都生成一个副本。
    但是,有时候需要在多个任务之间共享变量,或者在任务(Task)和任务控制节点(Driver Program)之间共享变量。
    为了满足这种需求,Spark 提供了两种类型的变量:
    • 累加器 accumulators:累加器支持在所有不同节点之间进行累加计算(比如计数或者求和)。
    • 广播变量 broadcast variables:广播变量用来把变量在所有节点的内存之间进行共享,在每个机器上缓存一个只读的变量,而不是为机器上的每个任务都生成一个副本。

Spark SQL

数据分析方式

  1. 命令式
    在前面的 RDD 部分, 非常明显可以感觉的到是命令式的, 主要特征是通过一个算子, 可以得到一个结果, 通过结果再进行后续计算。
    命令式的优点

    • 操作粒度更细,能够控制数据的每一个处理环节;

    • 操作更明确,步骤更清晰,容易维护;

    • 支持半/非结构化数据的操作。

    命令式的缺点

    • 需要一定的代码功底;

    • 写起来比较麻烦。

  2. SQL
    对于一些数据科学家/数据库管理员/DBA, 要求他们为了做一个非常简单的查询, 写一大堆代码, 明显是一件非常残忍的事情, 所以 SQL on Hadoop 是一个非常重要的方向。
    SQL 的优点

    • 表达非常清晰

    SQL 的缺点

    • 试想一下 3 层嵌套的 SQL 维护起来应该挺力不从心的吧;
    • 试想一下如果使用 SQL 来实现机器学习算法也挺为难的吧。
  3. 总结
    SQL 擅长数据分析和通过简单的语法表示查询,命令式操作适合过程式处理和算法性的处理。
    在 Spark 出现之前,对于结构化数据的查询和处理, 一个工具一向只能支持 SQL 或者命令式,使用者被迫要使用多个工具来适应两种场景,并且多个工具配合起来比较费劲。
    而 Spark 出现了以后,统一了两种数据处理范式是一种革新性的进步。

Hive和SparkSQL

Hive 是将 SQL 转为 MapReduce。

SparkSQL 可以理解成是将 SQL 解析成:“RDD + 优化” 再执行。

数据分类和 SparkSQL 适用场景

  1. 结构化数据
    一般指数据有固定的 Schema(约束),例如在用户表中,name 字段是 String 型,那么每一条数据的 name 字段值都可以当作 String 来使用:
id name url alexa country
1 Google https://www.google.cm/ 1 USA
2 淘宝 https://www.taobao.com/ 13 CN
3 菜鸟教程 https://www.runoob.com/ 4689 CN
4 微博 http://weibo.com/ 20 CN
5 Facebook https://www.facebook.com/ 3 USA
  1. 半结构化数据
    一般指的是数据没有固定的 Schema,但是数据本身是有结构的。
  • 没有固定 Schema
    指的是半结构化数据是没有固定的 Schema 的,可以理解为没有显式指定 Schema。
    比如说一个用户信息的 JSON 文件,
    第 1 条数据的 phone_num 有可能是数字,
    第 2 条数据的 phone_num 虽说应该也是数字,但是如果指定为 String,也是可以的,
    因为没有指定 Schema,没有显式的强制的约束。

  • 有结构
    虽说半结构化数据是没有显式指定 Schema 的,也没有约束,但是半结构化数据本身是有有隐式的结构的,也就是数据自身可以描述自身。
    例如 JSON 文件,其中的某一条数据是有字段这个概念的,每个字段也有类型的概念,所以说 JSON 是可以描述自身的,也就是数据本身携带有元信息。

  1. 总结
  • 数据分类总结:
定义 特点 举例
结构化数据 有固定的 Schema 有预定义的 Schema 关系型数据库的表
半结构化数据 没有固定的 Schema,但是有结构 没有固定的 Schema,有结构信息,数据一般是自描述的 指一些有结构的文件格式,例如 JSON
非结构化数据 没有固定 Schema,也没有结构 没有固定 Schema,也没有结构 指图片/音频之类的格式
  • Spark 处理什么样的数据?
    RDD 主要用于处理非结构化数据 、半结构化数据、结构化;
    SparkSQL 主要用于处理结构化数据(较为规范的半结构化数据也可以处理)。

  • 总结
    SparkSQL 是一个既支持 SQL 又支持命令式数据处理的工具;
    SparkSQL 的主要适用场景是处理结构化数据(较为规范的半结构化数据也可以处理)。

Spark SQL 数据抽象

  1. DataFrame
    DataFrame 的前身是 SchemaRDD,从 Spark 1.3.0 开始 SchemaRDD 更名为 DataFrame。并不再直接继承自 RDD,而是自己实现了 RDD 的绝大多数功能。
    DataFrame 是一种以 RDD 为基础的分布式数据集,类似于传统数据库的二维表格,带有 Schema 元信息(可以理解为数据库的列名和类型)。
    总结:
    DataFrame 就是一个分布式的表;
    DataFrame = RDD - 泛型 + SQL 的操作 + 优化。

  2. DataSet
    DataSet 是在 Spark1.6 中添加的新的接口。
    与 RDD 相比,保存了更多的描述信息,概念上等同于关系型数据库中的二维表。
    与 DataFrame 相比,保存了类型信息,是强类型的,提供了编译时类型检查。
    调用 Dataset 的方法先会生成逻辑计划,然后被 spark 的优化器进行优化,最终生成物理计划,然后提交到集群中运行!
    DataSet 包含了 DataFrame 的功能。
    Spark2.0 中两者统一,DataFrame 表示为 DataSet[Row],即 DataSet 的子集。

  3. 总结
    DataFrame = RDD - 泛型 + Schema + SQL + 优化
    DataSet = DataFrame + 泛型
    DataSet = RDD + Schema + SQL + 优化

总结:

  1. DataFrame 和 DataSet 都可以通过 RDD 来进行创建;

  2. 也可以通过读取普通文本创建--注意:直接读取没有完整的约束,需要通过 RDD+Schema;

  3. 通过 josn/parquet 会有完整的约束;

  4. 不管是 DataFrame 还是 DataSet 都可以注册成表,之后就可以使用 SQL 进行查询了! 也可以使用 DSL!

Spark Streaming

Spark Streaming 是一个基于 Spark Core 之上的实时计算框架,可以从很多数据源消费数据并对数据进行实时的处理,具有高吞吐量和容错能力强等特点。

Spark Streaming 的特点:

  • 易用
    可以像编写离线批处理一样去编写流式程序,支持 java/scala/python 语言。

  • 容错
    SparkStreaming 在没有额外代码和配置的情况下可以恢复丢失的工作。

  • 易整合到 Spark 体系
    流式处理与批处理和交互式查询相结合。

整体流程

Spark Streaming 中,会有一个接收器组件 Receiver,作为一个长期运行的 task 跑在一个 Executor 上。Receiver 接收外部的数据流形成 input DStream。

DStream 会被按照时间间隔划分成一批一批的 RDD,当批处理间隔缩短到秒级时,便可以用于处理实时数据流。时间间隔的大小可以由参数指定,一般设在 500 毫秒到几秒之间。

对 DStream 进行操作就是对 RDD 进行操作,计算处理的结果可以传给外部系统。

Spark Streaming 的工作流程像下面的图所示一样,接受到实时数据后,给数据分批次,然后传给 Spark Engine 处理最后生成该批次的结果。

数据抽象

Spark Streaming 的基础抽象是 DStream(Discretized Stream,离散化数据流,连续不断的数据流),代表持续性的数据流和经过各种 Spark 算子操作后的结果数据流。

可以从以下多个角度深入理解 DStream:

  1. DStream 本质上就是一系列时间上连续的 RDD
  2. 对 DStream 的数据的进行操作也是按照 RDD 为单位来进行的
  3. 容错性,底层 RDD 之间存在依赖关系,DStream 直接也有依赖关系,RDD 具有容错性,那么 DStream 也具有容错性
  4. 准实时性/近实时性
    Spark Streaming 将流式计算分解成多个 Spark Job,对于每一时间段数据的处理都会经过 Spark DAG 图分解以及 Spark 的任务集的调度过程。
    对于目前版本的 Spark Streaming 而言,其最小的 Batch Size 的选取在 0.5~5 秒钟之间。
    所以 Spark Streaming 能够满足流式准实时计算场景,对实时性要求非常高的如高频实时交易场景则不太适合。

总结
简单来说 DStream 就是对 RDD 的封装,你对 DStream 进行操作,就是对 RDD 进行操作。

对于 DataFrame/DataSet/DStream 来说本质上都可以理解成 RDD。

DStream 相关操作

DStream 上的操作与 RDD 的类似,分为以下两种:

Transformations(转换)

Output Operations(输出)/Action

  • Transformations
    以下是常见 Transformation---都是无状态转换:即每个批次的处理不依赖于之前批次的数据:
Transformation 含义
map(func) 对 DStream 中的各个元素进行 func 函数操作,然后返回一个新的 DStream
flatMap(func) 与 map 方法类似,只不过各个输入项可以被输出为零个或多个输出项
filter(func) 过滤出所有函数 func 返回值为 true 的 DStream 元素并返回一个新的 DStream
union(otherStream) 将源 DStream 和输入参数为 otherDStream 的元素合并,并返回一个新的 DStream
reduceByKey(func, [numTasks]) 利用 func 函数对源 DStream 中的 key 进行聚合操作,然后返回新的(K,V)对构成的 DStream
join(otherStream, [numTasks]) 输入为(K,V)、(K,W)类型的 DStream,返回一个新的(K,(V,W)类型的 DStream
transform(func) 通过 RDD-to-RDD 函数作用于 DStream 中的各个 RDD,可以是任意的 RDD 操作,从而返回一个新的 RDD

除此之外还有一类特殊的 Transformations---有状态转换:当前批次的处理需要使用之前批次的数据或者中间结果。

有状态转换包括基于追踪状态变化的转换(updateStateByKey)和滑动窗口的转换:

  1. UpdateStateByKey(func)

  2. Window Operations 窗口操作

  • Output/Action
    Output Operations 可以将 DStream 的数据输出到外部的数据库或文件系统。

当某个 Output Operations 被调用时,spark streaming 程序才会开始真正的计算过程(与 RDD 的 Action 类似)。

Output Operation 含义
print() 打印到控制台
saveAsTextFiles(prefix, [suffix]) 保存流的内容为文本文件,文件名为"prefix-TIME_IN_MS[.suffix]"
saveAsObjectFiles(prefix,[suffix]) 保存流的内容为 SequenceFile,文件名为 "prefix-TIME_IN_MS[.suffix]"
saveAsHadoopFiles(prefix,[suffix]) 保存流的内容为 hadoop 文件,文件名为"prefix-TIME_IN_MS[.suffix]"
foreachRDD(func) 对 Dstream 里面的每个 RDD 执行 func

Structured Streaming

在 2.0 之前,Spark Streaming 作为核心 API 的扩展,针对实时数据流,提供了一套可扩展、高吞吐、可容错的流式计算模型。
Spark Streaming 会接收实时数据源的数据,并切分成很多小的 batches,然后被 Spark Engine 执行,产出同样由很多小的 batchs 组成的结果流。
本质上,这是一种 micro-batch(微批处理)的方式处理,用批的思想去处理流数据.这种设计让Spark Streaming 面对复杂的流式处理场景时捉襟见肘。

spark streaming 这种构建在微批处理上的流计算引擎,比较突出的问题就是处理延时较高(无法优化到秒以下的数量级),以及无法支持基于 event_time 的时间窗口做聚合逻辑。

spark 在 2.0 版本中发布了新的流计算的 API,Structured Streaming/结构化流。

Structured Streaming 是一个基于 Spark SQL 引擎的可扩展、容错的流处理引擎。
统一了流、批的编程模型,你可以使用静态数据批处理一样的方式来编写流式计算操作。
并且支持基于 event_time 的时间窗口的处理逻辑。

默认情况下,结构化流式查询使用微批处理引擎进行处理,该引擎将数据流作为一系列小批处理作业进行处理,从而实现端到端的延迟,最短可达 100 毫秒,并且完全可以保证一次容错。
自 Spark 2.3 以来,引入了一种新的低延迟处理模式,称为连续处理,它可以在至少一次保证的情况下实现低至 1 毫秒的端到端延迟。
也就是类似于 Flink 那样的实时流,而不是小批量处理。
实际开发可以根据应用程序要求选择处理模式,但是连续处理在使用的时候仍然有很多限制,目前大部分情况还是应该采用小批量模式。

1 Spark Streaming & Structured Streaming

  • Spark Streaming 时代 -DStream-RDD
    Spark Streaming 采用的数据抽象是 DStream,而本质上就是时间上连续的 RDD,对数据流的操作就是针对 RDD 的操作。

  • Structured Streaming 时代 - DataSet/DataFrame -RDD
    Structured Streaming 是 Spark2.0 新增的可扩展和高容错性的实时计算框架,它构建于 Spark SQL 引擎,把流式计算也统一到 DataFrame/Dataset 里去了。
    Structured Streaming 相比于 Spark Streaming 的进步就类似于 Dataset 相比于 RDD 的进步。

  1. 核心思想
    Structured Streaming 最核心的思想就是将实时到达的数据看作是一个不断追加的 unbound table 无界表,到达流的每个数据项(RDD)就像是表中的一个新行被附加到无边界的表中.这样用户就可以用静态结构化数据的批处理查询方式进行流计算,如可以使用 SQL 对到来的每一行数据进行实时查询处理。

  2. 应用场景
    Structured Streaming 将数据源映射为类似于关系数据库中的表,然后将经过计算得到的结果映射为另一张表,完全以结构化的方式去操作流式数据,这种编程模型非常有利于处理分析结构化的实时数据;

Spark的两种核心Shuffle

有些运算需要将各节点上的同一类数据汇集到某一节点进行计算,把这些分布在不同节点上的数据按照一定的规则汇集到一起的过程称为shuffle。

Spark Shuffle 分为两种:一种是基于 Hash 的 Shuffle;另一种是基于 Sort 的 Shuffle
在 Spark 2.0 版本中, Hash Shuffle 方式己经不再使用。
使用 HashShuffle 的 Spark 在 Shuffle 时产生大量的文件。当数据量越来越多时,产生的文件量是不可控的,这严重制约了 Spark 的性能及扩展能力

Sorted-Based Shuffle 也有缺点,其缺点反而是它排序的特性,它强制要求数据在 Mapper 端必须先进行排序,所以导致它排序的速度有点慢。好在出现了 Tungsten-Sort Shuffle ,它对排序算法进行了改进,优化了排序的速度。Tungsten-Sort Shuffle 已经并入了 Sorted-Based Shuffle,Spark 的引擎会自动识别程序需要的是 Sorted-Based Shuffle,还是 Tungsten-Sort Shuffle。
以下仅介绍SortShuffle执行原理

SortShuffle

SortShuffleManager 的运行机制主要分成三种:

  1. 普通运行机制;

  2. bypass 运行机制,当 shuffle read task 的数量小于等于spark.shuffle.sort.bypassMergeThreshold参数的值时(默认为 200),就会启用 bypass 机制;

  3. Tungsten Sort 运行机制,开启此运行机制需设置配置项 spark.shuffle.manager=tungsten-sort。开启此项配置也不能保证就一定采用此运行机制(后面会解释)。

普通运行机制

在该模式下,数据会先写入一个内存数据结构中,此时根据不同的 shuffle 算子,可能选用不同的数据结构。如果是 reduceByKey 这种聚合类的 shuffle 算子,那么会选用 Map 数据结构,一边通过 Map 进行聚合,一边写入内存;如果是 join 这种普通的 shuffle 算子,那么会选用 Array 数据结构,直接写入内存。接着,每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界阈值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。

在溢写到磁盘文件之前,会先根据 key 对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入磁盘文件。默认的 batch 数量是 10000 条,也就是说,排序好的数据,会以每批 1 万条数据的形式分批写入磁盘文件。写入磁盘文件是通过 Java 的 BufferedOutputStream 实现的。

一个 task 将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时文件。最后会将之前所有的临时磁盘文件都进行合并,这就是merge 过程,此时会将之前所有临时磁盘文件中的数据读取出来,然后依次写入最终的磁盘文件之中。此外,由于一个 task 就只对应一个磁盘文件,也就意味着该 task 为下游 stage 的 task 准备的数据都在这一个文件中,因此还会单独写一份索引文件,其中标识了下游各个 task 的数据在文件中的 start offset 与 end offset。

SortShuffleManager 由于有一个磁盘文件 merge 的过程,因此大大减少了文件数量。

普通运行机制的 SortShuffleManager 工作原理如下图所示:
image

bypass运行机制

Reducer 端任务数比较少的情况下,基于 Hash Shuffle 实现机制明显比基于 Sort Shuffle 实现机制要快,因此基于 Sort huffle 实现机制提供了一个回退方案,就是 bypass 运行机制。对于 Reducer 端任务数少于配置属性spark.shuffle.sort.bypassMergeThreshold设置的个数时,使用带 Hash 风格的回退计划。

bypass 运行机制的触发条件如下:

  • shuffle map task 数量小于spark.shuffle.sort.bypassMergeThreshold=200参数的值。
  • 不是聚合类的 shuffle 算子。

此时,每个 task 会为每个下游 task 都创建一个临时磁盘文件,并将数据按 key 进行 hash 然后根据 key 的 hash 值,将 key 写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。

该过程的磁盘写机制其实跟未经优化的 HashShuffleManager 是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的 HashShuffleManager 来说,shuffle read 的性能会更好。

而该机制与普通 SortShuffleManager 运行机制的不同在于:第一,磁盘写机制不同;第二,不会进行排序。也就是说,启用该机制的最大好处在于,shuffle write 过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销

bypass 运行机制的 SortShuffleManager 工作原理如下图所示:
image

Tungsten Sort Shuffle 运行机制

基于 Tungsten Sort 的 Shuffle 实现机制主要是借助 Tungsten 项目所做的优化来高效处理 Shuffle。

Spark 提供了配置属性,用于选择具体的 Shuffle 实现机制,但需要说明的是,虽然默认情况下 Spark 默认开启的是基于 SortShuffle 实现机制,但实际上,参考 Shuffle 的框架内核部分可知基于 SortShuffle 的实现机制与基于 Tungsten Sort Shuffle 实现机制都是使用 SortShuffleManager,而内部使用的具体的实现机制,是通过提供的两个方法进行判断的:

对应非基于 Tungsten Sort 时,通过 SortShuffleWriter.shouldBypassMergeSort 方法判断是否需要回退到 Hash 风格的 Shuffle 实现机制,当该方法返回的条件不满足时,则通过 SortShuffleManager.canUseSerializedShuffle 方法判断是否需要采用基于 Tungsten Sort Shuffle 实现机制,而当这两个方法返回都为 false,即都不满足对应的条件时,会自动采用普通运行机制。

因此,当设置了 spark.shuffle.manager=tungsten-sort 时,也不能保证就一定采用基于 Tungsten Sort 的 Shuffle 实现机制。

要实现 Tungsten Sort Shuffle 机制需要满足以下条件:

  • Shuffle 依赖中不带聚合操作或没有对输出进行排序的要求。

  • Shuffle 的序列化器支持序列化值的重定位(当前仅支持 KryoSerializer Spark SQL 框架自定义的序列化器)。

  • Shuffle 过程中的输出分区个数少于 16777216 个。

Spark底层执行原理

具体运行流程如下:

  1. SparkContext 向资源管理器注册并向资源管理器申请运行 Executor

  2. 资源管理器分配 Executor,然后资源管理器启动 Executor

  3. Executor 发送心跳至资源管理器

  4. SparkContext 构建 DAG 有向无环图

  5. 将 DAG 分解成 Stage(TaskSet)

  6. 把 Stage 发送给 TaskScheduler

  7. Executor 向 SparkContext 申请 Task

  8. TaskScheduler 将 Task 发送给 Executor 运行

  9. 同时 SparkContext 将应用程序代码发放给 Executor

  10. Task 在 Executor 上运行,运行完毕释放所有资源

Spark 数据倾斜

注意,要区分开数据倾斜与数据过量这两种情况,数据倾斜是指少数task被分配了绝大多数的数据,因此少数task运行缓慢;数据过量是指所有task被分配的数据量都很大,相差不多,所有task都运行缓慢。

Spark中的数据倾斜问题主要指shuffle过程中出现的数据倾斜问题,是由于不同的key对应的数据量不同导致的不同task所处理的数据量不同的问题。

数据倾斜的表现:

  1. Spark作业的大部分task都执行迅速,只有有限的几个task执行的非常慢,此时可能出现了数据倾斜,作业可以运行,但是运行得非常慢;

  2. Spark作业的大部分task都执行迅速,但是有的task在运行过程中会突然报出OOM,反复执行几次都在某一个task报出OOM错误,此时可能出现了数据倾斜,作业无法正常运行。定位数据倾斜问题:

  3. 查阅代码中的shuffle算子,例如reduceByKey、countByKey、groupByKey、join等算子,根据代码逻辑判断此处是否会出现数据倾斜;

  4. 查看Spark作业的log文件,log文件对于错误的记录会精确到代码的某一行,可以根据异常定位到的代码位置来明确错误发生在第几个stage,对应的shuffle算子是哪一个;

解决方案

  1. 预聚合原始数据
  2. 预处理导致倾斜的keu
  3. 提高reduce并行度
  4. 使用map join

参考链接

史上最强干货!Spark知识点总结

posted @ 2021-10-13 20:01  cos晓风残月  阅读(418)  评论(0编辑  收藏  举报
*