前言:
Spark与Hadoop的根本差异是多个作业之间的数据通信问题:Spark多个作业之间数据通信是基于内存,而Hadoop是基于磁盘
环境搭建
本次示例环境为Windows环境,需要提前安装Scala(v2.12.10)、Hadoop(v3.2.3)以及spark(v3.0.0)
Scala
IDEA 下载Scala SDK对应版本的jar(scala-sdk-2.12.10),方便后续程序依赖使用
hadoop
进入hadoop官网下载对应Windows版本的压缩包(hadoop-3.2.3.tar.gz),管理员身份运行cmd后在对应路径下输入命令 tar -zxvf hadoop-3.2.3.tar.gz 解压,解压后配置HADOOP_HOME环境变量并加入到系统变量Path中
验证:cmd -> hadoop version
spark
进入spark官网下载对应Windows版本的压缩包(spark-3.0.0-bin-hadoop3.2.tgz),管理员身份运行cmd后再对应路径下输入命令 tar -zxvf spark-3.0.0-bin-hadoop3.2.tgz 解压,解压后配置SPARK_HOME环境变量并加入到系统变量Path中
验证:cmd -> spark-shell
Spark详解
运行结构
框架核心是一个计算引擎,整体采用master-slave运行结构。
如图,Driver表示master, 负责管理整个集群中的任务调度;executor表示salve,负责实际执行任务
Driver
Spark驱动器节点,用于执行Spark任务中的main方法,负责实际代码的执行工作。
主要负责:
- 将用户程序转为作业Job
- 在Executor之间调度任务Task
- 跟踪执行器的执行情况
- 通过UI展示查询运行情况
Executor
集群中工作节点Worker的一个JVM进程,负责在Spark作业中运行具体任务,任务彼此间独立。Spark启动时同时启动Executor节点,始终伴随Spark整个生命周期存在, 若有Executor中途崩溃则Spark会将崩溃的节点上的任务调度到其他正常的Executor节点。
主要负责:
- 负责运行组成Spark的任务,并将结果返回给Driver进程
- 通过自身的块管理器Block Manager为用户程序中要求缓存的RDD提供内存式存储。RDD是直接缓存在Executor进程内,故任务可以利用缓存加速运算
Master&Worker
Master主要负责资源调度和分配并进行集群的监控等职责,类似Yarn环境的RM;Worker一个进程运行在及权重的一台机器上,由Master分配资源对数据进行并行处理和计算,类似Yarn环境的NM
ApplicationMaster
Hadoop向Yarn集群提交应用时应该包含ApplicationMaster,用于向资源调度器申请执行任务的资源容器,运行用户自己的程序任务Job,监控整个任务执行,跟踪任务状态,处理任务失败等异常情况
并行度
并行度:整个集群并行执行任务的数量
DAG有向无环图
由点和线组成的具有方向不会闭环的拓扑图,可以理解为任务调度先走哪个后走哪个
提交应用参数说明
bin/spark-submit \
--class <main-class>
--master <master-url> \
... # other options
<application-jar> \
[application-arguments]
参数 | 说明 |
---|---|
–class | Spark程序中包含主函数main的类 |
–master | Spark程序运行模式/环境(模式枚举:local[*], spark://linux1:7077, Yarn) |
–executor-memory 1G | 指定每个executor可用内存1G |
–total-executor-cores 2 | 指定所有executor使用的CPU核数为2 |
–executor-cores | 指定每个executor使用的CPU核数 |
application-jar | 打好包的应用jar |
application-arguments | 命令行参数,如示例中的“10”,表示程序入口参数用于设定当前应用的任务数量为10个任务 |
数据结构
Spark封装三大数据结构,RDD(弹性分布式数据集)、累加器(分布式共享只写变量)、广播变量(分布式共享只读变量)
RDD
1)概念
RDD:弹性分布式数据集,Spark最基本的数据处理模型,也是Spark中最小计算单元。代码层面是一个抽象类,代表一个弹性的、不可变、可分区、元素可并行计算的集合。
弹性:
- 存储弹性:内存与磁盘的自动切换
- 容错弹性:数据丢失可以自动恢复
- 计算弹性:计算出错重试机制
- 分片弹性:可根据需要重新分片
不可变:RDD封装计算逻辑,是不可以改变的,若要改变只能产生新的RDD并且在新的RDD中添加新的计算逻辑
可分区、并行计算
分布式:数据存储在大数据集群不同节点上
数据集:RDD封装计算逻辑,不保存数据
数据抽象:RDD是抽象类,需要子类自行实现
RDD数据处理方式类似java IO流装饰者模式,其只有在调用collect方法时才会真正执行业务逻辑操作,前面的操作只是功能的扩展。
RDD不保存数据,而java IO可以临时保留一部分数据。
2)核心属性
RDD核心属性 | 描述 |
---|---|
分区列表partitions_ | 用于执行任务时并行计算,实现分布式计算的重要属性 |
分区计算函数compute | 用于对每个分区进行计算 |
多个RDD之间的依赖列表dependencies_ | 比如reduceByKey RDD依赖map RDD,map RDD依赖flatMap RDD, flatMap RDD依赖textFile RDD |
分区器partitioner | 用于确定将数据分配给哪个分区 |
首选位置getPreferredLocations | 用于确定将计算发送给哪个Executor执行最优 |
3)执行原理
RDD执行流程:
- 用户提交的RDD代码 -> DAGScheduler逻辑任务 -> TaskScheduler任务分配和管理分配 -> Worker工作
SparkSQL自动优化机制:Catalyst优化器
- SparkSQL的API层面会接收SQL语句,接着交给Catalyst优化器进行解析并生成执行计划等操作,然后输出RDD的执行计划,最后通过Spark集群中的DAGScheduler->TaskScheduler->Worker执行任务
Catalyst优化器工作流程:
- 解析SQL,并生成AST抽象语法树
- 在AST抽象语法树中加入元数据信息(此处可以理解为做标记,如id#1#L),方便Spark快速计算
- 对已经加入元数据信息的AST抽象语法树,进行断言下推(如先进行过滤再JOIN,将逻辑判断提到前面减少Shuffle数据量提高性能)和列值裁剪(如select只需要查询出某几个字段则再读取数据源时候只需读取那几个字段即可,将加载的列进行裁剪从而减少数据量提高性能)等进行优化
- 根据优化后的AST逻辑计划生成物理计划从而生成RDD运行
从计算角度来看,数据处理需要将计算资源(CPU&内存)和计算模型(逻辑)协调整合。
Spark框架执行时,先申请计算资源,然后将应用程序的数据处理逻辑分解成一个个的计算任务,然后将计算任务发到已分配资源的计算节点上,按照指定的计算模型进行数据计算,最后得到计算结果。
RDD主要职责:封装计算逻辑,生成Task,再由调度节点Driver将Task分发到对应计算节点Executor
Yarn模式下RDD工作原理:
- 启动Yarn集群环境(ResourceManager管理资源的角色,NodeManager真正干活的角色)
- Spark申请资源创建调度节点和计算节点(Driver调度节点,Executor计算节点,它们均工作在NodeManager中)
- 将计算逻辑根据分区划分到不同的任务
- 调度节点Driver根据计算节点状态和首选位置的配置将任务Task发送到对应的计算节点Executor
4)RDD算子(RDD方法)
RDD算子分两种,转换算子和行动算子。转换算子分为Value类型、双Value类型和Key-Value类型,转换算子是封装Job逻辑的方法,而行动算子是触发Job执行的方法,底层调用环境对象的runJob方法,创建ActiveJob并提交执行。
RDD方法与Scala集合对象方法的区别:
- 集合对象的方法都是在同一个节点的内存中完成的
- RDD方法可以将计算逻辑发送到Executor端(分布式节点)执行,RDD方法外部的操作都是在Driver端执行,而RDD方法内部逻辑则是在Executor端执行,为区分不同的处理方式将RDD的方法称为算子
5)RDD序列化
RDD算子中传递的函数是包含闭包操作,会进行闭包检测功能,检查函数中引用到函数外的变量是否已序列化
-
闭包检测
算子外的代码都是在Driver执行,算子内的代码都是在Executor执行。
Scala函数式编程中算子内经常用到算子外的数据,这样就形成了闭包效果,若使用到算子外的数据无法序列化意味着无法传值给Executor从而导致错误,所以在执行任务计算前,检测闭包内的对象是否可以进行序列化操作即为闭包检测。 -
序列化方法和属性
//类的构造参数为类的属性,构造参数需要进行闭包检测等同于类需要进行闭包检测
class Person(name: String) extends Serializable {
def isMatch(s: String) : Boolean = {
s.contains(this.name)
}
//函数序列化
def filterFunc(rdd: RDD[String]) : RDD[String] = {
rdd.filter(isMatch)
}
//属性序列化
def filterAttr(rdd: RDD[String]) : RDD[String] = {
rdd.filter(s => s.contains(name))
//或者可以不需要将Person混入序列化特质,改变属性name的生命周期即可
// val temp = name
// rdd.filter(s => s.contains(temp))
}
}
- Kryo序列化
由于Java自带的序列化机制可以序列化任何对象,因为字节多所以对象序列化的较大,属于比较重的一种方式。
考虑到性能,Spark2.0开始支持Kryo序列化机制,Kryo速度是Serializable的10倍,当RDD在Shuffle数据的时候简单数据类型、数组和字符串类型已经在Spark内部使用Kryo来序列化(注意:使用Kryo也需要extends Serializable)
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("RddTest_Par")
//使用Kryo序列化机制
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer").registerKryoClasses(Array(classOf[User]))
val sc = new SparkContext(conf)
- RDD依赖关系 & 血缘关系
新RDD依赖旧RDD,相邻两个RDD之间称为依赖关系,多个连续RDD的依赖关系称为血缘关系;为提供容错性,每个RDD中都会保存血缘关系,一旦某个RDD执行出现错误,可以根据血缘关系将数据源重新读取进行计算。
OneToOneDependencies OneToOne依赖(窄依赖),即新RDD的一个分区的数据依赖于旧RDD的一个分区的数据
ShuffledDependencies Shuffle依赖(宽依赖),即新RDD的一个分区的数据依赖于旧RDD的多个分区的数据
窄依赖中分区Partition与任务Task关系:一个分区对应一个任务
宽依赖中分区Partition与任务Task关系:新RDD与旧RDD处于不同的Stage阶段,上游Stage中Task任务执行完毕后再通知到下游Stage中Task任务执行(相同stage中的Task数量与分区数对应)
Stage与Shuffle依赖的数量关系:
Stage阶段的数量 = shuffle依赖的数量 + 1;ShuffleMapStage可能有多个,而ResultStage结果阶段只有一个,为最后执行结果阶段。
RDD任务划分:
任务划分中间分为:Application、Job、Stage和Task(Application->Job->Stage->Task 每层都是1对N关系)
Applicaiton: 初始化一个SparkContext即生成一个Application
Job:一个Action算子生成一个Job
Stage:Stage数量等于宽依赖(Shuffle依赖)数量加一
Task:当前Stage阶段最后一个RDD的分区个数就是Task个数
总结:一个Application应用程序有几个行动算子即表示有几个Job作业;一个Job作业有几个Shuffle依赖就有(几+1)个Stage阶段;一个Stage阶段中最后一个RDD有几个分区数量就有几个Task任务。
-
RDD持久化
RDD对象可重用,而RDD数据不可重用,若重用RDD对象,只能从头获取数据源进行处理。持久化操作必须在行动算子执行时完成;
rdd.cache() 持久化到缓存
rdd.persist(StorageLevel.DISK_ONLY) 持久化到磁盘文件
rdd.checkpoint() 持久化到检查点路径(设置检查点路径sc.setCheckpointDir(“cpd”),一般设置到分布式存储路径中如hdfs)(需要落盘,需要指定检查点保存路径,作业执行完后不会被删除;为保证数据安全一般情况下checkPoint会独立执行作业)
为提高效率,一般cache与checkpoint结合使用,先cache, 再checkpoint
应用场景:1.对象重用 2.数据较重要或数据执行时间较长的场景 -
RDD分区器
package com.itjeffrey.spark.core.rdd
import org.apache.spark.rdd.RDD
import org.apache.spark.{Partitioner, SparkConf, SparkContext}
/**
* 自定义RDD分区器
*
* @From: Jeffrey
* @Date: 2022/11/9
*/
object RddPartition {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("RddTest_Partitioner")
val sc = new SparkContext(conf)
val rdd: RDD[(String, String)] = sc.makeRDD(List(("jeffrey", "j10086"), ("qiutee", "q12345"), ("yifei", "y11111")))
val parRdd: RDD[(String, String)] = rdd.partitionBy(new CustomPartitioner)
parRdd.saveAsTextFile("output")
sc.stop()
}
class CustomPartitioner extends Partitioner{
//分区数
override def numPartitions: Int = 3
//根据key返回数据所在的分区索引(从0开始)
override def getPartition(key: Any): Int = {
key match {
case "jeffrey" => 0
case "qiutee" => 1
case _ => 2
}
}
}
}
- 文件读取与保存
package com.itjeffrey.spark.core.rdd
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* 文件读取与保存
*
* @From: Jeffrey
* @Date: 2022/11/9
*/
object RddIO {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("RddTest_IO")
val sc = new SparkContext(conf)
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3)))
//保存
rdd.saveAsTextFile("output1")
rdd.saveAsObjectFile("output2")
rdd.saveAsSequenceFile("output3")
//读取
println(sc.textFile("output1").collect().mkString(","))
println(sc.objectFile[(String, Int)]("output2").collect().mkString(","))
println(sc.sequenceFile[String, Int]("output3").collect().mkString(","))
sc.stop()
}
}
累加器
工作原理:用来将Executor端变量信息聚合到Driver端。在Driver程序中定义的变量,在Executor端的每个Task都会得到这个变量的一个新的副本,每个task更新这些副本的值后再回传给Driver端进行merge操作。
代码实例:
val conf: SparkConf = new SparkConf().setMaster("local").setAppName("AccTest")
val sc = new SparkContext(conf)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
//Spark自带累加器
val sumAcc: LongAccumulator = sc.longAccumulator("sum")
// sc.doubleAccumulator("sum")
// sc.collectionAccumulator("sum")
rdd.foreach(num => {
sumAcc.add(num)
})
println(sumAcc.value)
sc.stop()
一般情况下,累加器放到行动算子中运行即可,其他不正确操作可能会导致少加或多加问题
- 少加问题
在转换算子中使用累加器可能会导致累加器少加现象 - 多加问题
在应用中使用了多次行动算子,导致累加器多次执行进而出现累加器多加的现象
自定义累加器代码示例:
object AccTest{
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local").setAppName("AccTest")
val sc = new SparkContext(conf)
val rdd: RDD[String] = sc.makeRDD(List("a", "b", "a"))
val acc = new CustomAccumulator()
sc.register(acc, "acc")
rdd.foreach(word => {
acc.add(word)
})
println(acc.value)
sc.stop()
}
class CustomAccumulator extends AccumulatorV2[String, mutable.Map[String, Long]]{
private var scMap = mutable.Map[String, Long]()
//判断是否初始状态
override def isZero: Boolean = {
scMap.isEmpty
}
override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = {
new CustomAccumulator()
}
//重置
override def reset(): Unit = {
scMap.clear()
}
//获取累加器需要计算的值
override def add(v: String): Unit = {
val newCount = scMap.getOrElse(v, 0L) + 1
scMap.update(v, newCount)
}
//Driver合并多个累加器
override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]): Unit = {
val map1: mutable.Map[String, Long] = this.scMap
val map2: mutable.Map[String, Long] = other.value
map2.foreach{
case (word, count) => {
val newCount: Long = map1.getOrElse(word, 0L) + count
map1.update(word, newCount)
}
}
}
override def value: mutable.Map[String, Long] = {
scMap
}
}
}
广播变量
所谓闭包数据,都是以Task任务为单位从Driver发送到Executor端,每个任务中都会包含闭包数据,这样在一个Executor中可能会含有大量重复的闭包数据从而占用大量内存。
一个Executor其实就是一个JVM,启动时会自动分配内存,完全可以将任务的闭包数据放置在Executor内存中达到共享目的。
Spark广播变量是分布式共享只读变量,是不允许被修改的(避免线程安全问题),广播变量可以将闭包数据保存到Executor内存中。
代码示例:
package com.itjeffrey.spark.core.bc
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable
/**
* 广播变量
*
* @From: Jeffrey
* @Date: 2022/11/10
*/
object BcTest {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local").setAppName("BcTest")
val sc = new SparkContext(conf)
val rdd1: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3)))
val rdd2: RDD[(String, Int)] = sc.makeRDD(List(("a", 4), ("b", 5), ("c", 6)))
//join方式 可能会导致笛卡尔积(结果数量成倍增加)造成内存溢出,并且会影响Shuffle性能,一般不建议使用
// rdd1.join(rdd2).collect().foreach(println)
//map方式,没有Shuffle操作,但是当map数据量较大时会造成Executor端存在大量重复的变量数据,不建议使用
// val map = mutable.Map(("a", 4), ("b", 5), ("c", 6))
// rdd1.map{
// case (w, c) => {
// val l: Int = map.getOrElse(w, 0)
// (w, (c, l))
// }
// }.collect().foreach(println)
//broadcast方式
val map1: mutable.Map[String, Int] = mutable.Map(("a", 4), ("b", 5), ("c", 6))
val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map1)
rdd1.map{
case (s,c) => {
val i: Int = bc.value.getOrElse(s, 0)
(s, (c, i))
}
}.collect().foreach(println)
sc.stop()
}
}
基本使用示例
本次示例基于spark local本地模式,spark解压目录下有测试案例jar可供使用
-
本地模式提交jar至spark
spark-submit --class org.apache.spark.examples.SparkPi --master local[2] .\examples\jars\spark-examples_2.12-3.0.0.jar 10
-
独立部署模式提交jar至spark(前提需要搭建spark standalone环境)
spark-submit --class org.apache.spark.examples.SparkPi --master spark://linux1:7077 .\examples\jars\spark-examples_2.12-3.0.0.jar 10 -
Yarn模式提交jar至spark(前提需要搭建spark yarn环境)
spark-submit --class org.apache.spark.examples.SparkPi --master yarn --deploy-mode cluster/client .\examples\jars\spark-examples_2.12-3.0.0.jar 10
yarn部署模式:cluster,client, 主要区别在于Driver运行的位置,集群里面运行的就是cluster模式,集群外运行的就是client模式 -
idea程序示例
准备:新建Scala项目,依赖Scala SDK,创建测试对象,在项目根目录下建个文件夹datas, 文件夹下建两个文件test1.txt, text2.txt
text1.txt, text2.txt文件内容均为如下所示:
Hello Spark
Hello Scala
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local").setAppName("WorkCount")
val context = new SparkContext(conf)
val lines: RDD[String] = context.textFile("datas")
val words: RDD[String] = lines.flatMap(_.split(" "))
val wordToOne: RDD[(String, Int)] = words.map(word => (word, 1))
//Spark框架提供分组和聚合的方法
//reduceByKey: 相同key的数据对value进行聚合
// val wordToCount: RDD[(String, Int)] = wordToOne.reduceByKey((x, y) => {x + y})
// val wordToCount: RDD[(String, Int)] = wordToOne.reduceByKey((x, y) => x + y)
val wordToCount: RDD[(String, Int)] = wordToOne.reduceByKey(_ + _)
val result: Array[(String, Int)] = wordToCount.collect()
result.foreach(println)
context.stop()
}
//测试结果:
//(Hello,4)
//(Scala,2)
//(Spark,2)