Spark

Spark

Spark的特性:

1、RDD是什么:

RDD时弹性分布数据集,是Spark中最基本的数据抽象概念,代表一个不可变的,可分区的,内部元素可以并行计算的集合。

RDD的特性:
1)rdd由一组分片组成

2)使用一些算子计算RDD中的每个分片的数据

3)每次转换生成一个新的RDD

4)基于HashParitioner或RangePartitioner进行重分区

5)尽可能移动计算而不移动数据

2、容错机制

1、Lineage(血统)机制:Lineage记录的是粗颗粒度的特定数据Transformation操作(如filter、map、join等)行为。当这个RDD的部分分区数据丢失时,它可以通过Lineage获取足够的信息来重新运算和恢复丢失的数据分区。
		宽依赖:父RDD的每一个分区被多个个子RDD的分区所用
		窄依赖:父RDD的每一个分区最多被一个子RDD的分区所用
2、Checkpoint机制:就是把内存中的变化刷新到持久存储,斩断依赖链 在存储中 checkpoint 是一个很常见的概念

3、内存机制

使用统一内存管理,存储内存和执行内存共享同一块空间,动态占用对方的空闲区域,JVM堆内存中有300M的预留内存,其余的40%用来存储用户定义的数据结构或Spark内部元数据,其余的60%为统一内存,一半用来做缓存,一半用来做运算,可以动态占用对方空闲区域

4、快速

1、基于内存进行计算,减少了网络IO。
2、优秀的作业调度:Spark中使用了有向无环图(Directed Acyclic Graph,DAG)这一概念。一个Spark应用由若干个作业构成,首先Spark将每个作业抽象成一个图,图中的节点是数据集,图中的边是数据集之间的转换关系;然后Spark基于相应的策略将DAG划分出若干个子图,每个子图称为一个阶段,而每个阶段对应一组任务;最后每个任务交由集群中的执行器进行计算。借助于DAG,Spark可以对应用程序的执行进行优化,能够很好地实现循环数据流和内存计算。

5、易用性

Spark是基于Scala语言开发的,由于Scala是一种面向对象的、函数式的静态编程语言,其强大的类型推断、模式匹配、隐式转换等一系列功能结合丰富的描述能力使得Spark应用程序代码非常简洁。

6、通用性

Spark框架包含了多个紧密集成的组件。Spark提供了一系列面向不同应用需求的组件,主要有Spark SQL、Spark Streaming、MLlib、GraphX。

7、多种运行模式

1、Spark on Yarn模式:在这一模式下,Spark作为一个提交程序的客户端将Spark任务提交到Yarn上,然后通过Yarn来调度和管理Spark任务执行过程中所需的资源。在搭建此模式的Spark集群过程中,需要先搭建Yarn集群,然后将Spark作为Hadoop中的一个组件纳入到Yarn的调度管理下,这样将更有利于系统资源的共享。
2、Spark Standalone模式:该模式是不借助于第三方资源管理框架的完全分布式模式。Spark使用自己的Master进程对应用程序运行过程中所需的资源进行调度和管理。对于中小规模的Spark集群首选Standalone模式。本地测试

8、弹性的理解

1.自动进行内存和磁盘切换
2.基于lineage的高效容错
3.task如果失败会特定次数的重试
4.stage如果失败会自动进行特定次数的重试,而且只会只计算失败的分片
5.checkpoint【每次对RDD操作都会产生新的RDD,如果链条比较长,计算比较笨重,就把数据放在硬盘中】和persist 【内存或磁盘中对数据进行复用】(检查点、持久化)
6.数据调度弹性:DAG TASK 和资源管理无关
7.数据分片的高度弹性repartion

Spark运行架构和运行模式:

1、Spark架构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hX0mO9oe-1590336211350)(.\imgs\driver&executor.png)]

2、Spark-yarn-cluster:任务提交

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pn4jAOvq-1590336211351)(.\imgs\Spark on Yarn-cluster.png)]

1、SparkSubmit提交任务,最终利用反射执行编写程序的main方法。

2、main方法中会存在SparkContext的初始化。

3、首先创建Spark程序提交所需要的环境,SparkEnv

4、创建TaskScheduler,封装调度方法。FIFO,FAIR

5、创建DAGScheduler,初始化线程池,等待任务提交

6、TaskScheduler开始执行任务,首先是调用封装的backend的start方法,首先调用backend的父类的start方法,创建driver,主要用来负责和Executor的通信和任务的发送,

7、创建client,负责去提交任务信息,等待任务被调度

8、任务被调度,通过尽量打散的方式去将任务分配给Worker,并通知Worker去启动executor。

9、创建一个ExecutorRunner去启动Executor进程,

10、Executor去找client去反向注册,注册成功后保留executor信息,返回注册成功信息。

11、创建executor类用来执行任务。

12、等用到action算子的时候,将最后一个RDD去提交job,提交到DAGScheduler中的线程池中。

13、执行创建Stage,通过栈的方式对递归创建Stage,最后拿到finalStage

14、提交Stage,并把Stage切分成Task,调用taskScheduler去提交tasks

15、TaskScheduler通过driver将任务提交给executor去运行。

3、任务调度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eEyCZ0N8-1590336211353)(.\imgs\Spark任务调度.png)]

Spark Streaming 任务如上文提到的是基于微批处理的,实际上每个批次都是一个 Spark Core 的任务。对于编码完成的 Spark Core 任务在生成到最终执行结束主要包括以下几个部分:
	阶段一:我们编写driver程序,定义RDD的action和 transformation操作。这些依赖关系形成操作的DAG
	阶段二:根据形成DAG,DAGScheduler将其划分为不同的 stage。
	阶段三:每一个stage中有一个TaskSet,DAGScheduler将 TaskSet交给TaskScheduler去执行,TaskScheduler将任务执行 完毕之后结果返回给DAGSCheduler。
	阶段四:TaskScheduler将任务分发到每一个Worker节点去执行, 并将结果返回给TaskScheduler。
    
对于 job 的调度执行有 fifo 和 fair 两种模式,

4、术语解释

