Apache Spark 是专为大规模数据处理而设计的快速通用的计算引擎
。 Spark 是加州大学伯克利分校的AMP实验室所开源的类 Hadoop MapReduce 的通用并行计算框架
, Spark 拥有 Hadoop MapReduce 所具有的优点;但不同于 MapReduce 的是 Job 中间输出结果可以缓存在内存中
,从而不再需要读写 HDFS
,减少磁盘数据交互,因此 Spark 能更好地适用于数据挖掘与机器学习等需要迭代的算法。
Spark 是 Scala 编写,方便快速编程。
Spark 提供了 Sparkcore RDD 、 Spark SQL(结构化数据) 、 Spark Streaming (流式计算框架)、 Spark MLlib(机器学习) 、 Spark GraphX(图计算引擎)等技术组件
,可以一站式地完成大数据领域的离线批处理、交互式查询、流式计算、机器学习、图计算等常见的任务。这就是 spark 一站式开发的特点。
都是分布式计算框架, Spark 计算中间结果基于内存缓存
, MapReduce 基于 HDFS 存储
。也正因此,Spark 处理数据的能力一般是 MR 的三到五倍以上, Spark 中除了基于内存计算这一个计算快的原因,还有 DAG(DAGShecdule) 有向无环图
来切分任务的执行先后顺序。
hadoop 和 spark 的相同点和不同点?
相同点:都属于计算框架,都可以进行并行计算,都是 MR 模型计算
不同点:
MR 的一个任务为一个 job,一个 job 里分为 map task 阶段和 reduce task 阶段;
spark 中的提交的一个任务成为一个 application,一个 application 中存在多个
job,没出发一个 action 算子就会产生一个 job,一个 job 可以被分为多个 stage,一
个 stage 存在多个 task 任务
spark 使用 DAG 来组织逻辑,实现逻辑计划,DAG 由一系列 RDD 通过血缘关系组成,
通过 job,stage 的划分,来实现计算机过程的优化。
spark application 在启动前会进行粗粒度的资源申请,即将本次 application 所需的所
有资源全部申请;MR 是 job 基于进程,每次执行一个 job 都会开启一个进程并申请
资源,在迭代计算时,MR 进程的切换以及资源的回收会大大降低 MR 计算性能,而
spark 没有这个问题
spark 下的 Task 执行有优化:推测优化,数据本地化。这点 MR 也有但 spark 更先进
spark 基于内存,迭代计算的中间结果不会落盘可以直接在内存中传递,只有 shuffle 时
或者中间结果内存放不下才会落盘。而 MR 的中间结果一定会落盘。读写的消耗和序
列化与反序列化在上百次的迭代计算就会很明显的体现出来
spark 的持久化机制,可以把数据持久化内存或磁盘中,减少重复计算的次数,可以容
错。MR 也有缓存,但目的是为了提高磁盘 IO 效率
spark 提供了丰富的算子,其底层以及已经被优化的很好了,可以大大减少开发成本,不
像 MR 需要自己写代码逻辑
多编程语言支持:Scala,Java,Python,R,SQL。
- Local
- 多用于本地测试,如在 eclipse , idea 中写程序测试等。
- Standalone
- Standalone 是 Spark 自带的一个资源调度框架,它支持完全分布式。
- Yarn
- Hadoop 生态圈里面的一个资源调度框架, Spark 也是可以基于 Yarn 来计算的。
- Mesos
- 资源调度框架
若要基于 Yarn 来进行资源调度,必须实现
AppalicationMaster
接口, Spark 实现了这个接口,所以可以基于 Yarn 来进行资源调度。
分布式的数据集
,由于数据量很大,因此要它切分并存储在各个节点的分区当中。Spark包含两种数据分区方式:HashPartitioner
(哈希分区)和RangePartitioner
(范围分区)。
在Spark Shuffle阶段中,共分为Shuffle Write阶段和Shuffle Read阶段,其中在Shuffle Write阶段中,Shuffle Map Task对数据进行处理产生中间数据,然后再根据数据分区方式对中间数据进行分区。最终Shffle Read阶段中的Shuffle Read Task会拉取Shuffle Write阶段中产生的并已经分好区的中间数据。
Hash分区
RangePartitioner
hdfs中的block是分布式存储的最小单元,类似于盛放文件的盒子,一个文件可能要占多个盒子,但一个盒子里的内容只可能来自同一份文件。假设block设置为128M,你的文件是260M,那么这份文件占3个block(128+128+4)。这样的设计虽然会有一部分磁盘空间的浪费,但是整齐的
block大小,便于快速找到、读取对应的内容。(p.s. 考虑到hdfs冗余设计,默认三份拷贝,实际上3*3=9个block的物理空间。)
spark中的partition 是弹性分布式数据集RDD的最小单元,RDD是由分布在各个节点上的partition组成的。partition 是指的spark在计算过程中,生成的数据在计算空间内最小单元,同一份数据(RDD)的partition 大小不一,数量不定,是根据application里的算子和最初读入的数据分块数量决定的
block位于存储空间、partition 位于计算空间,
block的大小是固定的、partition 大小是不固定的,
block是有冗余的、不会轻易丢失,partition(RDD)没有冗余设计、丢失之后重新计算得到
Spark从HDFS读入文件的分区数默认等于HDFS文件的块数(blocks),HDFS中的block是分布式存储的最小单元。如果我们上传一个30GB的非压缩的文件到HDFS,HDFS默认的块容量大小128MB,因此该文件在HDFS上会被分为235块(30GB/128MB);Spark读取SparkContext.textFile()读取该文
件,默认分区数等于块数即235。
RDD(Resilient Distributed Dataset)
弹性分布式数据集。
A list of partitions
- RDD 是由一系列的 partition 组成的。
A function for computing each split
- 函数是作用在每一个 partition/split 上。
A list of dependencies on other RDDs
- RDD 需要依赖其他RDD。
Optionally, a Partitioner for key-value RDDs
- 分区器是作用在 (K,V) 格式的 RDD 上。
Optionally, a list of preferred locations to compute each split on
- RDD默认会寻找最优的计算位置
计算向数据靠拢
尽可能少的进行数据拉取操作
注意:
计算移动数据不移动
”的理念。RDD 的最重要的特性之一就是血缘关系
(Lineage ),它描述了一个 RDD 是如何从父 RDD 计算得来的。如果某个 RDD 丢失了,则可以根据血缘关系,从父 RDD 计算得来。
Master ( standalone 模式):
资源管理的主节点(进程)。Cluster Manager :
在集群上获取资源的外部服务(例如: standalone ; yarn ; mesos )。Worker ( standalone 模式):
资源管理的从节点(进程)或者说是管理本机资源的进程。Application :
基于 Spark 的用户程序,包含 driver 程序和运行在集群上的 executor 程序,即一个完整的 spark 应用 。Dirver ( program ):
用来连接工作进程( worker )的程序 。Executor :
是在一个 worker 进程所管理的节点上为某 Application 启动的一个个进程,这个进程负责运行任务,并且负责将数据存在内存或者磁盘上,每个应用之间都有各自独立的executors 。Task :
被发送到 executor 上的工作单元。Job :
包含很多任务( Task )的并行计算,和 action 算子对应Stage :
一个 job 会被拆分成很多组任务,每组任务被称为 Stage (就像 MapReduce 分为MapTask 和 ReduceTask 一样)。Spark 记录了 RDD 之间的生成和依赖关系。但是只有当 F 进行行动操作时,Spark 才会根据 RDD的依赖关系生成 DAG,并从起点开始真正的计算。
Transformations
类算子叫做转换算子(本质就是函数), Transformations 算子是延迟执行,也叫懒加载执行。Action
类算子叫做行动算子, Action 类算子是触发执行。checkpoint
算子不仅能将 RDD 持久化到磁盘,还能切断 RDD 之间的依赖关系。内存
中。 cache 是懒执行
。可以指定持久化的级别。懒执行
。最常用的是 MEMORY_ONLY 和 MEMORY_AND_DISK 。
checkpoint 将 RDD 持久化到磁盘
,还可以切断 RDD 之间的依赖关系,也是懒执行
。
执行原理:
使用 checkpoint 时常用优化手段:
对 RDD 执行 checkpoint 之前,最好对这个 RDD 先执行 cache
这样新启动的 job 只需要将内存中的数据拷贝到Checkpint目录中就可以,省去了重新计算
这一步。
def main(args: Array[String]): Unit = {
val sparkConf = new
SparkConf().setMaster("local").setAppName("SparkCheckPoint" +
System.currentTimeMillis())
val sparkContext = new SparkContext(sparkConf)
sparkContext.setCheckpointDir("./checkpoint")
val lines: RDD[String] =
sparkContext.textFile("src/main/resources/NASA_access_log_Aug95")
val words: RDD[String] = lines.flatMap(_.split(" "))
println("words" + words.getNumPartitions)
words.checkpoint
words.count
sparkContext.stop
}
Standalone集群只有一个Master,如果Master挂了就无法提交应用程序,需要给Master进行高可用配置
Master的高可用可以使用fileSystem(文件系统)和zookeeper(分布式协调服务)
启动集群
主节点:
备用节点
注意点:
主备切换过程中不能提交 Application 。
主备切换过程中不影响已经在集群中运行的 Application 。因为 Spark 是粗粒度资源调度。
UI环境基于Single或者HA环境
借助于Hadoop的Yarn进行集群的资源管理,启动集群前配置和Standalone相同
spark-submit --master spark://node01:7077 --deploy-mode client --class
org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/sparkexamples_2.12-2.4.6.jar 10
总结
spark-submit --master spark://node01:7077 --deploy-mode cluster --class
org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/spark-examples_2.12-2.4.6.jar 10
执行流程:
cluster 模式提交应用程序后,会向 Master 请求启动 Driver 。
Master 接受请求,随机在集群一台节点启动 Driver 进程。
Driver 启动后为当前的应用程序申请资源。
Driver 端发送 task 到 worker 节点上执行(任务的分发)。
worker 上的 executor 进程将执行情况和执行结果返回给 Driver 端(任务结果的回收)。
总结
提交命令
spark-submit --master yarn --class org.apache.spark.examples.SparkPi
$SPARK_HOME/examples/jars/spark-examples_2.12-2.4.6.jar 10
spark-submit --master yarn–client --class
org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/spark-examples_2.12-2.4.6.jar 10
spark-submit --master yarn --deploy-mode client --class
org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/spark-examples_2.12-2.4.6.jar 10
执行流程:
客户端提交一个 Application ,并在客户端启动一个 Driver 进程。
应用程序启动后会向 RS ( ResourceManager )(相当于 standalone 模式下的 master 进程)发送请求,启动 AM ( ApplicationMaster )。
RS 收到请求,随机选择一台 NM ( NodeManager )启动 AM 。这里的 NM 相当于 Standalone 中的 Worker 进程。
AM 启动后,会向 RS 请求一批 container 资源,用于启动 Executor 。
RS 会找到一批 NM (包含 container )返回给 AM ,用于启动 Executor 。
AM 会向 NM 发送命令启动 Executor 。
Executor 启动后,会反向注册给 Driver , Driver 发送 task 到 Executor ,并将执行情况和结果返回给 Driver 端。
总结
适用于测试
,因为 Driver 运行在本地, Driver 会与 yarn 集群中的 Executor 进行大量的通信提交命令
spark-submit --master yarn --deploy-mode cluster --class
org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/spark-examples_2.12-2.4.6.jar 10
spark-submit --master yarn-cluster --class
org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/spark-examples_2.12-2.4.6.jar 1
总结
生产环境
中,因为 Driver 运行在 Yarn 集群中某一台 nodeManager中,每次提交任务的 Driver 所在的机器都是不再是提交任务的客户端机器,而是多个 NM 节点中的一台,不会产生某一台机器网卡流量激增的现象,但同样也有**缺点
**,任务提交后不能看到日志。只能通过 yarn 查看日志。leftOuterJoin
左连接rightOuterJoin
右链接fullOuterJoin
全连接K,V格式的RDD上
。根据key值进行连接, (K,V)join(K,W)返回(K,(V,W))分区数的总和
RDD 之间有一系列的依赖关系,依赖关系又分为
窄依赖
和宽依赖
。
父 RDD 和子 RDD 的 partition 之间的关系是
一对一
的。或者父 RDD 和子 RDD 的 partition关系是多对一的
。不会有 shuffle 的产生。目的地是唯一的
父 RDD 与子 RDD 的 partition 之间的关系是
一对多
。会有shuffle 的产生
。目的地有多个
Stage
如果父RDD与子RDD不需要进行Shuffle(窄依赖)我们可以将他们连接到一起。减少数据的传输
pipeline
因为将窄依赖的RDD连接到一起,当前RDD链和其他RDD链数据是不相关的,子RDD链不必等父RDD全部执行完毕后才开始执行,只需要等当前链的上—个task计算出结果当前Task就可以执行
依赖关系
,形成一个 DAG 有向无环图
, DAG 会提交给DAG Scheduler , DAGScheduler 会把 DAG 划分成相互依赖
的多个 stage ,划分 stage 的依据就是 RDD 之间的宽窄依赖。遇到宽依赖就划分 stage ,每个 stage 包含一个或多个 task 任务。然后将这些 task 以 taskSet 的形式提交给 TaskScheduler 运行。并行
的 task 组成
- 切割规则:从后往前,遇到
宽依赖
就进行切分stage
- 1.从后向前推理,遇到宽依赖就断开,遇到窄依赖就把当前的RDD加入到Stage中;
- 2.每个Stage里面的Task的数量是由该Stage中最后一个RDD的Partition数量决定的;
- 3.最后一个Stage里面的任务的类型是ResultTask,前面所有其他Stage里面的任务类型都是ShuffleMapTask;
- 4.代表当前Stage的算子一定是该Stage的最后一个计算步骤;
- 总结:由于spark中stage的划分是根据shuffle来划分的,而宽依赖必然有shuffle过程,因此可以说spark是根据宽窄依赖来划分stage的
pipeline 管道计算模式
, pipeline 只是一种计算思想、模式。分区num参数
就是分区 partition
的数量什么是shuffle?
有些运算需要将各节点上的同一类数据汇集到某一节点进行计算,把这些分布在不同节点的数据按照一定的规则汇集到一起的过程称为 Shuffle
reduceByKey会将上一个RDD中的每一个key对应的所有value聚合成一个value,然后生成一个新的RDD,元素类型是
问题:
如何聚合?
Shuffle Write
:上一个stage的每个map task就必须保证将自己处理的当前分区,数据相同的key写入一个分区文件中,可能会写入多个不同的分区文件中。
Shuffle Read
:reduce task就会从上一个stage的所有task所在的机器上寻找属于自己的那些分区文件,这样就可以保证每一个key所对应的value都会汇聚到同一个节点上去处理和聚合。
Spark中有两种Shuffle类型,HashShuffle和SortShuffle
buffer
起到数据缓存的作用。
- 产生的磁盘小文件过多,会导致以下问题:
- 在Shuffle Write过程中会产生很多写磁盘小文件的对象。
- 在Shuffle Read过程中会产生很多读取磁盘小文件的对象。
- 在JVM堆内存中对象过多会造成频繁的gc(垃圾回收机制),gc还无法解决运行所需要的内存的话,就会OOM。
- 在数据传输过程中会有频繁的网络通信,频繁的网络通信出现通信故障的可能性大大增加,一
旦网络通信出现了故障会导致shuffle file cannot find 由于这个错误导致的task失败,TaskScheduler不负责重试,由DAGScheduler负责重试Stage
复用buffer
,开启合并机制的配置是spark.shuffle.consolidateFiles。该参数默认值为false,将其设置为true即可开启优化机制。5M
。2*M(map task的个数)
2*M(map task的个数)
。推测执行机制
。在 Spark 中推测执行默认是关闭的。推测执行可以通过 spark.speculation 属性来配置。
在 Application 执行之前,将所有的资源申请完毕,当资源申请成功后,才会进行任务的调度,当所有的 task 执行完成后,才会释放这部分资源。
优点: 在 Application 执行之前,所有的资源都申请完毕,每一个 task 直接使用资源就可以了,不需要 task 在执行前自己去申请资源, task 启动就快了, task 执行快了, stage 执行就快了,job 就快了, application 执行就快了。 缺点: 直到最后一个 task 执行完成才会释放资源,集群的资源无法充分利用。
Application 执行之前不需要先去申请资源,而是直接执行,让 job 中的每一个 task 在执行前自己去申请资源, task 执行完成就释放资源。
优点:集群的资源可以充分利用。 缺点: task 自己去申请资源, task 启动变慢, Application 的运行就响应的变慢了。
KV格式/非KV格式
)变成一个 KV 格式的 RDD ,两个 RDD 的个数必须相同。用于多个节点对一个变量进行共享性的操作
。Accumulator 只提供了累加的功能,即确提供了多个 task 对一个变量并行操作的功能Shark
是SparkSQL
的前身,SparkSQL
产生的根本原因是其完全脱离了Hive
的限制。
SparkSQL
支持查询原生的RDD
。 RDD
是Spark
的核心概念,是Spark
能够高效的处理大数据的各种场景的基础。scala
/java
中写SQL
语句。支持简单的SQL
语法检查,能够在SQL
中写Hive
语句访问Hive
数据,并将结果取回作为RDD
使用。<dependency>
<groupId>org.apache.sparkgroupId>
<artifactId>spark-sql_2.12artifactId>
<version>2.4.6version>
dependency>
DataSet是分布式的数据集合
,DataSet提供了强类型支持,在RDD的每行数据加了类型约束
Dataset
的底层封装的是RDD
,当RDD
的泛型是Row
类型的时候,我们也可以称它为DataFrame
。
show
只显示前20条记录。show(numRows: Int)
显示 numRows 条show(truncate: Boolean)
是否最多只显示20个字符,默认为 true 。collect
方法会将 jdbcDF 中的所有数据都获取到,并返回一个 Array 对象。collectAsList:
获取所有数据到Listdescribe(cols: String*):
获取指定字段的统计信息first, head, take, takeAsList
:获取若干行记
where(conditionExpr: String) :
SQL语言中where关键字后的条件
and
和 or
。得到DataFrame类型的返回结果filter :
根据字段进行筛选
select :
获取指定字段值
selectExpr :
可以对指定字段进行特殊处理
col :
获取指定字段
apply :
获取指定字段
drop :
去除指定字段,保留其他字段
limit
方法获取指定DataFrame的前n行记录,得到一个新的DataFrame对象。
orderBy
和 sort
:按指定字段排序,默认为升序
sortWithinPartitions
groupBy :
根据字段进行 group by 操作
cube
和 rollup :
group by的扩展
GroupedData对象
max(colNames: String)
方法,获取分组中指定字段或者所有的数字类型字段的最大值,只能作用于数字型字段
min(colNames: String)
方法,获取分组中指定字段或者所有的数字类型字段的最小值,只能作用于数字型字段
mean(colNames: String)
方法,获取分组中指定字段或者所有的数字类型字段的平均值,只能作用于数字型字段
sum(colNames: String)
方法,获取分组中指定字段或者所有的数字类型字段的和值,只能作用于数字型字段
count()
方法,获取分组中的元素个数
distinct :
返回一个不包含重复记录的DataFrame
dropDuplicates :
根据指定字段去重
聚合操作调用的是 agg
方法,该方法有多种调用方式。一般与 groupBy 方法配合使用。
以下示例其中最简单直观的一种用法,对 id 字段求最大值,对 c4 字段求和。
jdbcDF.agg("id" -> "max", "c4" -> "sum")
unionAll
方法:对两个DataFrame进行组合 ,类似于 SQL 中的 UNION ALL 操作。save
可以将data数据保存到指定的区域
dataFrame.write.format("json").mode(SaveMode.Overwrite).save()
首先拿到sql
后解析一批未被解决的逻辑计划,再经过分析得到分析后的逻辑计划,再经过一批优化规则转换成一批最佳优化的逻辑计划,再经过SparkPlanner
的策略转化成一批物理计划,随后经过消费模型转换成一个个的Spark
任务执行。
Predicate Pushdown简称谓词下推,简而言之,就是在不影响结果的情况下,尽量将过滤条件提
前执行。谓词下推后,过滤条件在map端执行,减少了map端的输出,降低了数据在集群上传输的
量,节约了集群的资源,也提升了任务的性能。
df.write().mode(SaveMode.Overwrite)format("parquet").save("src/main/data/parquet");
df.write().mode(SaveMode.Overwrite).parquet("src/main/data/parquet");
SaveMode
指定文件保存时的模式。
Overwrite
:覆盖
Append
:追加
ErrorIfExists
:如果存在就报错
Ignore
:如果存在就忽略
object HelloSourceParquet {
def main(args: Array[String]): Unit = {
//创建SQL环境
val sparkSession =
SparkSession.builder().master("local").appName("HelloSourceParquet").get OrCreate()
import sparkSession.implicits._
var dataSet: Dataset[_] =sparkSession.read.format("parquet").load("src/main/data/parquet")
//打印数据
dataSet.show()
}
}
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.32version>
dependency>
//创建SQL环境
val sparkSession = SparkSession.builder().master("local").appName("Hello02DataFrameAvg").getOrCreate()
//读取数据库的参数
val map: mutable.Map[String, String] = new mutable.HashMap[String, String]()
map.put("url", "jdbc:mysql://192.168.88.101:3306/scott")
map.put("driver", "com.mysql.jdbc.Driver")
map.put("user", "root")
map.put("password", "123456")
map.put("dbtable", "emp")
//读取JDBC数据
val dataFrame: DataFrame = sparkSession.read.format("jdbc").options(map).load()
//打印数据
dataFrame.show()
// val dataFrame1 = sparkSession.sql("select empno,ename from emp where sal > 3000")
// dataFrame1.show()
println(dataFrame.schema)
val properties = new Properties()
properties.setProperty("driver", "com.mysql.jdbc.Driver")
properties.setProperty("user", "root")
properties.setProperty("password", "123456")
dataFrame.write.mode(SaveMode.Overwrite).jdbc("jdbc:mysql://192.168.88.101:3306/scott", "empcopy", properties)
}
UDF:
one to one,进来一个出去一个,row mapping。是row级别操作,如:upper、substr函数
UDAF:
many to one,进来多个出去一个,row mapping。是row级别操作,如sum/min。UDTF:
one to mang,进来一个出去多行
。如lateral view 与 explode,T:table-generatingrow_number()
开窗函数是按照某个字段分组,然后取另一字段的前几个的值,相当于分组取topN。
--开窗函数格式:
row_number() over (partitin by XXX order by XXX)
流式处理框架
,是Spark API(RDD)的扩展,支持可扩展、高吞吐量、容错的准实时数据流处理工作原理:
如果job执行的时间大于batchInterval会有什么样的问题?
pom.xml
<dependency>
<groupId>org.apache.sparkgroupId>
<artifactId>spark-streaming_2.12artifactId>
<version>2.4.6version>
dependency>
<dependency>
<groupId>org.apache.sparkgroupId>
<artifactId>spark-streaming-kafka-0-10_2.12artifactId>
<version>2.4.6version>
dependency>
启动socket server 服务器(指定9999端口号):
元数据检查点
数据检查点
streamingContext.socketTextStream()
方法,可以通过 TCP 套接字连接,从文本数据中创建了一个 DStream。streamingContext.fileStream(dataDirectory)
方法可以从任何文件系统(如:HDFS、S3、NFS 等)的文件中读取数据receiver
的方法,新的基于direct stream
的方法。数据流入的速度远高于数据处理的速度,对流处理系统构成巨大的负载压力,如果不能正确处理,可能导致集群资源耗尽最终集群崩溃,因此有效的反压机制(backpressure)对保障流处理系统的稳定至关重要。
batch processing time > batch interval
的情况时,(其中batch processing time为实际计算一个批次花费时间,batch interval为Streaming应用设置的批处理间隔)spark.streaming.receiver.maxRate
来限制Receiver的数据接收速率,此举虽然可以通过限制接收速率,来适配当前的处理能力,防止内存溢出,但也会引入其它问题。RateController
,这个组件负责监听“OnBatchCompleted”事件,然后从中抽取processingDelay 及schedulingDelay信息. Estimator依据这些信息估算出最大处理速度(rate),最后由基于Receiver的Input Stream将rate通过ReceiverTracker与ReceiverSupervisorImpl转发给BlockGenerator(继承自RateLimiter).RateController
速率控制器
RateEstimator
速率估算器
,主要用来估算最大处理速率**RateLimiter
限流器
**,也可以叫做令牌,如果Executor中task每秒计算的速度大于该值则阻塞,如果小于该值则通过令牌桶机制
: 大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。当进行某操作时需要令牌时会从令牌桶中取出相应的令牌数,如果获取到则继续操作,否则阻塞。用完之后不用放回。
在spark安装包的conf下spark-env.sh文件
SPARK_WORKER_CORES
SPARK_WORKER_MEMORY
SPARK_WORKER_INSTANCES #每台机器启动worker数
--executor-cores
--executor-memory
--total-executor-cores
spark.executor.cores
spark.executor.memory
spark.max.cores
spark.shuffle.service.enabled true //启用External shuffle Service服务
spark.shuffle.service.port 7337 //Shuffle Service服务端口,必须和yarn-site中的一致
spark.dynamicAllocation.enabled true //开启动态资源分配
spark.dynamicAllocation.minExecutors 1 //每个Application最小分配的executor数
spark.dynamicAllocation.maxExecutors 30 //每个Application最大并发分配的executor数
spark.dynamicAllocation.schedulerBacklogTimeout 1s
spark.dynamicAllocation.sustainedSchedulerBacklogTimeout 5s
并行度的合理调整,可以降低资源浪费提高spark任务的运行效率。
task的数量应该设置为sparkCPU cores的2-3倍。
如果读取的数据在HDFS中,降低block大小,相当于提高了RDD中partition个数sc.textFile(xx,numPartitions)
sc.parallelize(xxx, numPartitions)
sc.makeRDD(xxx, numPartitions)
sc.parallelizePairs(xxx, numPartitions)
repartitions/coalesce
redecByKey/groupByKey/join —(xxx, numPartitions)
spark.default.parallelism 500
spark.sql.shuffle.partitions—200
自定义分区器
如果读取数据是在SparkStreaming中
Receiver: spark.streaming.blockInterval—200ms定义的是max
Direct:读取的topic的分区数
val rdd1 = sc.textFile(path1)
val rdd2 = sc.textFile(path1)
这就是创建了重复的RDD
有什么问题? 对于执行性能来说没有问题,但是呢,代码乱。
//如何选择一种最合适的持久化策略?
默认情况下,性能最高的当然是MEMORY_ONLY,但前提是你的内存必须足够足够大,可以绰绰有余地存放下整个RDD的所有数据。因为不进行序列化与反序列化操作,就避免了这部分的性能开销;对这个RDD的后续算子操作,都是基于纯内存中的数据的操作,不需要从磁盘文件中读取数据,性能也很高;而且不需要复制一份数据副本,并远程传送到其他节点上。但是这里必须要注意的是,在实际的生产环境中,恐怕能够直接用这种策略的场景还是有限的,如果RDD中数据比较多时(比如几十亿),直接用这种持久化级别,会导致JVM的OOM内存溢出异常。
如果使用MEMORY_ONLY级别时发生了内存溢出,那么建议尝试使用MEMORY_ONLY_SER级别。该级别会将RDD数据序列化后再保存在内存中,此时每个partition仅仅是一个字节数组而已,大大减少了对象数量,并降低了内存占用。这种级别比MEMORY_ONLY多出来的性能开销,主要就是序列化与反序列化的开销。但是后续算子可以基于纯内存进行操作,因此性能总体还是比较高的。此外,可能发生的问题同上,如果RDD中的数据量过多的话,还是可能会导致OOM内存溢出的异常。
如果纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK策略。因为既然到了这一步,就说明RDD的数据量很大,内存无法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。
通常不建议使用DISK_ONLY和后缀为_2的级别:因为完全基于磁盘文件进行数据的读写,会导致性能急剧降低,有时还不如重新计算一次所有RDD。后缀为_2的级别,必须将所有数据都复制一份副本,并发送到其他节点上,数据复制以及网络传输会导致较大的性能开销,除非是要求作业的高可用性,否则不建议使用。
cache:
MEMORY_ONLY
persist:
MEMORY_ONLY
MEMORY_ONLY_SER
MEMORY_AND_DISK_SER
一般不要选择带有_2的持久化级别。
checkpoint:
① 如果一个RDD的计算时间比较长或者计算起来比较复杂,一般将这个RDD的计算结果保存到HDFS上,这样数据会更加安全。
② 如果一个RDD的依赖关系非常长,也会使用checkpoint,会切断依赖关系,提高容错的效率。
使用广播变量来模拟使用join,使用情况:一个RDD比较大,一个RDD比较小(条件)。
join算子=广播变量+filter、广播变量+map、广播变量+flatMap
即尽量使用有combiner的shuffle类算子。
combiner概念:
在map端,每一个map task计算完毕后进行的局部聚合。
combiner好处:
降低shuffle write写磁盘的数据量。
降低shuffle read拉取数据量的大小。
降低reduce端聚合的次数。
有combiner的shuffle类算子:
reduceByKey:这个算子在map端是有combiner的,在一些场景中可以使用reduceByKey代替groupByKey。
aggregateByKey
combinerByKey
使用reduceByKey替代groupByKey
使用mapPartition替代map
使用foreachPartition替代foreach
filter后使用coalesce减少分区数
使用repartitionAndSortWithinPartitions替代repartition与sort类操作
使用repartition和coalesce算子操作分区。
开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如100M以上的大集合),那么此时就应该使用Spark的广播(Broadcast)功能来提升性能,函数中使用到外部变量时,默认情况下,Spark会将该变量复制多个副本,通过网络传输到task中,此时每个task都有一个变量副本。如果变量本身比较大的话(比如100M,甚至1G),那么大量的变量副本在网络中传输的性能开销,以及在各个节点的Executor中占用过多内存导致的频繁GC,都会极大地影响性能。如果使用的外部变量比较大,建议使用Spark的广播功能,对该变量进行广播。广播后的变量,会保证每个Executor的内存中,只驻留一份变量副本,而Executor中的task执行时共享该Executor中的那份变量副本。这样的话,可以大大减少变量副本的数量,从而减少网络传输的性能开销,并减少对Executor内存的占用开销,降低GC的频率。
广播大变量发送方式:Executor一开始并没有广播变量,而是task运行需要用到广播变量,会找executor的blockManager要,bloackManager找Driver里面的blockManagerMaster要。
使用广播变量可以大大降低集群中变量的副本数。不使用广播变量,变量的副本数和task数一致。使用广播变量变量的副本和Executor数一致。
在Spark中,主要有三个地方涉及到了序列化
:
在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输。
将自定义的类型作为RDD的泛型类型时(比如JavaRDD,SXT是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现Serializable接口。
使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组。
Kryo序列化器介绍:
Spark支持使用Kryo序列化机制。Kryo序列化机制,比默认的Java序列化机制,速度要快,序列化后的数据要更小,大概是Java序列化机制的1/10。所以Kryo序列化优化以后,可以让网络传输的数据变少;在集群中耗费的内存资源大大减少。
对于这三种出现序列化的地方,我们都可以通过使用Kryo序列化类库,来优化序列化和反序列化的性能。Spark默认使用的是Java的序列化机制,也就是ObjectOutputStream/ObjectInputStream API来进行序列化和反序列化。但是Spark同时支持使用Kryo序列化库,Kryo序列化类库的性能比Java序列化类库的性能要高很多。官方介绍,Kryo序列化机制比Java序列化机制,性能高10倍左右。Spark之所以默认没有使用Kryo作为序列化类库,是因为Kryo要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说,这种方式比较麻烦。
Spark中使用Kryo:
Sparkconf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer").registerKryoClasses(new Class[]{SpeedSortKey.class})
使用注册器
public class KryoTest implements KryoRegistrator{
@Override
public void registerClasses(Kryo kryo) {
kryo.register(需要注册的类.class);
}
}
java中有三种类型比较消耗内存:
对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间。
字符串,每个字符串内部都有一个字符数组以及长度等额外信息。
集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Entry。
因此Spark官方建议,在Spark编码实现中,特别是对于算子函数中的代码,尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。
fasteutil介绍:
fastutil是扩展了Java标准集合框架(Map、List、Set;HashMap、ArrayList、HashSet)的类库,提供了特殊类型的map、set、list和queue;fastutil能够提供更小的内存占用,更快的存取速度;我们使用fastutil提供的集合类,来替代自己平时使用的JDK的原生的Map、List、Set,好处在于,fastutil集合类,可以减小内存的占用,并且在进行集合的遍历、根据索引(或者key)获取元素的值和设置元素的值的时候,提供更快的存取速度。fastutil的每一种集合类型,都实现了对应的Java中的标准接口(比如fastutil的map,实现了Java的Map接口),因此可以直接放入已有系统的任何代码中。
fastutil最新版本要求Java 7以及以上版本。
使用:
见RandomExtractCars.java类
task要计算的数据在本进程(Executor)的内存中。
① task所计算的数据在本节点所在的磁盘上。
② task所计算的数据在本节点其他Executor进程的内存中。
task所计算的数据在关系型数据库中,如mysql。
task所计算的数据在同机架的不同节点的磁盘或者Executor进程的内存中
跨机架。
Spark中任务调度时,TaskScheduler在分发之前需要依据数据的位置来分发,最好将task分发到数据所在的节点上,如果TaskScheduler分发的task在默认3s依然无法执行的话,TaskScheduler会重新发送这个task到相同的Executor中去执行,会重试5次,如果依然无法执行,那么TaskScheduler会降低一级数据本地化的级别再次发送task。
如上图中,会先尝试1,PROCESS_LOCAL数据本地化级别,如果重试5次每次等待3s,会默认这个Executor计算资源满了,那么会降低一级数据本地化级别到2,NODE_LOCAL,如果还是重试5次每次等待3s还是失败,那么还是会降低一级数据本地化级别到3,RACK_LOCAL。这样数据就会有网络传输,降低了执行效率。
可以增加每次发送task的等待时间(默认都是3s),将3s倍数调大, 结合WEBUI来调节:
• spark.locality.wait
• spark.locality.wait.process
• spark.locality.wait.node
• spark.locality.wait.rack
注意:等待时间不能调大很大,调整数据本地化的级别不要本末倒置,虽然每一个task的本地化级别是最高了,但整个Application的执行时间反而加长。
通过日志或者WEBUI
Spark JVM调优主要是降低gc时间,可以修改Executor内存的比例参数。
RDD缓存、task定义运行的算子函数,可能会创建很多对象,这样会占用大量的堆内存。堆内存满了之后会频繁的GC,如果GC还不能够满足内存的需要的话就会报OOM。比如一个task在运行的时候会创建N个对象,这些对象首先要放入到JVM年轻代中。比如在存数据的时候我们使用了foreach来将数据写入到内存,每条数据都会封装到一个对象中存入数据库中,那么有多少条数据就会在JVM中创建多少个对象。
Spark Executor堆内存中存放(以静态内存管理为例):RDD的缓存数据和广播变量(spark.storage.memoryFraction 0.6),shuffle聚合内存(spark.shuffle.memoryFraction 0.2),task的运行(0.2)那么如何调优呢?
提高Executor总体内存的大小
降低储存内存比例或者降低聚合内存比例
如何查看gc?
Spark WEBUI中job->stage->task
Spark底层shuffle的传输方式是使用netty传输,netty在进行网络传输的过程会申请堆外内存(netty是零拷贝),所以使用了堆外内存。默认情况下,这个堆外内存上限默认是每一个executor的内存大小的10%;真正处理大数据的时候,这里都会出现问题,导致spark作业反复崩溃,无法运行;此时就会去调节这个参数,到至少1G(1024M),甚至说2G、4G。
executor在进行shuffle write,优先从自己本地关联的mapOutPutTrackerWorker中获取某份数据如果本地mapOutPutTrackerWorker没有的话,那么会通过TransferService,去远程连接其他节点上executor的block manager去获取,如果本地block manager没有的话,那么会通过TransferService,去远程连接其他节点上executor的block manager去获取,尝试建立远程的网络连接,并且去拉取数据。频繁创建对象让JVM堆内存满溢,进行垃圾回收。正好碰到那个exeuctor的JVM在垃圾回收。处于垃圾回过程中,所有的工作线程全部停止;相当于只要一旦进行垃圾回收,spark / executor停止工作,无法提供响应,spark默认的网络连接的超时时长是60s;如果卡住60s都无法建立连接的话,那么这个task就失败了。task失败了就会出现shuffle file cannot find的错误。
那么如何调节等待的时长呢?
在./spark-submit提交任务的脚本里面添加:
--conf spark.core.connection.ack.wait.timeout=300
Executor由于内存不足或者堆外内存不足了,挂掉了,对应的Executor上面的block manager也挂掉了,找不到对应的shuffle map output文件,Reducer端不能够拉取数据。我们可以调节堆外内存的大小,如何调节?
在./spark-submit提交任务的脚本里面添加
yarn下:
--conf spark.yarn.executor.memoryOverhead=2048 #单位M
standalone下:
--conf spark.executor.memoryOverhead=2048 #单位M
方案适用场景:
如果导致数据倾斜的是Hive表。如果该Hive表中的数据本身很不均匀(比如某个key对应了100万数据,其他key才对应了10条数据),而且业务场景需要频繁使用Spark对Hive表执行某个分析操作,那么比较适合使用这种技术方案。
方案实现思路:
此时可以评估一下,是否可以通过Hive来进行数据预处理(即通过Hive ETL预先对数据按照key进行聚合,或者是预先和其他表进行join),然后在Spark作业中针对的数据源就不是原来的Hive表了,而是预处理后的Hive表。此时由于数据已经预先进行过聚合或join操作了,那么在Spark作业中也就不需要使用原先的shuffle类算子执行这类操作了。
方案实现原理:
这种方案从根源上解决了数据倾斜,因为彻底避免了在Spark中执行shuffle类算子,那么肯定就不会有数据倾斜的问题了。但是这里也要提醒一下大家,**这种方式属于治标不治本。**因为毕竟数据本身就存在分布不均匀的问题,所以Hive ETL中进行group by或者join等shuffle操作时,还是会出现数据倾斜,导致Hive ETL的速度很慢。我们只是把数据倾斜的发生提前到了Hive ETL中,避免Spark程序发生数据倾斜而已。
方案适用场景:
如果发现导致倾斜的key就少数几个,而且对计算本身的影响并不大的话,那么很适合使用这种方案。比如99%的key就对应10条数据,但是只有一个key对应了100万数据,从而导致了数据倾斜。
方案实现思路:
如果我们判断那少数几个数据量特别多的key,对作业的执行和计算结果不是特别重要的话,那么干脆就直接过滤掉那少数几个key。比如,在Spark SQL中可以使用where子句过滤掉这些key或者在Spark Core中对RDD执行filter算子过滤掉这些key。如果需要每次作业执行时,动态判定哪些key的数据量最多然后再进行过滤,那么可以使用sample算子对RDD进行采样,然后计算出每个key的数量,取数据量最多的key过滤掉即可。
方案实现原理:
将导致数据倾斜的key给过滤掉之后,这些key就不会参与计算了,自然不可能产生数据倾斜。
方案实现思路:
在对RDD执行shuffle算子时,给shuffle算子传入一个参数,比如reduceByKey(1000),该参数就设置了这个shuffle算子执行时shuffle read task的数量。对于Spark SQL中的shuffle类语句,比如group by、join等,需要设置一个参数,即spark.sql.shuffle.partitions,该参数代表了shuffle read task的并行度,该值默认是200,对于很多场景来说都有点过小。
方案实现原理:
增加shuffle read task的数量,可以让原本分配给一个task的多个key分配给多个task,从而让每个task处理比原来更少的数据。举例来说,如果原本有5个不同的key,每个key对应10条数据,这5个key都是分配给一个task的,那么这个task就要处理50条数据。而增加了shuffle read task以后,每个task就分配到一个key,即每个task就处理10条数据,那么自然每个task的执行时间都会变短了。
缺点:
不是万能的,没有从根本上改变数据的格式所以数据倾斜问题还是可能存在。
例如极端情况,有一个key对应数据就是100w
,别的key对应1w
,你再怎么增加task,相同的可以还是会分配到一个task中去计算。
不能根本解决问题,可以缓解一波。
方案适用场景:
对RDD执行reduceByKey、groupByKey等聚合类shuffle算子或者在Spark SQL中使用group by语句进行分组聚合时,比较适用这种方案。
方案实现思路:
这个方案的核心实现思路就是进行两阶段聚合
。第一次是局部聚合,先给每个key都打上一个随机数,Spark 优化 比如10以内的随机数,此时原先一样的key就变成不一样的了,比如(hello, 1) (hello, 1) (hello, 1) (hello, 1),就会变成(1_hello, 1) (1_hello, 1) (2_hello, 1) (2_hello, 1)。接着对打上随机数后的数据,执行reduceByKey等聚合操作,进行局部聚合,那么局部聚合结果,就会变成了(1_hello, 2) (2_hello, 2)。然后将各个key的前缀给去掉,就会变成(hello,2)(hello,2),再次进行全局聚合操作,就可以得到最终结果了,比如(hello, 4)。
方案实现原理:
将原本相同的key通过附加随机前缀的方式,变成多个不同的key,就可以让原本被一个task处理的数据分散到多个task上去做局部聚合,进而解决单个task处理数据量过多的问题。接着去除掉随机前缀,再次进行全局聚合,就可以得到最终的结果。
如果一个RDD中有一个key导致数据倾斜,同时还有其他的key,那么一般先对数据集进行抽样,然后找出倾斜的key,再使用filter对原始的RDD进行分离为两个RDD,一个是由倾斜的key组成的RDD1,一个是由其他的key组成的RDD2,那么对于RDD1可以使用加随机前缀进行多分区多task计算,对于另一个RDD2正常聚合计算,最后将结果再合并起来。
只适用于聚合类的shuffle操作,join这类不太适合。
BroadCast+filter(或者map)
方案适用场景:
在对RDD使用join类操作,或者是在Spark SQL中使用join语句时,而且join操作中的一个RDD或表的数据量比较小(比如几百M或者一两G),比较适用此方案。
方案实现思路:
不使用join算子进行连接操作,而使用Broadcast变量与map类算子实现join操作,进而完全规避掉shuffle类的操作,彻底避免数据倾斜的发生和出现。将较小RDD中的数据直接通过collect算子拉取到Driver端的内存中来,然后对其创建一个Broadcast变量;接着对另外一个RDD执行map类算子,在算子函数内,从Broadcast变量中获取较小RDD的全量数据,与当前RDD的每一条数据按照连接key进行比对,如果连接key相同的话,那么就将两个RDD的数据用你需要的方式连接起来。
方案实现原理:
普通的join是会走shuffle过程的,而一旦shuffle,就相当于会将相同key的数据拉取到一个shuffle read task中再进行join,此时就是reduce join。但是如果一个RDD是比较小的,则可以采用广播小RDD全量数据+map算子来实现与join同样的效果,也就是map join,此时就不会发生shuffle操作,也就不会发生数据倾斜。
方案适用场景:
两个RDD/Hive表进行join的时候,如果数据量都比较大,无法采用“解决方案五”,那么此时可以看一下两个RDD/Hive表中的key分布情况。如果出现数据倾斜,是因为其中某一个RDD/Hive表中的少数几个key的数据量过大,而另一个RDD/Hive表中的所有key都分布比较均匀,那么采用这个解决方案是比较合适的。
方案实现思路:
对包含少数几个数据量过大的key的那个RDD,通过sample算子采样出一份样本来,然后统计一下每个key的数量,计算出来数据量最大的是哪几个key。然后将这几个key对应的数据从原来的RDD中拆分出来,形成一个单独的RDD,并给每个key都打上n以内的随机数作为前缀,而不会导致倾斜的大部分key形成另外一个RDD。接着将需要join的另一个RDD,也过滤出来那几个倾斜key对应的数据并形成一个单独的RDD,将每条数据膨胀成n条数据,这n条数据都按顺序附加一个0~n的前缀,不会导致倾斜的大部分key也形成另外一个RDD。再将附加了随机前缀的独立RDD与另一个膨胀n倍的独立RDD进行join,此时就可以将原先相同的key打散成n份,分散到多个task中去进行join了。而另外两个普通的RDD就照常join即可。最后将两次join的结果使用union算子合并起来即可,就是最终的join结果 。
方案适用场景:
如果在进行join操作时,RDD中有大量的key导致数据倾斜,那么进行分拆key也没什么意义,此时就只能使用最后一种方案来解决问题了。
方案实现思路:
该方案的实现思路基本和“解决方案六”类似,首先查看RDD/Hive表中的数据分布情况,找到那个造成数据倾斜的RDD/Hive表,比如有多个key都对应了超过1万条数据。然后将该RDD的每条数据都打上一个n以内的随机前缀。同时对另外一个正常的RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都依次打上一个0~n的前缀。最后将两个处理后的RDD进行join即可。
rdd中数据倾斜,sample抽样取出样本(key),统计key对应的数量,以及最大的数据量的key。
把这些key从rdd中抽取出去,形成单独的rdd,给key打上随机数作为前缀,另一部分没有抽出去的数据在形成另外一个rdd。
另一个rdd(join),也将上次抽取出来的key过滤出一个单独的rdd,但是需要做扩容(数据量的膨胀)。在这些数据上追加上随机数,和之前的rdd加随机数的规则一致,没有抽离的数据也还是形成一个新的rdd。
两两join,有随机数的记得去掉前缀,然后最终结果union得出。
提高建立连接的超时时间,或者降低gc,降低gc了那么spark不能堆外提供服务的时间就少了,那么超时的可能就会降低。
提高拉取数据的重试次数以及间隔时间。
提高堆外内存大小,提高堆内内存大小。
BlockManager拉取的数据量大,reduce task处理的数据量小
解决方法:
降低每次拉取的数据量
提高shuffle聚合的内存比例
提高Executor的内存比例
val rdd = rdd.map{x=>{
x+”~”; -1
}}
filter(-1).coalesce
rdd.foreach{x=>{
System.out.println(x.getName())
}}