spark 核心笔记记录

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端更新。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值