1、application:基于 Spark 构建的用户程序,包括了集群上的一个 driver 程序与多个 executor;
2、driver:运行 application 的 main 函数,创建 SparkContext 的进程; 
3、executor:application在 worker node 上启动的一个进程, 该进程运行 task 并在内存或磁盘上保存数据。每个 application 都有其独 有的 executor; 
4、Job:由spark action操作触发,包含多个task组成的并行计算; 
5、stage:每个 job 被分解为多个 stage, 每个 stage 是一些 task 的集合。宽依赖是将job划分为stage的依据; 
6、taskset:即stage; 
7、task:任务处理的最小单元,由driver发送到 executor上的工作单元。这些Task之间处理逻辑相同,数据不同; 
8、DAGScheduler: 实现将Spark作业分解成一到多个Stage,每个Stage根据RDD的Partition个数决定Task的个数,然后生成相应的Task set放到TaskScheduler中。 
9、TaskScheduler:实现Task分配到Executor上执行

5、SparkShuffle

1、未经优化的HashShuffleManager

上游的每一个task都为下游所有的task生成一份文件

2、优化后的HashShuffleManager

生成文件之后,将相同类型的文件合并

3、SortShuffleManager的普通机制

每一个Task最后会将所有文件聚合并且建立索引文件

4、SortShuffleManager的bypass机制

使用未经优化的HashShuffleManager,合并文件


常见的shuffle算子:
byKey类算子
join类算子
sort类算子 
distinct
Spark shuffle的更新迭代:
Spark中负责shuffle过程的执行、计算、和处理的组件主要是ShuffleManager,它是shuffle管理器。
Spark 1.2之后的版本中,默认的ShuffleManager改成了SortShuffleManager,之前是的是HashShuffleManager因为HashShuffleManager<,有着一个非常严重的弊端,就是会产生大量的中间磁盘文件,进而由大量的磁盘IO操作影响了性能。
SortShuffleManager有一定的改进,在每个Task在进行shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并成一个磁盘文件,所以每个Task就只有一个磁盘文件,在stage的shuffle read task 拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据。


未经优化的HashShuffleManager:

SparkShuffle,先从shuffle write开始说起。
在shuffle write阶段,主要就是在一个stage结束计算之后,为了下一个stage可以执行shuffle类的算子(例如reduceBykey),而将每个task处理的数据按照key进行分类,所谓的分类就是,对相同的key执行hash算法,从而将相同key都写入同一个磁盘文件中,而每一个磁盘文件都只属于下游stage的一个task。在将数据写入磁盘之前,会将数据写入内存缓冲中,当内存缓冲填满之后,才会写入到磁盘文件中。
shuffle read阶段,通常就是一个stage刚开始时要做的事情。此时该stage的每一个task就需要将上一个stage的计算结果中的所有相同key,从各个节点上通过网络都拉取到了自己所有的节点上,然后进行key的聚合或连接等操作。由于shuffle write的过程中,task给下游stage的每个task都创建一个磁盘文件,因此shuffle read的过程中,每一个task只要从上游stage的所有task所在节点上,拉去属于自己的那一个磁盘文件。shuffle read的拉取过程是一边拉取一边进行聚合的。每个shuffle read task都会有一个自己的buffer缓冲,每次都只能拉取与buffer缓冲相同大小的数据,然后通过内存中的一个Map进行聚合等操作。聚合完一批数据后,再拉取下一批数据,并放到buffer缓冲中进行聚合操作。以此类推,直到最后将所有数据到拉取完,并得到最终的结果。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VEhimgYm-1590336211354)(.\imgs\未优化的HashShuffle.png.jpg)]

优化后的HashShuffleManager:

这里所说的优化就是,指的是设置一个参数spark.shuffle.consolidateFiles,默认是false,将其设置为true即可开启优化机制。
开启consolidate机制之后,在shuffle write过程中,此时出现了shuffleFileGroup,每个shuffleFileGroup会对应一批磁盘文件,磁盘文件的数量与下游stage的task数量是相同的。
一个Executor上有多少个CPU core,就可以并行执行多少个task。而第一批并行执行的每个task都会创建一个shuffleFileGroup,并将数据写入对应的磁盘文件内。
当Executor的CPU core执行完一批task,接着执行下一批task时,下一批task就会复用之前已有的shuffleFileGroup,包括其中的磁盘文件。也就是说,此时task会将数据写入已有的磁盘文件中,而不会写入新的磁盘文件中。因此,consolidate机制允许不同的task复用同一批磁盘文件,这样就可以有效将多个task的磁盘文件进行一定程度上的合并,从而大幅度减少磁盘文件的数量,进而提升shuffle write的性能。
就是对一个磁盘文件的重用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fOsjS7yZ-1590336211354)(.\imgs\优化的HashShuffle.png.jpg)]

SortShuffleManager运行原理:

SortShuffleManager的运行机制主要是分成两种,一种是普通的运行机制,另一种是bypass运行机制。
当shuffle read task 的数据小于spark.shuffle.sort.bypassMergeThreshold的参数的值时(默认时),就会启用bypass机制。



SortshuffleManager普通运行机制:

在该模式下,数据会先写入一个内存数据结构中,此时根据不同的shuffle算子,可能会选择不同的数据结构,如果时reduceByKey这种聚合类的shuffle算子,那么会选用Map数据结构,一边通过Map进行聚合,一边写入内存。如果是join这种普通的shuffle算子,那么会选用Array数据结构,直接写入内存。
接着每一条数据进入内存数据结构之后,就会进行判断一下,是否达到了某个临界阈值,如果达到的话,那么久会尝试将内存数据结构中的数据溢写道磁盘,然后清空数据结构。
在溢写道磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。排序过后,会分批次将数据写入磁盘文件,默认的batch数量是10000条,也就是说,排序好的数据,会以每批次10000条数据的形式分批写入磁盘文件。写入磁盘文件是通过Java的BuffleredOutputStream实现的。
一个task将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时文件。最后会将之前所有的临时磁盘文件都进行合并,这就是merge过程,此时会将之前所有临时磁盘文件中的数据读取出来,然后依次写入最终的磁盘文件之中。此外,由于一个task就只对应一个磁盘文件,也就意味着该task为下游stage的task准备的数据都在这一个文件中,因此还会单独写一份索引文件,其中标识了下游各个task的数据在文件中的start offset与end offset。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Es0GumS-1590336211355)(.\imgs\普通机制SortShuffleManager.png.jpg)]

bypass运行机制:

bypass运行机制的触发条件是:
1、shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值。
2、不是聚合类的shuffle算子(比如reduceByKey)。

