spark调优和JVM运行流程

算子继续:
MapPartitions:每次处理一个分区数据,常用,经常代替map进行使用,用于调优。调优的作用是减少task的个数。
Map每次只处理一条数据,频繁与Mysql进行交互,非常浪费。
FlatMapFunctionIterator(一批数据)
相当于MySQL中一次插入多条数据,有几个文件块就有几个分区(不大懂),如果分区不合理,有内存溢出的可能性,如果内存溢出,就需要重分区。
RepartitionAndSortWithinPartitions边repartition边sort(同时进行)
重分区:repartition
1. 分区数在任何情况下都是可以指定的rdd.repartition(2) 把分区数由少变多
2. Calese把分区数由多变少
通常一个分区,就对应一个task任务,怎样让分区数由多变少,只需要减少Task需求就能完成,分区是RDD内部并行计算的最小单元
默认有多少的分区数
默认分区数:spark.default.parallelism
1.分区
分区是RDD内部并行计算的一个计算单元,RDD的数据集在逻辑上被划分为多个分片,每一个分片称为分区,分区的格式决定了并行计算的粒度,而每个分区的数值计算都是在一个任务中进行的,因此任务的个数,也是由RDD(准确来说是作业最后一个RDD)的分区数决定。
2.分区的个数
RDD分区的一个分区原则:尽可能使得分区的个数等于集群核心数目
下面我们仅讨论Spark默认的分区个数,这里分别就parallelize和textFile具体分析其默认的分区数:
无论是本地模式、Standalone模式、YARN模式或Mesos模式,我们都可以通过spark.default.parallelism来配置其默认分区个数,若没有设置该值,则根据不同的集群环境确定该值
•本地模式:默认为本地机器的CPU数目,若设置了local[N],则默认为N
•Apache Mesos:默认的分区数为8
•Standalone或YARN:默认取集群中所有核心数目的总和,或者2,取二者的较大值
结论:
1.对于parallelize来说,没有在方法中的指定分区数,则默认为spark.default.parallelism
2.对于textFile来说,没有在方法中的指定分区数,则默认为min(defaultParallelism,2),而defaultParallelism对应的就是spark.default.parallelism。如果是从hdfs上面读取文件,其分区数为文件分片数(128MB/片)
Action
Collect :把数据拉倒driver端进行打印
默认存1G 2G数据1G(不报错,但只保存部分数据)
Take(n)取前几个 takeOrdered(n,[Ordering])
SaveAsSequenceFile 与mapReduce进行对接非常方便。

Shuffle
窄依赖和宽依赖
窄依赖:父RDD的每个分区都被子RDD的一个分区使用
宽依赖:父RDD的分区被多个子RDD的分区所依赖
特例:join宽依赖
有一个特殊的情况,通过hash进行join,因为hash值不同,则指定是分不到同一个分区中,所以就没有到所有的分区中去拉取
名词解释:

Term Meaning
Application 那么就是我们平台开发完代码以后提交的那个任务。他就是application
Application jar 就是我们提交任务的时候运行的那个jar包
Driver program 1)申请资源 2)生成task 发送task
Cluster manager 根据我们不同的运行模式。 可以yarn 可以是Mesos 也是可以local 也可以是standalone
Deploy mode client cluster Driver的位置不一样
Worker node 真正干活,执行我们任务的节点
Executor Executor其实就是一个JVM。我们的代码都需要在里面运行。
Task 代码执行的逻辑
Job (大概这么划分)Spark每遇到一个Action的操作,就会生成一个job任务。
Stage(stage的划分过程) 阶段
Stage
Stage的划分:从最后一个RDD往前推,直到遇到一个宽依赖
同一个stage中Task类型一样(代码逻辑一样),只是针对的数据不一样。
Application
数据倾斜的调优
常见的现象:
绝大多数task执行得都非常快,但个别task执行极慢。比如,总共有1000个task,997个task都在1分钟之内执行完了,但是剩余两三个task却要一两个小时。这种情况很常见。
产生的根本原因
由于整个Spark作业的运行进度是由运行时间最长的那个task决定的,所以在进行shuffle时是非常容易产生数据倾斜的。
Shuffle: distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition
如何得知产生数据倾斜
Yarn-client模式下,观察log可以观察长期运行的是哪一个stage
Yarn-cluster模式/Yarn-client模式,观察SparkWebUI,查看各个stage分配的task数量,已确定那个stage发生的数据倾斜。
之后根据stage划分原理,推算出来发生倾斜的那个stage对应代码中的哪一部分。一种简单的推算原则:
只要看到Spark代码中出现了一个shuffle类算子或者是Spark SQL的SQL语句中出现了会导致shuffle的语句(比如group by语句),那么就可以判定,以那个地方为界限划分出了前后两个stage。
知道了数据倾斜发生在哪里后,查看那个执行了shuffle操作并且导致了数据倾斜的RDD/Hive表,查看一下其中key的分布情况。一下是查看key分布的方式:
1.如果是Spark SQL中的group by、join语句导致的数据倾斜,那么就查询一下SQL中使用的表的key分布情况。
2.如果是对Spark RDD执行shuffle算子导致的数据倾斜,那么可以在Spark作业中加入查看key分布的代码,比如RDD.countByKey()。然后对统计出来的各个key出现的次数,collect/take到客户端打印一下,就可以看到key的分布情况。
3.也可以用sample算子采样或者测试一部分数据,countByKey,查看key的分布情况
数据倾斜的解决方案
1. 使用HiveETL预处理数据
使用场景:
导致数据倾斜的是Hive表。如果该Hive表中的数据本身很不均匀。而且业务场景需要频繁使用Spark对Hive表执行某个分析操作,那么比较适合使用这种技术方案。
方案的优缺点:
优点:实现起来简单便捷,效果还非常好,完全规避掉了数据倾斜,Spark作业的性能会大幅度提升。
缺点:应用场景要求特殊,实际上数据倾斜还是存在的,只不过从spark端转移到了Hive端
2. 过滤少数导致数据倾斜的key
使用场景
如果发现导致倾斜的key就少数几个,而且对计算本身的影响并不大的话,那么很适合使用这种方案。比如99%的key就对应10条数据,但是只有一个key对应了100万数据,从而导致了数据倾斜。
方案的优缺点:
优点:实现简单,而且效果也很好,可以完全规避掉数据倾斜。
缺点:适用场景不多,大多数情况下,导致倾斜的key还是很多的,并不是只有少数几个。
3. 提高shuffle操作的并行度
使用场景:
很简单好用的一种方式,增加shuffle read task的数量,可以让原本分配给一个task的多个key分配给多个task,从而让每个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)。
方案的优缺点:
优点:对于聚合类的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,也就根本不会发生数据倾斜。
缺点:适用场景较少,因为这个方案只适用于一个大表和一个小表的情况。不适合两站大表的操作。
6.采样倾斜key并分拆join的操作
使用的场景
两个RDD/Hive表进行join的时候,如果数据量都比较大,无法采用“解决方案五”,那么此时可以看一下两个RDD/Hive表中的key分布情况。如果出现数据倾斜,是因为其中某一个RDD/Hive表中的少数几个key的数据量过大,而另一个RDD/Hive表中的所有key都分布比较均匀,那么采用这个解决方案是比较合适的。
实现的思路
1对包含少数几个数据量过大的key的那个RDD,通过sample算子采样出一份样本来,然后统计一下每个key的数量,计算出来数据量最大的是哪几个key。
2.然后将这几个key对应的数据从原来的RDD中拆分出来,形成一个单独的RDD,并给每个key都打上n以内的随机数作为前缀,而不会导致倾斜的大部分key形成另外一个RDD。
3.接着将需要join的另一个RDD,也过滤出来那几个倾斜key对应的数据并形成一个单独的RDD,将每条数据膨胀成n条数据,这n条数据都按顺序附加一个0~n的前缀,不会导致倾斜的大部分key也形成另外一个RDD。
4.再将附加了随机前缀的独立RDD与另一个膨胀n倍的独立RDD进行join,此时就可以将原先相同的key打散成n份,分散到多个task中去进行join了。
5.而另外两个普通的RDD就照常join即可。最后将两次join的结果使用union算子合并起来即可,就是最终的join结果。

