spark 核心笔记记录
文章目录
一、spark简介
1.spark是什么:
Apache Spark is a fast and general-purpose cluster computing system. It provides high-level APIs in Java, Scala, Python and R, and an optimized engine that supports general execution graphs. It also supports a rich set of higher-level tools including Spark SQL for SQL and structured data processing, MLlib for machine learning, GraphX for graph processing, and Spark Streaming.
spark使用快速通用的集群计算系统,它提供了java,scala,python和R多种语言的api,以及支持通用执行图的优化引擎;同时也提供想spark SQL这样的工具来处理结构化数据, 使用MLlib进行机器学习,Graphx进行图像处理,Spark Streaming进行流处理
2.spark的运行模式:
a.local:这是一种适用于本地开发的运行模式
b.standalone:spark自带的一套资源调度框架,支持分布式搭建,spark可以基于standalone运行任务
c.yarn:hadoop yand的资源调度框架,国内运用广泛
d.mesos:国外被广泛使用的资源调度框架
3.spark与MR的区别
a.spark是基于内存迭代处理数据,MR基于磁盘迭代处理数据
b.Spark中有DAG有向无环图执行引擎,而spark比mr快的原因基本在spark是基于内存和dag计算模型的原因,可以说DAG相比MapReduce在大多数情况下可以减少shuffle的次数。spark中的DAGScheduler相当于一个改进版的MapReduce,如果在计算过程中不涉及节点间数据传输,spark可以直接基于内存运输,数据无需落地磁盘,这样速度会比MapReduce快很多,但是如果涉及多节点间数据传输,那么spark也会将数据落地磁盘。
c.spark是粗粒度申请资源,MR是细粒度申请资源
d.在mr中只有mapper和reducer,相当于spark中的map和reduceByKey两个算子,在MR业务逻辑需要自己实现,而spark中有各种算子对应各种作业
4.spark核心RDD
1.RDD是什么:(Resilient Distributed Dateset)弹性分布式数据集
2.RDD的五大特性
A list of partitions
A function for computing each partition
A list of dependencies on other RDDs
Optionally, a Partitioner for key-value RDDs
Optionally, a list of preferred locations to compute each split on
(1)由一些列Partitions组成
(2)算子(函数)作用于partition上的
(3)RDD之间存在上下级依赖
(4)分区器是作用在(K, V)格式的RDD上
(5)partition对外提供最佳计算位置,有利于实现计算跟着数据移动
3.RDD中不存储数据
4.基本问题:
(1)(K, V)格式的RDD就是在RDD中的原始是二元组
(2)哪里体现了RDD的分布式:RDD中的partition分布在不同的节点上
(3)哪里体现了RDD的弹性:partition的个数可调整,RDD间存在依赖关系
5.RDD的宽窄依赖
Spark中rdd存在父子依赖关系,在依赖关系中又分为宽依赖和窄依赖
窄依赖:窄依赖是指父RDD中与子RDD中的partition之间是一对一关系
宽依赖:窄依赖是指父RDD中与子RDD中的partition之间是一对多关系,当出现宽依赖时,会出现Shuffle现象
二、一个简单的SPARK程序
package com.mjlf.scala_study
import org.apache.spark.rdd.RDD
import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
object SparkWordCount {
def main(args: Array[String]): Unit = {
//定义SparkConf配置
val sparkConf = new SparkConf();
//设置运行模式和应用名称,必须
sparkConf.setMaster("local").setAppName("scalaWordCount")
//创建sparkContext上下文
val sc = new SparkContext(sparkConf)
//读取本地磁盘文件,并将其转为为RDD
val lines:RDD[String] = sc.textFile("D:\\project\\scala\\study_test\\src\\com\\mjlf\\scala_study\\words");
//使用flatMap 按" "切割每行
val words:RDD[String] = lines.flatMap(line => {
line.split(" ");
})
//将每个单词转换为为二元组,如 word -> (word, 1)
val pairWords:RDD[(String, Int)] = words.map(word => {
new Tuple2(word, 1)
})
//统计单词数量
val reduce:RDD[(String, Int)] = pairWords.reduceByKey((v1:Int, v2:Int) => {
v1 + v2
})
//执行action算子,计算上边的过程,在遇见action算子之前,transformation算子都不会执行,关于action和transformation算子后续讲解
reduce.foreach(x => {
println(x)
})
//关闭sparkContext上下文
sc.stop()
}
}
三、spark算子
1.Transfromation算子
transfromation算子在spark中是一些列的懒执行算子,也就是这些算子在遇到action算之前都不会执行
filter:过滤算子, filter(f: X => Boolean)),表示传入一个参数,要求返回true|false,true表示留下该元素,false表示过滤该元素:
val rdd = sc.makeRDD(List(1, 3, 3, 3, 2, 2, 1, 5), 3)
rdd.filter( x => {x % 2 == 0}).collect().foreach(println(_))
结果:
1 3 3 3 1 5
map:map(f: x => U),针对rdd中的每个元素进行计算,最后每个元素计算后返回一个值
//对rdd中每个元素求平方
val rdd = sc.makeRDD(List(1, 3, 3, 3, 2, 2, 1, 5), 3)
rdd.map(x => (x*x)).collect().foreach( x => {print(x + " ")})
结果:1 9 9 9 4 4 1 25
mapToPair:在scala中没有该算子,类似于map算子
flatMap:可以将多个元素扁平化,如"hello appache spark"经过float(x=>{x.split(" ")})算子后变成rdd(hello, apache, spark)
val rdd = sc.makeRDD(List("hello appache spark"))
rdd.flatMap(x => (x.split(" "))).collect().foreach( x => {print(x + " ")})
结果:hello appache spark
reduceByKey:一个聚合算子,将相同的rdd中相同key每个元素进行聚合,
val rdd = sc.makeRDD(List(("appach", 1), ("scala", 1), ("appach", 1), ("java", 1),("appach", 1)))
rdd.reduceByKey(_+_).collect().foreach( x => {print(x + " ")})
结果:(scala,1) (appach,3) (java,1)
sortBy/SortByKey:排序算子
//根据rdd中key进行排序
val rdd = sc.makeRDD(List(("appach", 1), ("scala", 1), ("appach", 1), ("java", 1),("appach", 1)))
rdd.reduceByKey(_+_).collect().sortBy(x => {//根据rdd中key进行排序
x._1
}).foreach( x => {print(x + " ")})
结果:(appach,3) (java,1) (scala,1)
sample:随机算子
参数:
1、withReplacement:元素可以多次抽样(在抽样时替换)
2、fraction:期望样本的大小作为RDD大小的一部分,
当withReplacement=false时:选择每个元素的概率;分数一定是[0,1] ;
当withReplacement=true时:选择每个元素的期望次数; 分数必须大于等于0。
3、seed:随机数生成器的种子
需要知道最后返回的元素数量并不一定等于元素总数 * fraction,只是相近
taskSample:
def takeSample(
withReplacement: Boolean,
num: Int,
seed: Long = Utils.random.nextLong): Array[T] = withScope {...}
参数:
1、withReplacement:元素可以多次抽样(在抽样时替换)
2、num:返回的样本的大小
3、seed:随机数生成器的种子
该函数返回的元素个数固定
join:内链接
leftOuterJoin:左外连接
rightOuterJoin:右外连接
fullOuterJoin:全外连接
union:并集
intersection:两个rdd的交集
subtract:一个rdd减去另一个rdd
distract:对rdd进行去重
cogroup:合并两个rdd中key,将第一个rdd中的相同key对应的value放到一个集合中,另一个rdd中对应key的value放到另一个集合中, 最后将key, 第一个集合,第二个集合组成三元祖返回
如:
val rdd1 = sc.parallelize(Array(("aa",1),("bb",2),("cc",6)))
val rdd2 = sc.parallelize(Array(("aa",3),("dd",4),("aa",5)))
val rdd3 = rdd1.cogroup(rdd2).collect()
结果:
(aa,(CompactBuffer(1),CompactBuffer(3, 5)))
(dd,(CompactBuffer(),CompactBuffer(4)))
(bb,(CompactBuffer(2),CompactBuffer()))
(cc,(CompactBuffer(6),CompactBuffer()))
mapPartitions:和map类似, 但是该算子将每个分区中的元素整体放到了一个迭代器中
repartion:重新分区
coalsesce:
该函数用于将RDD进行重分区,使用HashPartitioner。
第一个参数为重分区的数目,第二个为是否进行shuffle,默认为false;
mapPartitionWithIndex:与mapPartition不同的是, 他会将分区号也带入计算中
var rdd1 = sc.makeRDD(1 to 5,2)
//rdd1有两个分区
var rdd2 = rdd1.mapPartitionsWithIndex{
(x,iter) => {//x表示分区号
var result = List[String]()
var i = 0
while(iter.hasNext){
i += iter.next()
}
result.::(x + "|" + i).iterator
}
}
//rdd2将rdd1中每个分区的数字累加,并在每个分区的累加结果前面加了分区索引
scala> rdd2.collect
res13: Array[String] = Array(0|3, 1|12)
groupByKey:通过key进行分组
zip:zip函数用于将两个RDD组合成Key/Value形式的RDD,这里默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常。
zipWithIndex:该函数将RDD中的元素和这个元素在RDD中的ID(索引号)组合成键/值对。
scala> var rdd2 = sc.makeRDD(Seq("A","B","R","D","F"),2)
rdd2: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[34] at makeRDD at :21
scala> rdd2.zipWithIndex().collect
res27: Array[(String, Long)] = Array((A,0), (B,1), (R,2), (D,3), (F,4))
...
2.Action算子
foreach:
count:会将结果返回到Driver端
first: = take(1)
take:
foreachPartition:
reduce:
collect:会将结果返回到Driver端
countByKey:
countByValue:
3.持久化算子
cahce(): = persist(MEMORY_ONLY)
persist(StorageLevel):
持久化级别:
val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(true, true, true, false, 1)
4.spark中算子的创建方式
Scala:
sc.textFile(path, minNumPartition)
sc.parallelize(xxx, numPartititon)
sc.makeRDD(xxx, numPartition)
Java:
sc.textFile(path, minNumPartition)
sc.parallelize(xx,numpartition)
sc.parallelizePairs(list(tuple2),numpartition)
四、spark集群搭建
1.基于standalone方式
1.选择合适的spark版本下载:http://ftp.jaist.ac.jp/pub/apache/spark/spark-2.4.0/spark-2.4.0-bin-hadoop2.7.tgz
2.解压:tar -xvf spark-2.0.0-bin-hadoop2.7.tgz
3.修改配置:在conf修改配置文件
1.复制文件spark-env.sh.template 为 spark-env.sh (cp spark-env.sh.template spark-env.sh)
2. 添加配置信息:
#jdk位置
export JAVA_HOME=/usr/local/jdk
#主master地址
export SPARK_MASTER_IP=node1
#master 服务端口
export SPARK_MASTER_PORT=7077
#该节点分配的核心数
export SPARK_MASTER_CORES=1
#该节点分配的内存
export SPARK_MASTER_MEMORY=512M
3.在salve文件中添加从节点地址:
node2
node3
4.将spark配置分发到其他节点上:scp -r sparkxxx node2:`pwd`
scp -r sparkxxx node3:`pwd`
5.启动服务器:到sbin目录下执行./start-all.sh
starting org.apache.spark.deploy.master.Master, logging to /opt/spark2/spark1.6.0/logs/spark-root-org.apache.spark.deploy.master.Master-1-node1.out
node2: starting org.apache.spark.deploy.worker.Worker, logging to /opt/spark2/spark1.6.0/logs/spark-root-org.apache.spark.deploy.worker.Worker-1-node2.out
node3: starting org.apache.spark.deploy.worker.Worker, logging to /opt/spark2/spark1.6.0/logs/spark-root-org.apache.spark.deploy.worker.Worker-1-node3.out
node2: failed to launch org.apache.spark.deploy.worker.Worker:
node2: full log in /opt/spark2/spark1.6.0/logs/spark-root-org.apache.spark.deploy.worker.Worker-1-node2.out
node3: failed to launch org.apache.spark.deploy.worker.Worker:
node3: full log in /opt/spark2/spark1.6.0/logs/spark-root-org.apache.spark.deploy.worker.Worker-1-node3.out
测试:访问node1:8080
2.基于yarn模式下任务提交配置
在conf下spark-env.sh添加如下配置信息,配置完成后需求重启spark,但是基于yarn提交任务,需要依赖hadoop,所以在任务提交前需要先启动hadoop
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
3.集群执行日志保存到HDFS
需要依赖hdfs,启动前需要先配置并启动hadoop
1.修改配置文件:spark-default.conf,在其中添加如下信息:
spark.eventLog.enabled true
spark.eventLog.dir hdfs://node1:8020/log/
spark.history.fs.logDirectory hdfs://node1:8020/log/
//启动HistorServer
../sbin/start-history-server.sh
2.分发配置信息到其他节点:
scp ./* node2:`pwd`
scp ./* node3:`pwd`
4.spark 高可用
spark高可用方式有两者模式,一种是将相关信息保存到FileSystem.另一种这是将信息保存到zookeeper,由于第一种方式使用很多少,我们这里也只介绍使用zookeeper的配置方式
1.配置高可用, 需要在spark-env.sh中添加如下信息
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=node3:2181,node4:2181,node5:2181 -Dspark.deploy.zookeeper.dir=/MasterHA0315"
2.分配配置信息到其他节点:
scp ./* node2:`pwd`
scp ./* node3:`pwd`
3.选在当你的主节点挂掉后启动从节点作为主节点的节点, 在其节点修改spark-env.sh
将SPARK_MASTER_IP修改从节点ip
SPARK_MASTER-IP=node2
5.spark基本配置信息:
在spark中存在很多默认配置,可以到spark官网查询:https://spark.apache.org/docs/latest/configuration.html
SPARK_MASTER_WEBUI_PORT=9999配置spark web ui端口,默认8080
五. spark任务提交
1.standalone-client调度模式
1.集群启动后,worker会向master注册资源
2.我们向Client提交Application,client收到请求会创建一个Driver
3.Driver创建完成之后,会向Master申请运行Application运行是需要的资源
4.Master接收到Driver的资源申请请求后,会根据worker注册在自己哪儿的数据,判断哪些worker满足Driver的需求,随后会在对应的worker上创建Executor,这些Executor就是实际用来运行Application的任务
5.当worker上的executor创建完成后,driver会向这些executor发送task,同步task执行完毕后,他会回收这些task任务的数据
Application 提交命令:
./spark-submit --master spark://node1:7077 --class classPath jar 参数
./spark-submit --master spark://node1:7077 --deploy-mode client --class classPath jar 参数
2.standalone-cluster调度模式
1.集群启动时,worker向master汇报资源
2.向client提交application
3.client收到application请求后,向master申请资源用于启动Driver
4.master收到Driver启动申请后,会选择何是的worker,并在其上边启动Driver
5.Driver启动后,会向Master申请资源启动Exector,用于运行实际的task
6.Master收到请求后,会分析选择出合适的worker,并在其上创建Executor
7.Executor启动后,Driver向Executor发送task,同时接收task产生的数据
Applicationt提交命令
./spark-submit --master spark://node1:7077 --deploy-mode cluster --class classPath jar 参数
3.yarn-client调度模式
1.集群启动,Node Manager向Resource Manager注册资源
2.向Client提交Application申请
3.Client收到Application提交申请后,启动Driver, Driver再向ResourceManager提交申请用于启动Application Master
4.Resource Manager收到申请后,选择合适NodeManger启动Application Master
5.Applicaiton Manater启动成功后,再向ResourceManager申请资源,用于启动Executor
6.Resource Manager选择合适的NodeManager返回给Application Master
7.Application Master收到Resource Manager返回的NodeManger信息后,在对应的NodeManager上启动Executor
8.Executor启动成功后,向Driver注册信息
9.Driver收到注册信息后,向Executor发送task,同时接收task返回的数据
Application提交命令:
./spark-submit --master yarn --class ... jar 参数
./spark-submit --master yarn-client --class ..jar 参数
./spark-submit --master yarn --deploy- mode client --class ... jar 参数
4.yarn-cluster调度模式
1.集群启动,NodeManager注册资源到Resource Manger
2.提交Application到client
3.client向ResourceManger申请资源启动ApplicationMaster和Driver
4.Resource Manger申请后,选择合适的NodeManger启动ApplicationMaster 和Driver
5.Application Master启动成功后,向Resource Manager申请资源,用于启动Executor
6.Resource Manager收到资源申请后,分析选择合适的NodeManager节点返回给Application Master
7.Application Master收到Resource Manager返回的节点信息后,在对应的NodeManger上启动Executor
8.启动后的Executor向Driver 注册信息
9.Driver向Executor发送Task和接收Task返回的数据
Application提交命令
./spark-submit --master yarn-cluster --classs ..jar 参数
./spark-submit --master yarn --deploy-mode cluster --class ..jar 参数
六.Stage
stage的划分是Spark作业调度的关键一步,它基于DAG确定依赖关系,借此来划分stage,将依赖链断开,每个stage内部可以并行运行,整个作业按照stage顺序依次执行,最终完成整个Job。实际应用提交的Job中RDD依赖关系是十分复杂的,依据这些依赖关系来划分stage自然是十分困难的,Spark此时就利用了前文提到的依赖关系,调度器从DAG图末端出发,逆向遍历整个依赖关系链,遇到ShuffleDependency(宽依赖关系的一种叫法)就断开,遇到NarrowDependency就将其加入到当前stage。stage中task数目由stage末端的RDD分区个数来决定,RDD转换是基于分区的一种粗粒度计算,一个stage执行的结果就是这几个分区构成的RDD。
每个Stage都是由一组并行的task组成,
stage的并行度依赖于最后一个RDD的partition
如果需要提高stage的并行度,需要增加最后一个RDD的partition,可以使用重分区算子
task采用pipline的计算方式,及数据时一个一个的取出计算,而非批量
对于管道中数据,在RDD持久化和shuffle write时才会落地磁盘
七.spark资源调度与任务调度
1.资源调度
1.集群启动,worker向master汇报资源
2.master掌握集群资源情况
3.向Client提交Application,启动Driver,Driver生成TaskScheduler和DAGSchedular
4.TaskScheduler向Master申请资源启动Executor
5.Master收到请求后,选择合适的Worker启动Executor
6.Executor向TaskScheduler注册
2.任务调度
7.当遇见Action算子,触发任务提交,DAGScheduler根据job中RDD宽窄依赖划分Stage,最后将RDD划分为Task保存到TaskSet中
8.DAGScheduler向TaskScheduler发送TaskSet
9.TaskScheduler遍历TaskSet,取出Task向Executor发送task,同时接收Task执行返回的结果
3.调度细节
1.如果Task执行失败,TaskScheduler会隔指定时间重新执行,重试3次,如果3次都失败,那么DAGScheduler会重试stage,重试4从,如果都失败,那么job失败,整个Application失败
2.TashScheduler不仅可以重试失败的task,还可以重试执行缓慢的task,这是Spark中的推测执行机制,默认关闭的,对于ETL的业务场景要关闭,这样可能会导致数据重复
3.遇见Application执行不停止的情况,首先查看是否有数据倾斜,然查看是否开启了推测执行,spark.speculation false
4.粗粒度资源申请
当Application提交时,会将所需要的所有资源都申请完毕,然后再执行job,如果申请不到资源,则一直等待;当所有的任务都执行完毕才会释放这些资源
优点:application在执行前就申请到了资源,每个task执行的时候无需再次申请资源,这样会提升application的执行速度
缺点:容易造成资源浪费,当出现数据倾斜的时候,会导致大部分资源浪费,导致资源利用不充分
5.细粒度资源申请
提交Application提交时不申请资源,在TaskScheduler发送task到Executor后,由每个task自行申请资源,然后每个Task执行完毕后释放资源
优点:资源充分利用
缺点:由每个Task自行申请资源,这样会导致Tas执行周期变成,会导致整个Application的执行速度变慢
八.spark-submit参数说明
1.参数说明
参数名称 含义
–master MASTER_URL 可以是spark://host:port, mesos://host:port, yarn, yarn-cluster,yarn-client, local
–deploy-mode DEPLOY_MODE Driver程序运行的地方,client或者cluster
–class CLASS_NAME 主类名称,含包名
–name NAME Application名称
–jars JARS Driver依赖的第三方jar包
–py-files PY_FILES 用逗号隔开的放置在Python应用程序PYTHONPATH上的.zip, .egg, .py文件列表
–files FILES 用逗号隔开的要放置在每个executor工作目录的文件列表
–conf PROP=VALUE 配置spark相关设置
–properties-file FILE 设置应用程序属性的文件路径,默认是conf/spark-defaults.conf
–driver-memory MEM Driver程序使用内存大小
–driver-java-options 将额外的java选项传给应用程序
–driver-library-path Driver程序的库路径
–driver-class-path Driver程序的类路径
–executor-memory MEM executor内存大小,默认1G
–proxy-user NAME
–driver-cores NUM Driver程序的使用CPU个数,仅限于Spark Alone模式
–supervise 失败后是否重启Driver,仅限于Spark Alone模式
–kill SUBMISSION_ID 杀死指定程序
–status SUBMISSION_ID 获取指定的驱动程序的状态
–total-executor-cores NUM executor使用的总核数,仅限于Spark Alone、Spark on Mesos模式
–driver-cores NUM 驱动程序使用的内核数,仅在群集模式下(默认值:1)。仅限于Spark on Yarn模式
–executor-cores NUM 每个executor使用的内核数,默认为1,仅限于Spark on Yarn模式
–queue QUEUE_NAME 提交应用程序给哪个YARN的队列,默认是default队列,仅限于Spark on Yarn模式
–num-executors NUM 启动的executor数量,默认是2个,仅限于Spark on Yarn模式
原文:https://blog.csdn.net/u013014440/article/details/71525093
2.参数调优
1.Executor 在集群中分散启动
2.提交Application如果不指定参数,那么集群中每个Worker会为当前Application启动一个Executor,这个Executor会使用当前节点的所有core和1G内存
3.如果想在Worker上启动多个Executor,需要指定–executor-cores
4.启动Executor不仅仅要查看core,还要查看内存
5.提交一个Application时,一定要指定–total-executor-cores, 否则系统core会被占用完
九.SparkShuffle
1.spark概念
对于Spark来讲,一些Transformation或Action算子会让RDD产生宽依赖,即parent RDD中的每个Partition被child RDD中的多个Partition使用,这时便需要进行Shuffle,根据Record的key对parent RDD进行重新分区。如果对这些概念还有一些疑问,可以参考我的另一篇文章《Spark基本概念快速入门》。
以Shuffle为边界,Spark将一个Job划分为不同的Stage,这些Stage构成了一个大粒度的DAG。Spark的Shuffle分为Write和Read两个阶段,分属于两个不同的Stage,前者是Parent Stage的最后一步,后者是Child Stage的第一步。如下图所示:
链接:https://www.jianshu.com/p/4c5c2e535da5
**Shuffle Write:**上一个stage的每个map task就必须保证将自己处理的当前分区的数据相同的key写入一个分区文件中,可能会写入多个不同的分区文件中。
**Shuffle Read:**reduce task就会从上一个stage的所有task所在的机器上寻找属于己的那些分区文件,这样就可以保证每一个key所对应的value都会汇聚到同一个节点上去处理和聚合。
2.普通hashShuffle
1.每个task通过hash % reduce数量的方式,将数据写到不同的buffer,每个buffer默认32K
2.最后每个buffer对应一个文件
3.reduce read分别从这些小文件中获取数据
最后会参数MapTaskNum * reduceTaskNum个小文件, 使用这种方式会导致平方的打开关闭文件
弊端: 1.在Shuffle Write过程中会产生很多写磁盘小文件的对象
2.在shuffle read过程中会参数很多读取磁盘小文件的对象
3.这么多小文件对象,有可能导致jvm 内存溢出
4.在数据传输过程中会有频繁的网络通信,频繁的网络通信出现通信故障的可能性大大增加,一旦网络通信出现了故障会导致shuffle file cannot find 由于这个错误导致的task失败,TaskScheduler不负责重试,由DAGScheduler负责重试Stage。
3.合并hashShuffle
同一个work中每个task用于一套buffer,这样产生的磁盘文件数量为:C(core的个数)*R(reduce的个数)
4.普通SortShuffle
执行流程:
1. map task 的计算结果会写入到一个内存数据结构里面,内存数据结构默认是5M
2. 在shuffle的时候会有一个定时器,不定期的去估算这个内存结构的大小,当内存结构中的数据超过5M时,比如现在内存结构中的数据为5.01M,那么他会申请5.01*2-5=5.02M内存给内存数据结构。
3. 如果申请成功不会进行溢写,如果申请不成功,这时候会发生溢写磁盘。
4. 在溢写之前内存结构中的数据会进行排序分区
5. 然后开始溢写磁盘,写磁盘是以batch的形式去写,一个batch是1万条数据,
6. map task执行完成后,会将这些磁盘小文件合并成一个大的磁盘文件,同时生成一个索引文件。
7. reduce task去map端拉取数据的时候,首先解析索引文件,根据索引文件再去拉取对应的数据。
产生磁盘小文件的个数: 2*M(map task的个数)
5.byPass SortShuffle
byPassSortShuffle不会执行选择排序,而是通过下边的条件触发排序
1 .bypass运行机制的触发条件如下:
shuffle reduce task的数量小于spark.shuffle.sort.bypassMergeThreshold的参数值。这个值默认是200。
2 .产生的磁盘小文件为:2*M(map task的个数)
6.Shuffle 寻址
1.当map task执行完成后,会将task的执行情况和磁盘小文件的地址封装到MpStatus对象中,通过MapOutputTrackerWorker对象向Driver中的MapOutputTrackerMaster汇报。
2.在所有的map task执行完毕后,Driver中就掌握了所有的磁盘小文件的地址。
3.在reduce task执行之前,会通过Excutor中MapOutPutTrackerWorker向Driver端的MapOutputTrackerMaster获取磁盘小文件的地址。
4.获取到磁盘小文件的地址后,会通过BlockManager中的ConnectionManager连接数据所在节点上的ConnectionManager,然后通过BlockTransferService进行数据的传输。
5.BlockTransferService默认启动5个task去节点拉取数据。默认情况下,5个task拉取数据量不能超过48M。
7.Spark内存分配
8.shuffle调优
spark.shuffle.file.buffer
默认值:32k
参数说明:该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲中,待缓冲写满之后,才会溢写到磁盘。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如64k),从而减少shuffle write过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
spark.reducer.maxSizeInFlight
默认值:48m
参数说明:该参数用于设置shuffle read task的buffer缓冲大小,而这个buffer缓冲决定了每次能够拉取多少数据。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
spark.shuffle.io.maxRetries
默认值:3
参数说明:shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败。
调优建议:对于那些包含了特别耗时的shuffle操作的作业,建议增加重试最大次数(比如60次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的shuffle过程,调节该参数可以大幅度提升稳定性。
shuffle file not find taskScheduler不负责重试task,由DAGScheduler负责重试stage
spark.shuffle.io.retryWait
默认值:5s
参数说明:具体解释同上,该参数代表了每次重试拉取数据的等待间隔,默认是5s。
调优建议:建议加大间隔时长(比如60s),以增加shuffle操作的稳定性。
spark.shuffle.memoryFraction
默认值:0.2
参数说明:该参数代表了Executor内存中,分配给shuffle read task进行聚合操作的内存比例,默认是20%。
调优建议:如果内存充足,而且很少使用持久化操作,建议调高这个比例,给shuffle read的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘。在实践中发现,合理调节该参数可以将性能提升10%左右。
spark.shuffle.manager
默认值:sort|hash
参数说明:该参数用于设置ShuffleManager的类型。Spark 1.5以后,有三个可选项:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默认选项,但是Spark 1.2以及之后的版本默认都是SortShuffleManager了。tungsten-sort与sort类似,但是使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。
调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑不需要对数据进行排序,那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort要慎用,因为之前发现了一些相应的bug。
spark.shuffle.sort.bypassMergeThreshold----针对SortShuffle
默认值:200
参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。
spark.shuffle.consolidateFiles----针对HashShuffle
默认值:false
参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。
调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。
十.spark 广播变量与累加器
1.广播变量
val conf = new SparkConf()
conf.setMaster("local").setAppName("brocast")
val sc = new SparkContext(conf)
val list = List("hello xasxt")
val broadCast = sc.broadcast(list)
val lineRDD = sc.textFile("./words.txt")
lineRDD.filter { x => broadCast.value.contains(x) }.foreach { println}
sc.stop()
Ø 注意事项
¬ 能不能将一个RDD使用广播变量广播出去?
不能,因为RDD是不存储数据的。可以将RDD的结果广播出去。
¬ 广播变量只能在Driver端定义,不能在Executor端定义。
¬ 在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的值。
2.累加器
val conf = new SparkConf()
conf.setMaster("local").setAppName("accumulator")
val sc = new SparkContext(conf)
val accumulator = sc.accumulator(0)
sc.textFile("./words.txt").foreach { x =>{accumulator.add(1)}}
println(accumulator.value)
sc.stop()
Ø 注意事项
¬ 累加器在Driver端定义赋初始值,累加器只能在Driver端读取,在Excutor端更新。