此时task会为每个下游task都创建一个临时磁盘文件,并将数据按key进行hash然后根据key的hash值,将key写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。
该过程的磁盘写机制其实跟未经优化的HashShuffleManager是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的HashShuffleManager来说,shuffle read的性能会更好。
而该机制与普通SortShuffleManager运行机制的不同在于:
第一,磁盘写机制不同;
第二,不会进行排序。
也就是说,启用该机制的最大好处在于,shuffle write过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yq0ZLYoW-1590336211357)(.\imgs\bypass的SortShuffleManager.png.jpg)]

Spark算子:

1、遍历算子:map --mapPartitions—mapPartitionsWithIndex

    //遍历类算子:map mapPartitions
    val rdd1: RDD[Int] = sc.parallelize(List(1,2,3,4,5,65),2)
	//map:
    val rdd2: RDD[Int] = rdd1.map(_ * 10)
    println(rdd2.collect.toBuffer)
	//mapPartitions
	println(rdd1.mapPartitions(_.toList.iterator).collect.toBuffer)
	//mapPartitionsWithIndex
    val func = (index: Int, iter: Iterator[(Int)]) => {
      iter.toList.map(x => "[partID:" +  index + ", val: " + x + "]").iterator
    }
    val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7, 8, 9), 2)
    println(rdd1.mapPartitionsWithIndex(func).collect.toBuffer)
	结果:
ArrayBuffer([partID:0, val: 1], [partID:0, val: 2], [partID:0, val: 3], [partID:0, val: 4], [partID:1, val: 5], [partID:1, val: 6], [partID:1, val: 7], [partID:1, val: 8], [partID:1, val: 9])

	


说明:
map(func):Return a new distributed dataset formed by passing each element of the source through a function func.(通过函数func传递源的每个元素,返回一个新的分布式数据集)

mapPartitions(func):Similar to map, but runs separately on each partition (block) of the RDD, so func must be of type Iterator<T> => Iterator<U> when running on an RDD of type T.(和map类似的,但是在RDD的每个分区()上单独运行,所以func在类型为T的RDD上运行时必须是类型Iterator<T> => Iterator)

mapPartitionsWithIndex(func):Similar to mapPartitions, but also provides func with an integer value representing the index of the partition, so func must be of type (Int, Iterator<T>) => Iterator<U> when running on an RDD of type T.(与mappartition类似,但也为func提供了一个表示分区索引的整数值,所以func的类型必须是(Int, Iterator<T>) =>迭代器,当运行在类型为T的RDD上时)


优缺点:
map:类似流式一条一条的读取,所以IO通信多速度慢效率低
mapPartitions:是以分区为单位所以类型必须是Iterator,相比Map速度快,效率高。但是可能会出现内存溢出的问题

适用情景:
数据量较大,适合使用mapPartitions
数据量很大,用mapPartitions容易出现oom(内存溢出),适合用map
数据量较小,Map即可

2、排序类算子:sortBykey–sortBy

 //排序类算子:sortBykey--sortBy
 val rdd1: RDD[(Int, String)] = sc.parallelize(Array((3,"aa"),(6,"cc"),(2,"bb"),(1,"dd")))
 val rdd2: RDD[(Int, String)] = rdd1.sortByKey()
//结果L:ArrayBuffer((1,dd), (2,bb), (3,aa), (6,cc))。应该是根据key进行排序
 val rdd1 = sc.parallelize(List(5,6,4,7,3,8,2,9,10))
 val rdd2: RDD[Int] = rdd1.sortBy(x => x, true)
//结果:ArrayBuffer(2, 3, 4, 5, 6, 7, 8, 9, 10)

说明:
sortByKey([ascending], [numTasks]):	When called on a dataset of (K, V) pairs where K implements Ordered, returns a dataset of (K, V) pairs sorted by keys in ascending or descending order, as specified in the boolean ascending argument.(在K实现有序的(K, V)对数据集调用时,返回按键升序或降序排序的(K, V)对数据集,如布尔升序参数中指定的那样)

sortBy:sortBy to make a globally ordered RDD  升序
  源码:def sortBy[K](
      f: (T) => K,
      ascending: Boolean = true,
      numPartitions: Int = this.partitions.length)
      (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T] = withScope {
    this.keyBy[K](f)
        .sortByKey(ascending, numPartitions)
        .values
  }



3、重新分区算子:repartition–coalesce–partitionBy–repartitionAndSortWithinPartitions

//重新分区算子:repartition--coalesce--repartitionAndSortWithinPartitions
//repartition:底层调用的coalesce算子,只是吧第二个参数设置成为true,默认支持shuffle
    val rdd1 = sc.parallelize(1 to 10, 3)
    println(rdd1.partitions.length)
    val res1: RDD[Int] = rdd1.repartition(5)
    println(res1.partitions.length)

//coalesce
    val res2: RDD[Int] = rdd1.coalesce(2, false)
    println(res2.partitions.length)
//partitionBy:写分区器
    val rdd1 = sc.parallelize(List(("e", 5), ("c", 3), ("d", 4), ("c", 2), ("a", 1)), 2)
    val res3: RDD[(String, Int)] = rdd1.partitionBy(new HashPartitioner(4))
    println(res3.partitions.length)
//repartitonAandSortWithinPartitions
    val rdd4 = sc.parallelize(Array((3, "aa"), (6, "cc"), (2, "bb"), (1, "dd")))
    val unit: RDD[(Int, String)] =  rdd4.repartitionAndSortWithinPartitions(new org.apache.spark.HashPartitioner(1))
    unit.foreach(println)


//groupBy(func)
//说明:按照传入函数的返回值进行分组,将相同的key对应的值放入一个迭代器,
	例如:rdd.groupBy(_%2)



说明:
repartition(numPartitions):Reshuffle the data in the RDD randomly to create either more or fewer partitions and balance it across them. This always shuffles all data over the network.(随机地重新洗牌RDD中的数据,以创建更多或更少的分区,并在它们之间进行平衡。这总是通过网络打乱所有数据)

coalesce(numPartitions):Decrease the number of partitions in the RDD to numPartitions. Useful for running operations more efficiently after filtering down a large dataset.(将RDD中的分区数量减少到numpartition。适用于过滤大型数据集后更有效地运行操作。)

partitionBy(partitioner):Repartition the RDD according to the given partitioner 