此时注意打上的随机数n和对数据扩容n倍。因为右侧的RDD扩容了n倍,所以无论左侧的RDD打上n以内的那些前缀,都能和右侧的RDD join上,这是该方法的核心内容。
方案的优缺点:
优点:对于join导致的数据倾斜,如果只是某几个key导致了倾斜,采用该方式可以用最有效的方式打散key进行join。而且只需要针对少数倾斜key对应的数据进行扩容n倍,不需要对全量数据进行扩容。避免了占用过多内存。
本来运行不出来或者说是运行得很慢。 但是我们通过这样的方式,虽然耗费了更多的资源。但是把问题解决了,这也是一种调优。
缺点:如果导致倾斜的key特别多的话,比如成千上万个key都导致数据倾斜,那么这种方式也不适合。
7. 使用随机前缀和扩容RDD进行join
使用的场景
如果在进行join操作时,RDD中有大量的key导致数据倾斜,那么进行分拆key也没什么意义。
因为和6方法的原理非常相似,所以不加赘述。和6方法的主要区别在于:
上一种方案是尽量只对少数倾斜key对应的数据进行特殊处理,由于处理过程需要扩容RDD,因此上一种方案扩容RDD后对内存的占用并不大;而这一种方案是针对有大量倾斜key的情况,没法将部分key拆分出来进行单独处理,因此只能对整个RDD进行数据扩容,对内存资源要求很高。
方案的优缺点:
优点:对join类型的数据倾斜基本都可以处理,而且效果也相对比较显著,性能提升效果非常不错。
缺点:该方案更多的是缓解数据倾斜,而不是彻底避免数据倾斜。而且需要对整个RDD进行扩容,对内存资源要求很高。
8. 多种方案组合使用
但是如果要处理一个较为复杂的数据倾斜场景,那么可能需要将多种方案组合起来使用。比如说,我们针对出现了多个数据倾斜环节的Spark作业,可以先运用解决方案一和二,预处理一部分数据,并过滤一部分数据来缓解;其次可以对某些shuffle操作提升并行度,优化其性能;最后还可以针对不同的聚合或join操作,选择一种方案来优化其性能。
Shuffle调优(更上一层楼)
大多数Spark作业的性能主要就是消耗在了shuffle环节,因为该环节包含了大量的磁盘IO、序列化、网络数据传输等操作。
ShuffleManager发展概述
在Spark的源码中,负责shuffle过程的执行、计算和处理的组件主要就是ShuffleManager,也即shuffle管理器。
ShuffleManager在版本更替中有很大的变动。变得越来越先进,早期在1.2以前的版本,用的是:
HashShuffleManager,该ShuffleManager而HashShuffleManager有着一个非常严重的弊端,就是会产生大量的中间磁盘文件,进而由大量的磁盘IO操作影响了性能。
1.2版本之后,shuffleManager变成了:SortShuffleManager,主要就在于,每个Task在进行shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并(merge)成一个磁盘文件,因此每个Task就只有一个磁盘文件。在下一个stage的shuffle read task拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据即可。
HashShuffleManager运行原理
简诉:
分成两个阶段,shuffleRead阶段和shuffleWrite阶段:
在shuffleWrite阶段,为了下一个stage可以执行shuffle类的算子(比如reduceByKey)(一个stage可以包含多个task),而将每个task处理的数据按key进行“分类”。所谓“分类”,就是对相同的key执行hash算法,从而将相同key都写入同一个磁盘文件中,而每一个磁盘文件都只属于下游stage的一个task。在将数据写入磁盘之前,会先将数据写入内存缓冲中,当内存缓冲填满之后,才会溢写到磁盘文件中去。
下一个stage的task有多少,那么shuffleWrite就会创建多少个磁盘文件。这样创建的磁盘文件的数量会非常的多,所以这只是早期的实现方法,现在已经不适用了。
在shuffleRead阶段,shuffle read,通常就是一个stage刚开始时要做的事情。此时该stage的每一个task就需要将上一个stage的计算结果中的所有相同key,从各个节点上通过网络都拉取到自己所在的节点上,然后进行key的聚合或连接等操作。shuffle read的过程中,每个task只要从上游stage的所有task所在节点上,拉取属于自己的那一个磁盘文件即可。
详情如下图:

HashShuffleManager(未优化)
优化后的HashShuffleManager
spark.shuffle.consolidateFiles=true 开启优化机制
出现shuffleFileGroup的概念,每个shuffleFileGroup会对应一批磁盘文件,磁盘文件的数量与下游stage的task数量是相同的。一个Executor上有多少个CPU core,就可以并行执行多少个task。而第一批并行执行的每个task都会创建一个shuffleFileGroup,并将数据写入对应的磁盘文件内。

  当Executor的CPU core执行完一批task,接着执行下一批task时,下一批task就会复用之前已有的shuffleFileGroup,包括其中的磁盘文件。因此,consolidate机制允许不同的task复用同一批磁盘文件,这样就可以有效将多个task的磁盘文件进行一定程度上的合并,从而大幅度减少磁盘文件的数量,进而提升shuffle write的性能。

如下图:

HashShuffleManager(优化)
SortShuffleManager运行原理
SortShuffleManager的运行机制主要分成两种,一种是普通运行机制,另一种是bypass运行机制。当shuffle read task的数量小于等于spark.shuffle.sort.bypassMergeThreshold参数的值时(默认为200),就会启用bypass机制。
普通运行机制
原理:
数据会先写入一个内存数据结构中,如果是reduceByKey这种聚合类的shuffle算子,那么会选用Map数据结构,一边通过Map进行聚合,一边写入内存;如果是join这种普通的shuffle算子,那么会选用Array数据结构,直接写入内存。(还是reduceByKey处理比较棘手啊!!)会在写入磁盘后判断是否达到阈值,如果达到阈值,则进行数据溢写到磁盘,清除内存数据结构。
在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入磁盘文件。默认的batch数量是10000条,也就是说,排序好的数据,会以每批1万条数据的形式分批写入磁盘文件。
一个task就只对应一个磁盘文件,也就意味着该task为下游stage的task准备的数据都在这一个文件中,因此还会单独写一份索引文件,其中标识了下游各个task的数据在文件中的start offset与end offset。

SortShuffleManager(普通运行机制)
bypass运行机制
bypass运行机制的触发条件如下:
shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值。
不是聚合类的shuffle算子(比如reduceByKey)。
该过程的磁盘写机制其实跟未经优化的HashShuffleManager是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的HashShuffleManager来说,shuffle read的性能会更好。
而该机制与普通SortShuffleManager运行机制的不同在于:
第一, 磁盘写机制不同;
第二, 不会进行排序。也就是说,启用该机制的最大好处在于,shuffle write过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。
原理图如下:

SortShuffleManager(byPass运行机制)
Shuffle的相关参数调优

Spark的内存模型
以上图片内的spark.shuffle.memoryFraction和spark.storage.memoryFraction的分配比例不是固定的,具体还要根据使用场景进行确定。
在分配资源的时候,最好的情况下,出来的结果就是1个cpu core 对应2-3个task. task: 分区数有关,有几个分区就有几个task. 1) 分区数是可以设置的,也就是说task就是我们可以控制的。 2)如果不设置,那么我们读取的HDFS上的数据。默认一个文件块就是一个分区 也就意味着一个文件块就对应一个task. 典型配置样例:
/bin/spark-submit \ –master yarn-cluster \ –num-executors 100 \ –executor-memory 6G \ –executor-cores 4 \ –driver-memory 1G \ –conf spark.default.parallelism=1000 \ –conf spark.storage.memoryFraction=0.5 \ –conf spark.shuffle.memoryFraction=0.3 \
JVM(JavaVirtualMachine):
JVM运行流程
我们一旦要运行这一段代码,对代码进行编译。产生class文件。
接着开始启动JVM。类加载器会从类加载路径里面去加载带有main方法class文件。会把
这个文件信息存放到运行时区域里面的方法区。
接着就开始执行里面的main方法。执行main方法首先遇到的这如下代码:
final Student student = new Student(“aura”);
此时就是会创建Student对象 ,对象创建完了以后用student 这个变量命名。
细节:
1)JVM会直接到方法区里面去查找Student 的类的信息。这个时候没有发现Student类。
没有发现就会用类加载器去加载这个类的class信息。
2)接下来在方法区里面就有这个Student类的信息了。这个时候就会在相对应的heap (堆)中给他分配内存。
并且堆内存中Student这个对象指向方法区中的Student的引用(其实也好想,不然以后怎么调用Student的方法呢?)
3) 让 statck(栈)里面的student 跟 heap 里面的Student 建立关系。
Jvm架构

架构图
方法区和栈由所有的线程所共享,其他都由线程所私有。栈的声明周期和线程一致。方法区又叫非堆,也叫永久区。奇怪的是方法区是存储在堆内部上的,可是为什么又叫非堆?。
对于一个对象而言,栈中存储的是对象的引用,堆中存储的是真正的对象,方法区中存储的是类的信息和方法等的信息。
以前的永久带(默认是64M),用来存放class,method等信息,在jdk1.8后就没有了,取而代之的是元空间(MetaSpace),元空间不在JVm中,而直接使用计算机的内存。
为什么要用元空间代替永久区?
1)类以及发放的信息比较难确定其大小,因此对于永久区的指定比较困难,大小容易导致永久带溢出,太大容易导致老年代溢出。
2)永久带会给GC带来不需要的复杂度,并且回收效率偏低
3)Oracle可能会将HotSport和JRockit合二为一商业原因才是重点
-XX:MaxMetaspaceSize 指定元空间的大小的语句,一定要设置一下,因为元空间不受java虚拟机控制,所以为了防止元空间占有很大的内存,所以要设置一下。

JVM中堆的模型
参数的设置:(常用的标注为红色)
Trace跟踪参数
1:打印GC的简要信息
-XX:+PrintGC
-verbose:gc 注:两个一样
2:打印GC的详细信息
-XX:+PrintGCDetails
3:打印GC发生的时间
-XX:+PrintGCDateStamps
4:指定GC的log位置,以文件输出
-Xloggc:log/gc.log
5:每一次GC后都打印堆信息
-XX:+PrintHeapAtGC
堆参数的配置
-Xms :初始堆大小 默认是物理内存的1/64
-Xmx :最大堆大小 默认是物理内存的1/4
-Xmn:新生代的大小。默认整个堆的3/8
-XX:NewRatio=n: 设置年老代和年轻带的比值。如:为3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3, 表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n :设置持久代大小 (永久带 1.7前有)
-X:MaxMetaspaceSize(元空间)
-XX:+HeapDumpOnOutOfMemoryError
参数表示当JVM发生OOM时,自动生成DUMP文件。
-XX:HeapDumpPath=${目录}。
栈和元空间
栈参数
-Xss264k 元空间参数
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
垃圾
简单的说就是内存中已经不再被使用到的空间就是垃圾
垃圾计数法
引用计数法
1:给对象添加一个引用计数器
2:优点:实现简单,效率高
3:缺点:不能解决对象之间循环引用的问题
对象间循环引用的例子:
public class MyObject{ public Object ref=null; public static void main(String[] args){ MyObject myObject1=new MyObject(); MyObject myObject2=new MyObject(); myObject1.ref=myObject2 myObject2.ref=myObject1 myObject1=null; myObject2=null; } }
跟搜索法
从根节点(GC Roots)向下搜索对象节点,搜索走过的路径称为引用链, 当一个对象到根之间没有连通的话,则该对象不可用。
Stop-the-word
STW是java中一种全局暂停的现象,多半由于GC引起。所谓全局停顿,就是所有Java代码停止运行
native代码可以执行,但不能和JVM交互。
其危害是服务长时间停止,没有响应:对于HA系统可能引起主备切换,严重危害生产环境
串行,并行,并发的收集
1:串行收集:GC单线程内存回收、会暂停所有的用户线程
2:并行收集:多个GC线程并发工作,此时用户线程是暂停的
3:多个线程和GC线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程
4:Serial是串行的,Parallel是并行的,CMS是并发的
垃圾收集算法
标记清除法

你可能感兴趣的:(Spark知识笔记)