repartitionAndSortWithinPartitions(partitioner):Repartition the RDD according to the given partitioner and, within each resulting partition, sort records by their keys. This is more efficient than calling repartition and then sorting within each partition because it can push the sorting down into the shuffle machinery.(根据给定的分区程序重新划分RDD,并在每个结果分区中根据键对记录进行排序。这比调用重划分然后在每个分区内排序更有效,因为它可以将排序向下推到洗牌机制中。)


4、聚合类算子(分区内的运算规则和分区间的运算规则):aggregate–aggregateByKey–combineByKey

//聚合类算子:aggregate--aggregateByKey--combineByKey
//aggregate
    val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7, 8), 2)
	val i: Int = rdd1.aggregate(0)(math.max(_,_), _+_)//12
	val i1: Int = rdd1.aggregate(5)(math.max(_, _), _ + _)//18  
	//第一个分区得到:5,第二个分区得到 8:总的时候再加一个初始值5
  	val rdd2 = sc.parallelize(List("a","b","c","d","e","f"),2)
    val str: String = rdd2.aggregate("yyyy-")(_ + _, _ + _)//yyyy-yyyy-abcyyyy-def

	//空字符串的Length为1

    val rdd3 = sc.parallelize(List("12","23","345","4567"),2)
    rdd3.aggregate("")((x,y) => math.max(x.length, y.length).toString, (x,y) => x + y)
			
//aggregateByKey
	val pairRDD = sc.parallelize(List(("mouse", 2),("cat",2), ("cat", 5), ("mouse", 4),("cat", 12), ("dog", 12)), 2)
    println(pairRDD.aggregateByKey(0)(math.max(_, _), _ + _).collect.toBuffer)
//ArrayBuffer((dog,12), (cat,17), (mouse,6))
	    println(pairRDD.aggregateByKey(100)(math.max(_, _), _ + _).collect.toBuffer)
//ArrayBuffer((dog,100), (cat,200), (mouse,200))

//combineByKey
    val rdd4 = sc.parallelize(List("dog", "cat", "gnu", "salmon", "rabbit", "turkey", "wolf", "bear", "bee"), 3)
    val rdd5 = sc.parallelize(List(1, 1, 2, 2, 2, 1, 2, 2, 2), 3)
    val rdd6 = rdd5.zip(rdd4)
    println(rdd6.collect.toBuffer)
    val rdd7 = rdd6.combineByKey(List(_), (x: List[String], y: String) => x :+ y, (m: List[String], n: List[String]) => m ++ n)
    println(rdd7.collect.toBuffer)
//ArrayBuffer((1,List(dog, cat, turkey)), (2,List(gnu, salmon, rabbit, wolf, bear, bee)))

说明:
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]):When called on a dataset of (K, V) pairs, returns a dataset of (K, U) pairs where the values for each key are aggregated using the given combine functions and a neutral "zero" value. Allows an aggregated value type that is different than the input value type, while avoiding unnecessary allocations. Like in groupByKey, the number of reduce tasks is configurable through an optional second argument.(当对一个(K, V)对的数据集调用时,返回一个(K, U)对的数据集,其中每个键的值使用给定的combine函数和一个中性的“0”值进行聚合。允许与输入值类型不同的聚合值类型,同时避免不必要的分配。与groupByKey类似,reduce任务的数量可以通过第二个可选参数进行配置。)



aggregate:
初始值会作用到局部聚合的每个分区上,也会作用到全局聚合的过程中
aggregateByKey:
初始值会作用到局部聚合的每个分区上,不会作用到全局聚合的过程中

combineByKey:
 :: Experimental ::
   * Generic function to combine the elements for each key using a custom set of aggregation
   * functions. Turns an RDD[(K, V)] into a result of type RDD[(K, C)], for a "combined type" C
   *
   * Users provide three functions:
   *
   *  - `createCombiner`, which turns a V into a C (e.g., creates a one-element list)
   *  - `mergeValue`, to merge a V into a C (e.g., adds it to the end of a list)
   *  - `mergeCombiners`, to combine two C's into a single one.
   *
   * In addition, users can control the partitioning of the output RDD, and whether to perform
   * map-side aggregation (if a mapper can produce multiple items with the same key).
   *
   * @note V and C can be different -- for example, one might group an RDD of type
   * (Int, Int) into an RDD of type (Int, Seq[Int]).


  def combineByKey[C](
      createCombiner: V => C,
      mergeValue: (C, V) => C,
      mergeCombiners: (C, C) => C): RDD[(K, C)] = self.withScope {
    combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners)(null)
  }

//foldByKey分区内和分区间的计算规则一样

5、action类算子

val rdd1 = sc.parallelize(List(2,1,3,6,5),2)
reduce、count、top、take、takeOrdered、first
//reduce
    val i: Int = rdd1.reduce((x:Int, y:Int) => x+y)
    //说明reduce(func):Aggregate the elements of the dataset using a function func (which takes two arguments and returns one). The function should be commutative and associative so that it can be computed correctly in parallel.(使用函数func(它接受两个参数并返回一个)聚合数据集的元素。这个函数应该是交换的和结合的,这样它才能被正确地并行计算)

//count
     val i: Long  =rdd1.count()
    //说明:count():Return the number of elements in the dataset.(返回数据集中元素的数量。)

//top
     val i: Array[Int] = rdd1.top(3)
    //说明:top(num:Int);This method should only be used if the resulting array is expected to be small, as all the data is loaded into the driver's memory.

     @param num k, the number of top elements to return
     @param ord the implicit ordering for T
     @return an array of top elements

//take
	val i: Array[Int] = rdd1.take(3)
	//说明:take(n);Return an array with the first n elements of the dataset.(返回数据集的前n个元素的数组)


//takeOrdered

	//说明:takeOrdered(n, [ordering]):Return the first n elements of the RDD using either their natural order or a custom comparator.(使用RDD的自然顺序或自定义比较器返回RDD的前n个元素。)

// first()

	//说明:Return the first element of the dataset (similar to take(1)).(返回数据集的第一个元素(类似于take(1)))


countByKey、countByValue
val rdd1 = sc.parallelize(List(("a", 1), ("b", 2), ("b", 2), ("c", 2), ("c", 1)))
rdd1.countByKey//ArrayBuffer((a,1), (b,2), (c,2))
rdd1.countByValue//ArrayBuffer(((b,2),2), ((c,2),1), ((a,1),1), ((c,1),1))
	//countByKey():Only available on RDDs of type (K, V). Returns a hashmap of (K, Int) pairs with the count of each key.(仅在类型(K, V)的RDDs上可用。返回(K, Int)对的hashmap和每个键的计数。)
	//countByValue:整个元组,------>((整个元组),元组个数)

foreach、foreachPartition
val rdd1 = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7, 8, 9), 3)
rdd1.foreachPartition(x => println(x.reduce(_ + _)))

比较:
以上算子一般是在最后持久化后使用的,此时的应用场景:
如果选哟将数据写到数据库中,如果用foreach,数据量也较大,回创建很多个数据连接,占用内存的同时回给数据库的节点带来很大压力。--此时可用用foreachPartition,一个分区对应一个连接

6、其它算子

filterByRange
val rdd1 = sc.parallelize(List(("e", 5), ("c", 3), ("d", 4), ("c", 2), ("a", 1)))
val rdd2 = rdd1.filterByRange("c", "d")
rdd2.collect
-------------------------------------------------------------------------------------------
flatMapValues
val rdd3 = sc.parallelize(List(("a", "1 2"), ("b", "3 4")))
rdd3.flatMapValues(_.split(" "))
-------------------------------------------------------------------------------------------
foldByKey
val rdd1 = sc.parallelize(List("dog", "wolf", "cat", "bear"), 2)
val rdd2 = rdd1.map(x => (x.length, x))
val rdd3 = rdd2.foldByKey("")(_+_)

val rdd = sc.textFile("hdfs://node01:9000/wc").flatMap(_.split(" ")).map((_, 1))
rdd.foldByKey(0)(_+_)
-------------------------------------------------------------------------------------------
keyBy
val rdd1 = sc.parallelize(List("dog", "salmon", "salmon", "rat", "elephant"), 3)
val rdd2 = rdd1.keyBy(_.length)
rdd2.collect
-------------------------------------------------------------------------------------------
keys values
val rdd1 = sc.parallelize(List("dog", "tiger", "lion", "cat", "panther", "eagle"), 2)
val rdd2 = rdd1.map(x => (x.length, x))
rdd2.keys.collect
rdd2.values.collect
-------------------------------------------------------------------------------------------
collectAsMap
val rdd = sc.parallelize(List(("a", 1), ("b", 2)))
rdd.collectAsMap
-------------------------------------------------------------------------------------------

Spark调优:

1、避免创建重复的RDD

我们在开发过程中要注意:对于同一份数据,只应该创建一个RDD,不能创建多个RDD来代表同一份数据。

2、尽可能复用同一个RDD

在对不同的数据执行算子操作时还要尽可能地复用一个RDD。比如说,有一个RDD的数据格式是key-value类型的,另一个是单value类型的,这两个RDD的value数据是完全一样的。那么此时我们可以只使用key-value类型的那个RDD,因为其中已经包含了另一个的数据。

3、对多次使用的RDD进行持久化

Spark中对于一个RDD执行多次算子的默认原理是这样的:每次你对一个RDD执行一个算子操作时,都会重新从源头处计算一遍,计算出那个RDD来,然后再对这个RDD执行你的算子操作。这种方式的性能是很差的。
因此对于这种情况,我们的建议是:对多次使用的RDD进行持久化。此时Spark就会根据你的持久化策略,将RDD中的数据保存到内存或者磁盘中。以后每次对这个RDD进行算子操作时,都会直接从内存或磁盘中提取持久化的RDD数据,然后执行算子,而不会从源头处重新计算一遍这个RDD,再执行算子操作。

如何选择一种最合适的持久化策略

1、默认情况下,性能最高的当然是MEMORY_ONLY,但前提是你的内存必须足够足够大,可以绰绰有余地存放下整个RDD的所有数据。因为不进行序列化与反序列化操作,就避免了这部分的性能开销;对这个RDD的后续算子操作,都是基于纯内存中的数据的操作,不需要从磁盘文件中读取数据,性能也很高;而且不需要复制一份数据副本,并远程传送到其他节点上。但是这里必须要注意的是,在实际的生产环境中,恐怕能够直接用这种策略的场景还是有限的,如果RDD中数据比较多时(比如几十亿),直接用这种持久化级别,会导致JVM的OOM内存溢出异常。
2、如果使用MEMORY_ONLY级别时发生了内存溢出,那么建议尝试使用MEMORY_ONLY_SER级别。该级别会将RDD数据序列化后再保存在内存中,此时每个partition仅仅是一个字节数组而已,大大减少了对象数量,并降低了内存占用。这种级别比MEMORY_ONLY多出来的性能开销,主要就是序列化与反序列化的开销。但是后续算子可以基于纯内存进行操作,因此性能总体还是比较高的。此外,可能发生的问题同上,如果RDD中的数据量过多的话,还是可能会导致OOM内存溢出的异常。
3、如果纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK策略。因为既然到了这一步,就说明RDD的数据量很大,内存无法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。

4、尽量避免使用shuffle类算子

因为Spark作业运行过程中,最消耗性能的地方就是shuffle过程。shuffle过程,简单来说,就是将分布在集群中多个节点上的同一个key,拉取到同一个节点上,进行聚合或join等操作。比如reduceByKey、join等算子,都会触发shuffle操作。

shuffle过程中,各个节点上的相同key都会先写入本地磁盘文件中,然后其他节点需要通过网络传输拉取各个节点上的磁盘文件中的相同key。而且相同key都拉取到同一个节点进行聚合操作时,还有可能会因为一个节点上处理的key过多,导致内存不够存放,进而溢写到磁盘文件中。因此在shuffle过程中,可能会发生大量的磁盘文件读写的IO操作,以及数据的网络传输操作。磁盘IO和网络数据传输也是shuffle性能较差的主要原因。

因此在我们的开发过程中,能避免则尽可能避免使用reduceByKey、join、distinct、repartition等会进行shuffle的算子,尽量使用map类的非shuffle算子。这样的话,没有shuffle操作或者仅有较少shuffle操作的Spark作业,可以大大减少性能开销。

5、使用map-side预聚合的shuffle操作

所谓的map-side预聚合,说的是在每个节点本地对相同的key进行一次聚合操作,类似于MapReduce中的本地combiner。map-side预聚合之后,每个节点本地就只会有一条相同的key,因为多条相同的key都被聚合起来了。其他节点在拉取所有节点上的相同key时,就会大大减少需要拉取的数据数量,从而也就减少了磁盘IO以及网络传输开销。通常来说,在可能的情况下,建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子。因为reduceByKey和aggregateByKey算子都会使用用户自定义的函数对每个节点本地的相同key进行预聚合。而groupByKey算子是不会进行预聚合的,全量的数据会在集群的各个节点之间分发和传输,性能相对来说比较差。


6、使用高性能的算子

1、使用reduceByKey/aggregateByKey替代groupByKey
2、使用mapPartitions替代普通map
mapPartitions类的算子,一次函数调用会处理一个partition所有的数据,而不是一次函数调用处理一条,性能相对来说会高一些。但是有的时候,使用mapPartitions会出现OOM(内存溢出)的问题。因为单次函数调用就要处理掉一个partition所有的数据,如果内存不够,垃圾回收时是无法回收掉太多对象的,很可能出现OOM异常。所以使用这类操作时要慎重!
3、使用foreachPartitions替代foreach
原理类似于“使用mapPartitions替代map”,也是一次函数调用处理一个partition的所有数据,而不是一次函数调用处理一条数据。在实践中发现,foreachPartitions类的算子,对性能的提升还是很有帮助的。比如在foreach函数中,将RDD中所有数据写MySQL,那么如果是普通的foreach算子,就会一条数据一条数据地写,每次函数调用可能就会创建一个数据库连接,此时就势必会频繁地创建和销毁数据库连接,性能是非常低下;但是如果用foreachPartitions算子一次性处理一个partition的数据,那么对于每个partition,只要创建一个数据库连接即可,然后执行批量插入操作,此时性能是比较高的。实践中发现,对于1万条左右的数据量写MySQL,性能可以提升30%以上。
4、使用filter之后进行coalesce操作
通常对一个RDD执行filter算子过滤掉RDD中较多数据后(比如30%以上的数据),建议使用coalesce算子,手动减少RDD的partition数量,将RDD中的数据压缩到更少的partition中去。因为filter之后,RDD的每个partition中都会有很多数据被过滤掉,此时如果照常进行后续的计算,其实每个task处理的partition中的数据量并不是很多,有一点资源浪费,而且此时处理的task越多,可能速度反而越慢。因此用coalesce减少partition数量,将RDD中的数据压缩到更少的partition之后,只要使用更少的task即可处理完所有的partition。在某些场景下,对于性能的提升会有一定的帮助。
5、使用repartitionAndSortWithinPartitions替代repartition与sort类操作
repartitionAndSortWithinPartitions是Spark官网推荐的一个算子,官方建议,如果需要在repartition重分区之后,还要进行排序,建议直接使用repartitionAndSortWithinPartitions算子。因为该算子可以一边进行重分区的shuffle操作,一边进行排序。shuffle与sort两个操作同时进行,比先shuffle再sort来说,性能可能是要高的。

7、广播变量

如果使用的外部变量比较大,建议使用Spark的广播功能,对该变量进行广播。广播后的变量,会保证每个Executor的内存中,只驻留一份变量副本,而Executor中的task执行时共享该Executor中的那份变量副本。这样的话,可以大大减少变量副本的数量,从而减少网络传输的性能开销,并减少对Executor内存的占用开销,降低GC的频率。

8、使用Kryo优化序列化性能

在Spark中,主要有三个地方涉及到了序列化: 
1、 在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输(见“原则七:广播大变量”中的讲解)。 
2、 将自定义的类型作为RDD的泛型类型时(比如JavaRDD,Student是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现Serializable接口。 
3、* 使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组。

Kryo序列化机制比Java序列化机制,性能高10倍左右

方式:我们只要设置序列化类,再注册要序列化的自定义类型即可

9、优化数据结构

Java中,有三种类型比较耗费内存: 
1、 对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间。 
2、字符串,每个字符串内部都有一个字符数组以及长度等额外信息。 
3、 集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Entry。

因此Spark官方建议,在Spark编码实现中,特别是对于算子函数中的代码,尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。

10、参数调优

1、num-executors:参数说明:该参数用于设置Spark作业总共要用多少个Executor进程来执行。
参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适,设置太少或太多的Executor进程都不好。设置的太少,无法充分利用集群资源;设置的太多的话,大部分队列可能无法给予充分的资源。

2、executor-memory:参数说明:该参数用于设置每个Executor进程的内存。
参数调优建议:每个Executor进程的内存设置4G~8G较为合适。但是这只是一个参考值,具体的设置还是得根据不同部门的资源队列来定。可以看看自己团队的资源队列的最大内存限制是多少,num-executors乘以executor-memory,是不能超过队列的最大内存量的。此外,如果你是跟团队里其他人共享这个资源队列,那么申请的内存量最好不要超过资源队列最大总内存的1/3~1/2,避免你自己的Spark作业占用了队列所有的资源,导致别的同学的作业无法运行。

3、executor-cores:参数说明:该参数用于设置每个Executor进程的CPU core数量。
参数调优建议:Executor的CPU core数量设置为2~4个较为合适。

4、driver-memory:参数说明:该参数用于设置Driver进程的内存。
参数调优建议:Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。

5、spark.default.parallelism:参数说明:该参数用于设置每个stage的默认task数量。
参数调优建议:Spark作业的默认task数量为500~1000个较为合适。


Spark和MapReduce区别:

1、Spark把运算中间数据放在内存,迭代计算效率高,mapreduce的中间结果需要落地,需要保存到磁盘,会有IO操作。

2、Spark容错性高,数据集是弹性的,某一部分丢失或者出错,通过整个数据集的血缘重新计算。

3、Spark通用,存在其他的API

Spark处理数据倾斜问题:

1、数据倾斜发生时的现象

情况一:绝大多数task执行得都非常快,但个别task执行极慢。比如,总共有1000个task,997个task都在1分钟之内执行完了,但是剩余两三个task却要一两个小时。这种情况很常见。
情况二:原本能够正常执行的Spark作业,某天突然报出OOM(内存溢出)异常,观察异常栈,是我们写的业务代码造成的。这种情况比较少见。

2、数据倾斜发生的原理

数据倾斜的原理很简单:在进行shuffle的时候,必须将各个节点上相同的key拉取到某个节点上的一个task来进行处理,比如按照key进行聚合或join等操作。此时如果某个key对应的数据量特别大的话,就会发生数据倾斜。比如大部分key对应10条数据,但是个别key却对应了100万条数据,那么大部分task可能就只会分配到10条数据,然后1秒钟就运行完了;但是个别task可能分配到了100万数据,要运行一两个小时。因此,整个Spark作业的运行进度是由运行时间最长的那个task决定的。
因此出现数据倾斜的时候,Spark作业看起来会运行得非常缓慢,甚至可能因为某个task处理的数据量过大导致内存溢出。

3、如何定位导致数据倾斜的代码

数据倾斜只会发生在shuffle过程中。这里给大家罗列一些常用的并且可能会触发shuffle操作的算子:distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等。出现数据倾斜时,可能就是你的代码中使用了这些算子中的某一个所导致的。

4、某个task执行特别慢的情况

yarn-cluster模式提交,则可以通过Spark Web UI来查看当前运行到了第几个stage。此外,无论是使用yarn-client模式还是yarn-cluster模式,我们都可以在Spark Web UI上深入看一下当前这个stage各个task分配的数据量,从而进一步确定是不是task分配的数据不均匀导致了数据倾斜。


知道数据倾斜发生在哪一个stage之后,接着我们就需要根据stage划分原理,推算出来发生倾斜的那个stage对应代码中的哪一部分,这部分代码中肯定会有一个shuffle类算子。精准推算stage与代码的对应关系,需要对Spark的源码有深入的理解,这里我们可以介绍一个相对简单实用的推算方法:只要看到Spark代码中出现了一个shuffle类算子或者是Spark SQL的SQL语句中出现了会导致shuffle的语句(比如group by语句),那么就可以判定,以那个地方为界限划分出了前后两个stage。

5、某个task莫名其妙内存溢出的情况

这种情况下去定位出问题的代码就比较容易了。我们建议直接看yarn-client模式下本地log的异常栈,或者是通过YARN查看yarn-cluster模式下的log中的异常栈。一般来说,通过异常栈信息就可以定位到你的代码中哪一行发生了内存溢出。然后在那行代码附近找找,一般也会有shuffle类算子,此时很可能就是这个算子导致了数据倾斜。

通过Spark Web UI查看报错的那个stage的各个task的运行时间以及分配的数据量,才能确定是否是由于数据倾斜才导致了这次内存溢出。

6、查看导致数据倾斜的key的数据分布情况

知道了数据倾斜发生在哪里之后,通常需要分析一下那个执行了shuffle操作并且导致了数据倾斜的RDD/Hive表,查看一下其中key的分布情况。这主要是为之后选择哪一种技术方案提供依据。针对不同的key分布与不同的shuffle算子组合起来的各种情况,可能需要选择不同的技术方案来解决。

此时根据你执行操作的情况不同,可以有很多种查看key分布的方式: 
1. 如果是Spark SQL中的group by、join语句导致的数据倾斜,那么就查询一下SQL中使用的表的key分布情况。 
2. 如果是对Spark RDD执行shuffle算子导致的数据倾斜,那么可以在Spark作业中加入查看key分布的代码,比如RDD.countByKey()。然后对统计出来的各个key出现的次数,collect/take到客户端打印一下,就可以看到key的分布情况。

7、数据倾斜的解决方案:

1、解决方案一:使用Hive ETL预处理数据
方案适用场景:导致数据倾斜的是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程序发生数据倾斜而已。

方案优点:实现起来简单便捷,效果还非常好,完全规避掉了数据倾斜,Spark作业的性能会大幅度提升。

方案缺点:治标不治本,Hive ETL中还是会发生数据倾斜。

方案实践经验:在一些Java系统与Spark结合使用的项目中,会出现Java代码频繁调用Spark作业的场景,而且对Spark作业的执行性能要求很高,就比较适合使用这种方案。将数据倾斜提前到上游的Hive ETL,每天仅执行一次,只有那一次是比较慢的,而之后每次Java调用Spark作业时,执行速度都会很快,能够提供更好的用户体验。

项目实践经验:在途牛的交互式用户行为分析系统中使用了这种方案,该系统主要是允许用户通过Java Web系统提交数据分析统计任务,后端通过Java提交Spark作业进行数据分析统计。要求Spark作业速度必须要快,尽量在10分钟以内,否则速度太慢,用户体验会很差。所以我们将有些Spark作业的shuffle操作提前到了Hive ETL中,从而让Spark直接使用预处理的Hive中间表,尽可能地减少Spark的shuffle操作,大幅度提升了性能,将部分作业的性能提升了6倍以上。
2、解决方案二:过滤少数导致倾斜的key
方案适用场景:如果发现导致倾斜的key就少数几个,而且对计算本身的影响并不大的话,那么很适合使用这种方案。比如99%的key就对应10条数据,但是只有一个key对应了100万数据,从而导致了数据倾斜。

方案实现思路:如果我们判断那少数几个数据量特别多的key,对作业的执行和计算结果不是特别重要的话,那么干脆就直接过滤掉那少数几个key。比如,在Spark SQL中可以使用where子句过滤掉这些key或者在Spark Core中对RDD执行filter算子过滤掉这些key。如果需要每次作业执行时,动态判定哪些key的数据量最多然后再进行过滤,那么可以使用sample算子对RDD进行采样,然后计算出每个key的数量,取数据量最多的key过滤掉即可。

方案实现原理:将导致数据倾斜的key给过滤掉之后,这些key就不会参与计算了,自然不可能产生数据倾斜。

方案优点:实现简单,而且效果也很好,可以完全规避掉数据倾斜。

方案缺点:适用场景不多,大多数情况下,导致倾斜的key还是很多的,并不是只有少数几个。

方案实践经验:在项目中我们也采用过这种方案解决数据倾斜。有一次发现某一天Spark作业在运行的时候突然OOM了,追查之后发现,是Hive表中的某一个key在那天数据异常,导致数据量暴增。因此就采取每次执行前先进行采样,计算出样本中数据量最大的几个key之后,直接在程序中将那些key给过滤掉。
3、解决方案三:提高shuffle操作的并行度
方案适用场景:如果我们必须要对数据倾斜迎难而上,那么建议优先使用这种方案,因为这是处理数据倾斜最简单的一种方案。

方案实现思路:在对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对应的数据量有100万,那么无论你的task数量增加到多少,这个对应着100万数据的key肯定还是会分配到一个task中去处理,因此注定还是会发生数据倾斜的。所以这种方案只能说是在发现数据倾斜时尝试使用的第一种手段,尝试去用嘴简单的方法缓解数据倾斜而已,或者是和其他方案结合起来使用。


4、解决方案四:两阶段聚合(局部聚合+全局聚合)
方案适用场景:对RDD执行reduceByKey等聚合类shuffle算子或者在Spark SQL中使用group by语句进行分组聚合时,比较适用这种方案。

方案实现思路:这个方案的核心实现思路就是进行两阶段聚合。第一次是局部聚合,先给每个key都打上一个随机数,比如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处理数据量过多的问题。接着去除掉随机前缀,再次进行全局聚合,就可以得到最终的结果。具体原理见下图。

方案优点:对于聚合类的shuffle操作导致的数据倾斜,效果是非常不错的。通常都可以解决掉数据倾斜,或者至少是大幅度缓解数据倾斜,将Spark作业的性能提升数倍以上。

方案缺点:仅仅适用于聚合类的shuffle操作,适用范围相对较窄。如果是join类的shuffle操作,还得用其他的解决方案。
5、解决方案五:将reduce join转为map join
方案适用场景:在对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操作,也就不会发生数据倾斜。

方案优点:对join操作导致的数据倾斜,效果非常好,因为根本就不会发生shuffle,也就根本不会发生数据倾斜。

方案缺点:适用场景较少,因为这个方案只适用于一个大表和一个小表的情况。毕竟我们需要将小表进行广播,此时会比较消耗内存资源,driver和每个Executor内存中都会驻留一份小RDD的全量数据。如果我们广播出去的RDD数据比较大,比如10G以上,那么就可能发生内存溢出了。因此并不适合两个都是大表的情况。


6、解决方案六:采样倾斜key并分拆join操作
方案适用场景:两个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导致的数据倾斜,如果只是某几个key导致了倾斜,可以将少数几个key分拆成独立RDD,并附加随机前缀打散成n份去进行join,此时这几个key对应的数据就不会集中在少数几个task上,而是分散到多个task进行join了。具体原理见下图。

方案优点:对于join导致的数据倾斜,如果只是某几个key导致了倾斜,采用该方式可以用最有效的方式打散key进行join。而且只需要针对少数倾斜key对应的数据进行扩容n倍,不需要对全量数据进行扩容。避免了占用过多内存。

方案缺点:如果导致倾斜的key特别多的话,比如成千上万个key都导致数据倾斜,那么这种方式也不适合。
7、解决方案七:使用随机前缀和扩容RDD进行join
方案适用场景:如果在进行join操作时,RDD中有大量的key导致数据倾斜,那么进行分拆key也没什么意义,此时就只能使用最后一种方案来解决问题了。

方案实现思路: * 该方案的实现思路基本和“解决方案六”类似,首先查看RDD/Hive表中的数据分布情况,找到那个造成数据倾斜的RDD/Hive表,比如有多个key都对应了超过1万条数据。 * 然后将该RDD的每条数据都打上一个n以内的随机前缀。 * 同时对另外一个正常的RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都依次打上一个0~n的前缀。 * 最后将两个处理后的RDD进行join即可。

方案实现原理:将原先一样的key通过附加随机前缀变成不一样的key,然后就可以将这些处理后的“不同key”分散到多个task中去处理,而不是让一个task处理大量的相同key。该方案与“解决方案六”的不同之处就在于,上一种方案是尽量只对少数倾斜key对应的数据进行特殊处理,由于处理过程需要扩容RDD,因此上一种方案扩容RDD后对内存的占用并不大;而这一种方案是针对有大量倾斜key的情况,没法将部分key拆分出来进行单独处理,因此只能对整个RDD进行数据扩容,对内存资源要求很高。

方案优点:对join类型的数据倾斜基本都可以处理,而且效果也相对比较显著,性能提升效果非常不错。

方案缺点:该方案更多的是缓解数据倾斜,而不是彻底避免数据倾斜。而且需要对整个RDD进行扩容,对内存资源要求很高。

方案实践经验:曾经开发一个数据需求的时候,发现一个join导致了数据倾斜。优化之前,作业的执行时间大约是60分钟左右;使用该方案优化之后,执行时间缩短到10分钟左右,性能提升了6倍。
8、解决方案八:多种方案组合使用
在实践中发现,很多情况下,如果只是处理较为简单的数据倾斜场景,那么使用上述方案中的某一种基本就可以解决。但是如果要处理一个较为复杂的数据倾斜场景,那么可能需要将多种方案组合起来使用。比如说,我们针对出现了多个数据倾斜环节的Spark作业,可以先运用解决方案一和二,预处理一部分数据,并过滤一部分数据来缓解;其次可以对某些shuffle操作提升并行度,优化其性能;最后还可以针对不同的聚合或join操作,选择一种方案来优化其性能。大家需要对这些方案的思路和原理都透彻理解之后,在实践中根据各种不同的情况,灵活运用多种方案,来解决自己的数据倾斜问题。


9、ShuffleManager发展概述
在Spark的源码中,负责shuffle过程的执行、计算和处理的组件主要就是ShuffleManager,也即shuffle管理器。而随着Spark的版本的发展,ShuffleManager也在不断迭代,变得越来越先进。

在Spark 1.2以前,默认的shuffle计算引擎是HashShuffleManager。该ShuffleManager而HashShuffleManager有着一个非常严重的弊端,就是会产生大量的中间磁盘文件,进而由大量的磁盘IO操作影响了性能。
因此在Spark 1.2以后的版本中,默认的ShuffleManager改成了SortShuffleManager。SortShuffleManager相较于HashShuffleManager来说,有了一定的改进。主要就在于,每个Task在进行shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并(merge)成一个磁盘文件,因此每个Task就只有一个磁盘文件。在下一个stage的shuffle read task拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据即可。


10、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过程,调节该参数可以大幅度提升稳定性。
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
参数说明:该参数用于设置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
默认值:200
参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。
spark.shuffle.consolidateFiles
默认值:false
参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。
调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。

Spark–wordcount:

Spark使用场景

适用于需要多次操作特定数据集的应用场景

sten-sort要慎用,因为之前发现了一些相应的bug。
spark.shuffle.sort.bypassMergeThreshold
默认值:200
参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。
spark.shuffle.consolidateFiles
默认值:false
参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。
调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。




## Spark--wordcount:

## Spark使用场景

适用于需要多次操作特定数据集的应用场景


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值