spark调优的相关总结

1、reduce task OOM?

         增大reduce端的聚合操作的内存比例

         增大executor memory 内存大小 --executor-memory

         减少reduce task每次拉取的数据量  设置spark.reducer.maxSizeInFlight参数

2、在shuffle阶段executor挂掉?

     分析原因:(1)map task阶段的所运行的executor的内存不足,导致executor挂掉了。导致executor里面的blockmanager挂掉,不能和connctionmanager建立连接,

2.executor并没有挂掉

      2.1 BlockManage之间的连接失败(map task所运行的executor正在GC)

      2.2建立连接成功,map task所运行的executor正在GC

3.reduce task向Driver中的MapOutputTracker获取shuffle file位置的时候出现了问题

下面有几个重要的参数的调节:

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%的提升。

            错误:reduce oom

            reduce task去map拉数据,reduce 一边拉数据一边聚合   reduce段有一块聚合内存(executor memory * 0.2)

            解决办法:1、增加reduce 聚合的内存的比例  设置spark.shuffle.memoryFraction

            2、 增加executor memory的大小  --executor-memory 5G

            3、减少reduce task每次拉取的数据量  设置spark.reducer.maxSizeInFlight  24m

        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

            参数说明:该参数用于设置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%。

1、资源调优

在开发过程中,增加或者分配更多的资源对于任务的执行的效率是显而易见的。但是资源本身是受到限制的,那么我们该如何解决这个问题呢?

一般在开发的时候,我们的提交任务的脚本

Spark-submit.sh 内容如下:

#!/bin/bash

/opt/modules/spark-1.6.1-bin-2.5.0-cdh5.3.6/bin/spark-submit \

--class com.hypers.sparkproject.spark.session.UserVisitSessionAnalyzeSpark \

--num-executors 3 \                        --配置executor的数量

--driver-memory  1024M \                   --配置driver的内存,影响不大

--executor-memory 2G \                 --配置每个executor的内存大小

--executor-cores 3 \ --Spark standalone and YARN only --配置每个executor的cpu核数

/usr/loacl/recommend-1.0-SNAPSHOT.jar \

分配多少合理呢?

第一种:Spark Standalone即Spark运行在自己的分布式框架时,需要知道每台机器能够使用的内存,CPU核数,假如每台机器能够使用4G内存和2个CPU核数,一共20台机器,那么就可以executor数量设置20,每个executor内存设置4G,每个executor设置2 CPU core

第二种: Yarn 当Spark运行在yarn上时,需要查看资源队列有多少资源,假如资源队列有500G内存,100个CPU core可用,那么就可以设置50个executor,每个executor内存设置10G,每个executor设置2个CPU core

2、设置并行度

设置task的并行度

保证并行度与你设置的资源相匹配,不至于会浪费资源。例如10个executor,每个内存是4GB, 2个CPU, 10个task, 总的cpu20个,可以并行运行20个task。现在假设你要让每个executor都运行一个task. 这样的话,其实每一个executor上面就有一个空闲的cpu, 导致并行度与设置的任务不相符合,就浪费掉了资源。

那么根据官方的建议:task的数量设置成spark application的cpu总数的2-3倍。

3、重构RDD以及RDD序列化

原则一:尽量去复用RDD

原则二:公共RDD进行持久化到内存或者磁盘上面,那么之后对于这个RDD的操作都是直接取的持久化的数据

原则三:持久化数据进行序列化的操作

原则四:持久化 + 双副本机制

为了数据的高可靠性,而且内存充足,可以使用双副本机制进行持久化 
持久化的双副本机制,持久化后的一个副本,因为机器宕机了,副本丢了,就还是得重新计算一次;持久化的每个数据单元,存储一份副本,放在其他节点上面;从而进行容错;一个副本丢了,不用重新计算,还可以使用另外一份副本。

  1. 广播变量:将大变量广播出去而不是直接使用的。

为什么要用Broadcast

当进行随机抽取一些操作,或者从某个表里读取一些维度的数据,比如所有商品品类的信息,在某个算子函数中要使用到,加入该数据大小为100M,那么1000个task将会消耗100G的内存, 集群损失不可估量

Broadcast的原理

默认的情况下,每个task执行的算子中,使用到了外部的变量,每个task都会获取一份变量的副本,所以会消耗很多的内存,进而导致RDD持久化内存不够等情况,大大影响执行速度

广播变量,在driver上会有一份初始的副本,task在运行的时候,如果要使用广播变量中的数据,首先会在自己本地的Executor对应的BlockManager中尝试获取变量副本,并保存在本地的BlockManager中,此后这个Executor上的所有task,都会直接使用本地的BlockManager中的副本, Executor的BlockManager除了从driver上拉取,也可能从其他节点的BlockManager上拉取变量副本,距离越近越好.

总而言之: 广播变量的好处不是每一个task一份变量副本,而是变成每个节点的executor才一份副本,这样的话就可以变量产生的副本大大减少。

4、Kryo序列化的使用

默认是使用了java的序列化机制ObjectInputStream/ObjectoutputStream,这种序列化的方式简单,并且便于处理。但是效率不高,而且在进行RDd的持久化操作的时候,内存占比相对比较大。那么利用kryo序列化的好处:

  1. 算子函数中使用的外部变量,在经过kryo序列化之后,会优化网络传输的性能,优化集群中内存的占用和消耗.
  2. 持久化RDD的时候,优化内存的占用和消耗
  3. 优化shuffle操作的网络传输的数据

设置kryo 并且要注册自动义的类

set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")

.registerKryoClasses(new Class[]{CategorySortKey.class})

 

5、Fastutil库

fastutil 是扩展了java标准集合框架(Map,List,Set,HashMap,ArrayList,HashSet)的类库,提供了特殊类型的Map,Set,List和queue,fastutil能够提供更小的内存占用,更快的存取速度,fastutil也提供了64位的array、set和list,以及高性能快速的,以及实用的IO类,来处理二进制和文本类型的文件;

fastutil最新版本要求Java 7以及以上版本

 

Spark中fastutil应用场景:

1.如果算子函数使用了外部变量,那么可以用三步来优化:

a.使用Broadcast广播变量优化,

b. 使用Kryo序列化类库优化,提升性能和效率,

c.如果外部变量是某种比较大的集合,可以使用fastutil改写外部变量

2.在算子函数中,如果要创建比较大的Map.List等集合,可以考虑将这些集合类型使用fastutil类库重写。

Maven库:

<dependency>

    <groupId>fastutil</groupId>

    <artifactId>fastutil</artifactId>

    <version>5.0.9</version>

</dependency>

6、数据本地化等待时长

Spark在Driver上面对application的每一个stage的task进行分配之前,都会计算每个task分配的是RDd的那个partitoion的数据呢?

Spark的task分配算法优先会希望每个task正好分配到它要计算的数据所在的节点,这样就避免了网络间传输数据。

但是,task可能没有机会分配到它的数据所在的节点,因为可能计算资源和计算能力都满了,这种情况下,                                   Spark会等待一段时间,过了这个时间,才会选择一个比较差的本地化级别,比如将这个task分配到相邻的一个节点上,这个时候肯定发生网络传输,会通过一个getRemote()方法,通过TransferService(网络数据传输组件)从数据所在节点的BlockManager中获取数据,上述中的一段时间即为本地化等待时长。那么如何调整这个等待时间呢?

PROCESS_LOCAL:进程本地化,代码和数据在同一个进程中,也就是在同一个executor中;计算数据的task由executor执行,数据在executor的BlockManager中;性能最好

NODE_LOCAL:节点本地化,代码和数据在同一个节点中;比如说,数据作为一个HDFSblock块,就在节点上,而task在节点上某个executor中运行;或者是,数据和task在一个节点上的不同executor中;数据需要在进程间进行传输

NO_PREF:对于task来说,数据从哪里获取都一样,没有好坏之分

RACK_LOCAL:机架本地化,数据和task在一个机架的两个节点上;数据需要通过网络在节点之间进行传输

ANY:数据和task可能在集群中的任何地方,而且不在一个机架中,性能最差。

如果大多都是PROCESS_LOCAL,那就不用调节了

如果是发现,好多的级别都是NODE_LOCAL、ANY,那么最好就去调节一下数据本地化的等待时长

调节完,应该是要反复调节,每次调节完以后,再来运行,观察日志

看看大部分的task的本地化级别有没有提升;看看,整个spark作业的运行时间有没有缩短

调节方法:

spark.locality.wait,默认是3s;可以调节为6s,10s

默认情况下,下面3个的等待时长,都是跟上面那个是一样的,都是3s

spark.locality.wait.process

spark.locality.wait.node

spark.locality.wait.rack

new SparkConf()

  .set("spark.locality.wait", "10")

Young/New Generation

年轻代与Spark调优息息相关,所以这里单独拿出来讲解

所有新生成的对象首先都是放在年轻代中,年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象

大部分对象在Eden区生成,当Eden区满时,还存活的对象将被复制到Survivor区中(两中的一个),当这个Survivor区满的时候,此区存放的对象会被放在另一个Survivor区中,当另一个Survivor也满的时候,从第一个Survivor复制过来的还存活的对象将被复制到老年代中,Survivor的两个区是对称的,没有先后关系,所以同一个Survivor取中可能存在从Eden复制过来的对象和从另一个Survivor复制过来的对象,而且Survivor总有一个是空的,而且可以配置多余两个

7、Spark的JVM调优

降低cache操作的内存占比 降低 缓存的大小占比

Spark task执行算子RDD函数时会生成大量对象,这些对象会被放入年轻代中,当年轻代内存比较小时,会导致年轻代中Eden区和Survivor区频繁内存溢满,导致频繁的minor GC,而频繁的minorGC或导致一些存活的短声明周期(其实就是在后面用不到的对象)对象直接放入老年代中,而当老年代内存溢满是,则会导致Full GC 。full gc / minor gc,无论是快,还是慢,都会导致jvm的工作线程停止工作,简而言之,就是说,gc的时候,spark停止工作了。等着垃圾回收结束。

总而言之,上面的情况都是由内存不足引起的即内存不充足的时候,问题:

1、频繁minor gc,也会导致频繁spark停止工作

2、老年代囤积大量活跃对象(短生命周期的对象),导致频繁full gc,full gc时间很长,短则数十秒,长则数分钟,甚至数小时。可能导致spark长时间停止工作。

3、严重影响咱们的spark的性能和运行的速度。

如何增大内存?

Spark中,堆内存又被划分成了两块,一块是专门用来给RDD的cache,persist操作进行RDD缓存用的,另一块就是用来给Spark算子函数用的,存放函数中自己创建的对象

默认情况下,给RDD的cache操作的内存占比是0.6,即百分之六十的内存用来给RDD做缓存用,但其实RDD并不需要这么大的内存,我们可以通过WEbUI查看每个stage中每个task运行的时间,GC时间等来判断是否发生了频繁的minorGC和fullGC,从而来调低这个比例   

调节方法:

spark.storage.memoryFraction0.6 -> 0.5 -> 0.4 -> 0.2 默认是0.6

spark.shffule.memoryFraction 表示的是shuffle过程中一个task拉取上一个stage的task的数据进行聚合操作的能够占用的内存默认是0.2。在shuffle的时候,如果内存使用超过20%,那么数据会溢出到磁盘,这样就降低了效率。

这个参数调优建议:

   如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

 

资源参数的调优,没有一个固定的值,需要根据自己的实际情况(包括Spark作业中的shuffle操作数量、RDD持久化操作数量以及spark web ui中显示的作业gc情况)来灵活的调优。

Executor堆外内存

当Spark处理超大数据量时(数十亿,百亿级别),executor的堆外内存可能会不够用,出现shuffle file can’t find, task lost,OOM等情况

默认情况下,这个堆外内存是300M,当运行超大数据量时,通常会出现问题,因此需要调节到1G,2G,4G等大小

调节方法必须在spark-submit提交脚本中设置而不能在程序中设置

--conf spark.yarn.executor.memoryOverhead=2048

GC引起的连接等待时长(调大超时时间)

Spark在处理超大数据量时,task可能会创建很大很多的对象,频繁的让JVM内存溢满,导致频繁GC,而前面提到过executor获取数据优先的从本地关联的blockmanager获取,如果没有的话,会通过transferService去远程连接其他executor的blockmanager,如果正好碰到那个executor垃圾回收GC操作,,那么程序就会卡住,spark默认网络连接时长是60s,当超过60s没有获取到数据,则直接宣告任务失败,也有可能DAGscheduler反复提交几次stage,TaskScheduler反复提交task,则会大大影响spark运行速度,所以可以考虑适当调节等待时长。

调节方式同调节堆外内存一样,必须在提交spark程序的脚本中设置

--conf spark.core.connection.ack.wait.timeout=300 executor没有挂,建立连接的时候发生了GC

那么在拉取数据的时候遇到GC

--conf spark.shuffle.io.maxRetries  增加重试次数
--conf spark.shuffle.io.retryWait  增加重试间隔时间

spark.network.timeout (默认120s)该参数对应到一下五个参数spark.core.connection.ack.wait.timeout, spark.storage.blockManagerSlaveTimeoutMs,

spark.shuffle.io.connectionTimeout,

spark.rpc.askTimeout

spark.rpc.lookupTimeout

8、shuffle调优

什么情况下面会发生shuffle操作呢? 尽量使用高性能的算子(但是有要注意可能会发生OOM)

在Spark中,主要有以下几个算子

- groupByKey : 把分布在各个节点上的数据中的同一个key对应的value都集中到一块儿,集中到集群中的一个节点中,也即是集中到一个节点的executor的一个task中

- reduceByKey : 算子函数对values集合进行reduce操作,最后生成一个value

- countByKey : 在一个task中获取同一个key对应的所有value,然后计数,统计总共有多少value

- join : 两个RDD,Key相同的两个value都集中到一个executor的task中

Shuffle操作的过程:

在一个shuffle过程中,前半部分stage中,每个task都会创建后半部分stage中相同task数量的文件,比如stage后半部分有100个task,那么前半部分的每个task都会创建100个文件(先写入到内存缓冲中,后溢满写入到磁盘),会将同一个key对应的values写入同一个文件中,shuffle后半部分的stage中的task,每个task都会从各个节点的task创建其中一份属于自己的那份文件中,拉取属于自己的key-value对,然后task会有一个内存缓冲区,然后调用HashMap进行key-values的聚合,最终调用我们定义的聚合函数来进行相应的操作。

Shuffle调优合并map端输出的文件

默认情况下spark是不开启map端输出文件的合并的机制的,当spark在分批次执行task的时候,task每次都会创建新的文件,而不会共享这些数据的,所以开启合并机制能够提升性能:

new SparkConf().set("spark.shuffle.consolidateFiles", "true")

设置合并机制之后:

第一个stage,并行运行2个task,运行这两个task时会创建下一个stage的文件,运行完之后,会运行下一批次的2个task,而这一批次的task则不会创建新的文件,会复用上一批次的task创建的文件

第二stage的task在拉取上一个stage创建的文件时就不会拉取那么多文件了,而是拉取少量文件,每个输出文件都可能包含了多个task给自己的map端输出。

shuffle调优之map端内存缓冲和reduce内存占比

默认情况下:

每个task的内存缓冲为32kb,reduce端内存占比为0.2(即默认executor内存中划分给reduce task的微20%)

所以在不调优的情况下,如果map端task处理的比较大,内存不足则溢满写入磁盘

比如:

每个task就处理320kb,32kb,总共会向磁盘溢写320 / 32 = 10次。

每个task处理32000kb,32kb,总共会向磁盘溢写32000 / 32 = 1000次。

同理,ruduce端也一样

何时调优?

通过Spark UI查看shuffle磁盘的read和write是不是很大,如果很大则应相应调优

如何调优?

spark.shuffle.file.buffer : 32kb -> 128kb

spark.shuffle.memoryFraction: 0.2 -> 0.3

9、spark算子调优

MapPartitions提升Map类操作性能

spark中,最基本的原则是每个task处理一个RDD的partition

如果是普通的Map,假如一个partition中有一万条数据,那么map中的function就要执行和计算一万次,但是使用MapPartitions操作之后,一个task只会执行一次function,function一次接收了所有partition数据,性能比较高

MapPartitions的缺点:

如果是普通的Map,一条一条处理数据,当出现内存不够的情况时,那么就可以将已经处理掉的数据从内存里面垃圾回收掉,所以普通map通常不会出现OOM情况

如果是MapPartitions,对于大量数据来说,如果一个partiton数据有一百万条,一次性传入function之后,可能导致内存不足,但是又没办法腾出空间,直接就导致了内存溢出,OOM

所以,当使用MapPartitons算子时,要估算每个partiton的数据能不能一下子缓存到分配给executor的内存中,如果可以,就是用该算子,对性能有显著提升

所以对于map和mapPartition的区别使用。看具体场景

GroupByKey和ReduceByKey的区别

也是从存在效率问题的。

10、Spark算子调优之filter过后使用coalesce减少分区数量

Spark程序经过filter之后会出现以下两种情况的:

  1. 导致每个partition里面的数据不一样的,有的很多,而有的数据很少,导致在 后面的操作会数据倾斜的。
  2. 由于partition的数据量减少,在后面的计算还是按照跟partition的task相同的数据量,这样就导致了资源的浪费。

12. Spark算子调优之foreachPartition优化写数据库性能

foreach是对每条数据进行处理的,task对partition中的每一条数据都会执行function操作,如果function中有写数据库的操作,那么有多少条数据就会创建和销毁多少个数据库连接,这对性能的影响很大

在生产环境中,通常都是使用foreachPartition来写数据库的,使用了该算子之后,对于用户自定义的function函数,就调用一次,一次传入一个partition的所有数据,这里只需创建一个数据库连接,然后向数据库发送一条sql语句外加多组参数即可,但是这个时候要配合数据库的批处理

同样,该算子在超大数据量面前同样会出现OOM情况

13. Spark算子调优之使用repartition解决SparkSQL低并行度性能问题

通常情况下,在上面第二条的并行度调优时,使用spark.default.parallelism来设置并行度,那么这个设置在什么地方有效,什么地方无效?

当程序中没有使用SparkSQL,那么整个sparkapplication的所有stage的并行度都是设置的这个参数,除非使用了coalesce算子缩减过partition.

当程序中使用了SparkSQL,那么SparkSQl的 stage的并行度无法设置,因为SparkSQL会默认的根据Hive表对应的hdfs文件的block,自动设置SparkSQL那个stage的并行度,所以这就导致出现了用少量task来处理复杂逻辑的情况, 这种情况下,需要使用repartition来设置SparkSQL的并行度,即对从Hive中读取出来的RDD,使用repartiton重新分区为预期的数量来设置并行度。

14、spark算子reduceBykey进行本地的聚合操作

在map端,给下个stage的每个task创建的输出文件中,写数据之前,会进行本地的combiner操作,也就是说,对每一个key,对应的values都会执行用户自定义的算子函数,比如+_,当进行了这个combiner操作之后,减少了数据量,也即是减少了磁盘IO,同时减少了网络传输,对性能有明显提升,所以,在实际的项目中,能用reduceByKey实现的就尽量用该算子实现。

 

15、数据倾斜

1、数据倾斜的现象:

数据倾斜是Spark中极其影响性能的现象,它甚至能导致程序无法跑完,更不用提性能调优什么的了

数据倾斜如何产生的?

在shuffle操作的时候,是按照key来进行value的数据的输出,拉取和聚合的,同一个key的values,一定是分配到同一个reduce task进行处理的,假如多个key对应的value一共有90万条数据,但是可能某条key对应了88万条,其他key最多也就对应数万条数据,那么处理这88万条数据的reduce task肯定会特别耗费时间,甚至会直接导致OOM,这就是所谓的数据倾斜

  1. 解决方案:

1.聚合源数据

Spark的数据源通常情况下都是来自于Hive表,(HDFS或其它大数据分布式系统),而Hive本身就是适合做离线数据分析的,所以说通常要变换一下思路,能在Hive中做聚合的,通常就可以跑定时任务在Hive中做聚合,最终spark拿到的只是每个key对应的一个value值,然后就可以使用map来对这个特殊的value串来处理,省去了groupByKey的过程

 

2.过滤掉导致倾斜的key

这种情况只适合用户能够接受摒弃某些特殊的数据,比如大部分key都对应了几十万条,而少数key只对应了几十条,那么直接在Hive中过滤掉这些key就从源头上避免了数据倾斜

 

3.提高shuffle操作reduce端并行度

提高shuffle操作reduce端并行度会有更多task来处理数据,那么每个task处理的数据会相对来说更少一些

如何操作?

给shuffle算子传递进去一个参数,即一个数字,这个数字就代表了shuffle操作时reduce端的并行度,然后在进行shuffle操作的时候,就会对应创建指定数量的reduce task

4.使用随机key实现双重聚合

这个场景主要用于reduceByKey,groupByKey,而非join

主要原理就是:

在第一轮聚合时,对key进行打散,将原先一样的key,变成不一样的key,相当于将每个key分组,然后针对key的多个分组,进行key的局部聚合,接着再去掉key的前缀,然后对所有key进行全局聚合,这种方案对解决这两个算子产生的数据倾斜有比较好的效果

5.join算子操作的数据倾斜解决方案

将reduce join转换为map join

说明:

普通的join,肯定是要走shuffle,那么,既然走shuffle,那么普通的join肯定是reduce join, 即将所有相同的key对应的values,聚合到一个task中,再进行join操作

那么如何将reduce join转换为map join?

当两个RDD要进行join时,其中一个RDD是比较小的,那么就可将该小数据量RDD广播broadcast出去,该RDD数据将会在每个executor的blockmanager中驻留一份数据,然后在map操作中就可以使用该数据,这种方式下,根本就不会发生shuffle操作,从而从根本上杜绝了数据倾斜

总结丰巢的面试问道了spark2,0参数调优

简历上面淘宝双十一回头客预测的特征工程的问题:

(1)首先对于test.csv  和train.csv 的数据对于其中的空缺值进行处理,空缺值的数据直接过滤掉。

数据的特征包括:user_id, age_range

1表示年龄<18,2表示年龄在[18,24],3表示年龄在[25,29],4表示年龄在[30,34],5表示年龄在[35,39],6表示年龄在[40,49],7和8表示年龄>=50,0和NULL则表示未知

gender: 0表示女性  1表示男性

merchant_id 表示商家id

label :表示是否是回头客,0表示不是回头,1表示是回头;

 (2)把test.csv里面的label这一列的数据-1全部替换成1,

(3)构建向量,利用ML的api

Val parts = line.split(“ ”)

labeledPoint(parts(4).toDouble, Vectors(parts(0), parts(1), parts(2)))

 

 

 

 

 

 

关于spark的jvm模型架构,Spark 1.6.x 以前是基于静态固定的JVM内存使用架构和运行机制,如果你不知道 Spark 到底对 JVM 是怎么使用,你怎么可以很有信心地或者是完全确定地掌握和控制数据的缓存空间呢,所以掌握Spark对JVM的内存使用内幕是至关重要的。很多人对 Spark 的印象是:它是基于内存的,而且可以缓存一大堆数据,显现 Spark 是基于内存的观点是错的,Spark 只是优先充分地利用内存而已。如果你不知道 Spark 可以缓存多少数据,你就误乱地缓存数据的话,肯定会有问题。所以说我们唏嘘要先知道到底可以缓村多少的数据 呢?

在数据规模已经确定的情况下,你有多少 Executor 和每个 Executor 可分配多少内存 (在这个物理硬件已经确定的情况下),你必须清楚知道你的內存最多能够缓存多少数据;在 Shuffle 的过程中又使用了多少比例的缓存,这样对于算法的编写以及业务实现是至关重要的!!!

Jvm架构剖析

包括了:本地方法栈:在进行递归的时候用的。

Java栈:Stack 区属于线程私有,高效的程序一般都是并发的,每个线程都会包含一个 Stack 区域,Stack 区域中含有基本的数据类型以及对象的引用,其它线程均不能直接访问该区域;Java 栈分为三大部份:基本数据类型区域、操作指令区域、上下文等;

程序计数器:在线程切换的时候计数

Java heap: 存储全部的Object对象实例,该区域是线程共享的,在垃圾回收主要是回收堆中的对象;

方法区:又名静态成员区域,包含整个程序的 class、static 成员等,类本身的字节码是静态的;它会被所有的线程共享和是全区级别的;

 

 

16、算法:

案例电商预测回头客:利用SVM算法

首先说明一下特征工程:

关于特征工程的总结:

      特征工程一般包括数据的预处理  特征提取  特征选择

     

      缩放特征值(归一化):将浮点特征值从自然范围(如 100 到 900)转换为标准范围(如 0 到 1)。特征集包含多个特征时,缩放特征可以加快梯度下降的收敛过程,并可以避免 NaN 陷阱。特征缩放的方法一般为 scaled-value = (value-mean)/stddevscaled−value=(value−mean)/stddev

      处理极端离群值,如取对数、限制最大最小值等方法

      分箱(离散化)

      填补遗漏值

      移除重复样本、不良标签、不良特征值等

      平滑

      正则化

     

      特征选择是一个从给定的特征集合中选择与当前学习任务相关的特征的过程。

      特征选择中所谓的 “无关特征” 是指与当前学习任务无关,比如有一类特征称为 “冗余特征” 可以从其他特征中推演出来,

  它在很多时候是不起作用的,并且会增加学习过程的负担。

由于案例已经给出了train.csv 和test.csv数据集  包含的字段包括了字段说明:user_id, age_range(1-8的取值,总共是划分了8个区间) gender: 0表示女性; 1表示男性。 Label表示是否是回头客,0 表示不是回头客; 1表示是回头客;-1表示超出了范围。

  1. 接下来利用脚本对于train.csv和test.csv里面的空缺值进行过滤

Predeal_test.sh

   #!/bin/bash

   Infile=$1

   Outfile=$2

   awk -F “,” ‘BEGIN{id=0;} {

   if($1 && $2 && $3 && $4 && !$5) {

       id = id +1;

print $1”,”$2”,”$3”,”$4”,”1  #输出到文件 对应的字段1-4 第五个字段改为1

if(id == 10000) {

   exit

}

}

}’  $infile > $outfile

以上的脚本用来处理test.csv数据集

Predeal_train.sh

#!/bin/bash

   Infile=$1

   Outfile=$2

   awk -F “,” ‘BEGIN{id=0;} {

   if($1 && $2 && $3 && $4 && ($5 != -1)) {

       id = id +1;

print $1”,”$2”,”$3”,”$4”,”$5  #输出到文件 对应的字段1-4 第五个字段改为1

if(id == 10000) {

   exit

}

}

}’  $infile > $outfile

处理train.csv数据集,并且把数据集的header去除掉

Sed -I ‘1d’ train.csv

完了利用spark MLlib里面的SVM算法

先是构建模型:

Val trainData = sc.textFile(“hdfs://master:9000/data”)

Val testData = sc.textFile(“hdfs://master:9000/data”)

Val train_RDD = trainData.map(line => {

   Var parts = line.split(“,”)

   labeledPoint(parts(4).toDouble, Vectors(parts(0).toDouble, …)) 

})

Val test_RDD = testData.map(line => {

   Var parts = line.split(“,”)

   labeledPoint(parts(4).toDouble, Vectors(parts(0).toDouble, …)) 

})

val numIterations = 1000

val model = SVMWithSGD.train(train, numIterations) 

利用随机梯度下降算法来求解SVM的参数。

模型的评估利用了

利用了二分类的评估器的areaunderRoc  areaubderPR的两种方法进行对比。

下面先介绍一下关于ROC曲线和PR曲线的相关内存

首先明确几个概念:

TP(true precesion) 预测为正样本,实际为正样本的特征数

FP(false precesion) 预测为正样本,实际为负样本的特征数

TN(true negative) 预测为负样本, 实际为负样本的特征数

FN(false negative) 预测为负样本, 实际为正样本的特征数

准确率precision

召回率 recall

F1值 :是precision 和recall的调和均值

F1=precisionprecision+recall×2

以P(查准率)为纵轴,R(查全率)为横轴作图,就得到了P-R曲线P-R图直观的显示出学习器在样本总体上的查全率、查准率,在进行比较时,若一个学习器的P-R曲线被另一个完全包住,则可断言后者优于前者,如图1,A优于C;如果两个学习器的P-R曲线发生了交叉,如A和B,则难以一般性的断言两者孰优孰劣,只能在具体的P或R条件下进行比较。然而,在很多情形下,人们往往仍希望把学习器A和B比个高低,这时一个比较合理的判断依据是比较曲线下面积的大小,它在一定程度上表征了学习器在P和R上取得相对“双高”的比例,但这个值不太容易估算,因此人们设计了一些综合考虑P和R的度量。平衡点(BEP)就是这样一个度量,是P=R时的取值,基于BEP,可任务A优于B。

以召回率(真正率)为y轴,以特异性(假正率)为x轴,我们就直接得到了RoC曲线。从召回率和特异性的定义可以理解,召回率越高,特异性越小,我们的模型和算法就越高效。也就是画出来的RoC曲线越靠近左上越好。如下图左图所示。从几何的角度讲,RoC曲线下方的面积越大越大,则模型越优。所以有时候我们用RoC曲线下的面积,即AUC(Area Under Curve)值来作为算法和模型好坏的标准。

P-R曲线

Roc曲线

 

下面总结SVm和LR的区别?

Loss function 不同,log loss  hinge loss 

LR是概率模型(最大似然估计求L(θ), 取对数 )  SVM是向量模型

两个都是判别式模型

LR求解过程中每一个数据都会对超平面产生影响的,则LR的解受到数据的分布的影响,在实际应用中,如果数据维度很高,LR模型都会配合参数的L1 regularization(降维)。那么数据量大的话适合用LR, 数据量小的话适合用SVM.

从数据分布对模型的影响来看:svm只考虑局部的边界附近的点(最小距离最大化间隔的数据点);LR要考虑所有的样本点;所以SVm模型直接依赖于数据的分布,分类超平面不受某一类点的影响。LR是利用所有样本的概率最后在最大似然估计因此会受到数据分布的影响。LR对应不平衡的数据在训练前要balance;所以SVM对于不平衡性更鲁棒性。

两种方法都是 常见的分类算法,其中心思想都是增加对分类影响较大的数据点的权重,减少与分类关系较小的数据点的权重。

SVM的处理方法是只考虑support vectors,也就是和分类最相关的少数点,去学习分类器。而逻辑回归通过非线性映射,大大减小了离分类平面较远的点的权重,

相对提升了与分类最相关的数据点的权重。两者的根本目的都是一样的。

 

比较:     

0、LR给出了后验概率,SVM只有01分类,没有后延概率。

1、LR采用logistic损失(误差平方和损失),SVM采用合页(Hinge)损失。(损失函数是二者的本质区别)

2、LR对异常值敏感;SVM对异常值不敏感,泛华能力强,分类效果好。

3、在训练集较小时,SVM较适用,而LR需要较多的样本。

4、LR模型找到的那个超平面,是尽量让所有点都远离他,而SVM寻找的那个超平面,是只让最靠近中间分割线的那些点尽量远离,即只用到那些支持向量的样本。

5、对非线性问题的处理方式不同,LR主要靠特征构造,必须组合交叉特征,特征离散化;SVM也可以这样,还可以通过kernel,kernel很强大。

6、LR相对来说模型更简单,好理解,实现起来,特别是大规模线性分类时比较方便。而SVM的理解和优化相对来说复杂一些。但是SVM的理论基础更加牢固,有一套结构化风险最小化的理论基础.

SVM优缺点:

优:泛化错误率低,计算开销不大,结果易解释。

缺:SVM对参数调节和核函数的选择敏感,原始分类器不加修改仅适用于处理二类问题。

Logistic优缺点:

优:计算代价不高,易于理解和实现。

缺:容易欠拟合,分类精度可能不高。

 

垃圾收集器的说明:

  1. 用于回收新生代的对象

Serial 单线程执行的,在执行Gc的时候需要停止工作线程的。

ParNew 这个是serial的多线程版本,一般在server端

Parallel Scavenge收集器(使用了复制算法): 不同于其他专注于减少在GC的时候造成的stop the world 的操作的时间的收集器,这个收集器专注于吞吐量。所谓的吞吐量就是说用户的程序执行的时间/(用户的程序的时间+垃圾收集的时间);

  1. 用户回收老年代的收集器

Serial Old的收集器(标记-整理算法)用于client端,与parallel scanvege配合使用。

Parallel Old 收集器: 这个提高了server端的老年代的回收效率,尤其是在注重吞吐量的情况下面。

CMS(concurrent mark sweep)收集器:专注于缩短GC造成的stop the world的时间,在重视系统响应速度,希望系统的停顿时间最小(标记-清除)算法

CMS的三个步骤:初始标记 并发标记 重新标记  并发清理  在整个过程中都是与用户线程并发执行的操作。

该收集器的缺点是: (1)CMS无法处理浮动垃圾,只能在下一次的额Full GC的时候进行触发FullGC操作进行的。那么什么是浮动垃圾呢?

所谓的“浮动垃圾”就是在并发执行垃圾标记的时候用户线程还在运行产生垃圾,但是在部分的垃圾没有被标记到,只能是留到下次的Full GC的时候了。(2)CMS对于CPU的资源比较的敏感,因为对于所有的并发程序来说的话,我必须把资源用在执行用户的程序上面的,但是当CPU资源下降的时候,还要去分担执行CMS垃圾回收的操作,这样的话,将会是非常的影响系统的性能的。(3)由于它使用的是标记-清除的回收算法所带来的本身的问题,会产生大量的内存碎片,导致老年代触发FullGc的操作。

 

3、G1收集器在进行收集的时候,是把java的堆内存划分为多个大小相等独立区域。不在有新生代和老年代的物理上的划分,在进行垃圾回收的时候,会对所有的region进行监控,并建立一个优先列表,每次回收这个列表里面最大的region.

 

 

整个特征工程的核心问题:特征预处理,特征选择,降维

特征预处理:

1、无量纲化(把定性的特征尽量转化为定量)对应的标准化,把特征的分布转换为标准正太分布(对于特征的列)

2、区间缩放法(最大值-最小值法)

3、归一化(对于特征矩阵的列来处理)

4、二值化(对于某一个特征大于阈值的是1,小于阈值的是0,这个规则可以自己定义的,看实际的场景)

5、onehotEncoder  对于特征矩阵的行来处理 (好处:处理了对于分类器不好处理的特征,对于那些非连续的特征,当我们利用one-hot编码来进行处理)

在机器学习的应用任务中,对于非连续的数据经常也会使用数字进行编码,便于处理。例如“男性”编码为1,“女性”编码为2。但是这二者之间是不存在数学上的连续关系的,然而如果按照上述1和2进行编码的话,机器学习算法会认为“男性”和“女性”之间存在数学的有序关系。

6、缺失值:填充(均值,nan)

7、多项式的数据转化(log函数,指数函数等进行转换)

 

特征选择:看看特征是否是发散或者是特征与目标的相关性

  1. Filter过滤法:方差选择法(计算各个特征的方差)  相关系数选择法(计算各个特征对于目标值的相关系数)卡方检验,互信息都是针对定性自变量对于定性因变量的相关性
  2. Wrapper包装法 递归特征消除法(基于一个base model来计算,每次选择不同的特征进行计算)
  3. 嵌入法:基于惩罚项的特征选择

利用L1惩罚:L1惩罚项降维的原理在于保留多个对目标值具有同等相关性的特征中的一个

 

降维:把高纬度的数据特征降到低纬度的特征空间上面

主要有PCA主成分分析  LDA线性判别分析  

两者的映射目标不同的:PCA主要是在映射后样本具有最大的发散性

LDA主要是在映射后让样本具有更好的分类性能

 

# kmeans

## 基本概念

###

要得到几个知心  需要确定k值

质心:均值 即各个向量的各个维度的平均

距离的度量:欧式距离  余弦相似度

优化目标:

    mini=1KxϵCidist(Ci,x)  

优化目标里面的第一个求和表示的是对于每一个簇,第二个求和表示的是每一个簇中的每一个数据样本到质心的距离和,这个距离越小越好,说明越是相似。因为我们聚类的目标就是让尽可能相似的数据划分到一个簇里面。

## kmeans算法的工作流程;

1.初始化k值,随机在空间内选定两个质心,然后对于所有空间的样本点来说,我要去求每个一个样本点到两个质心的距离d1, d2 比较大小,把距离小的划分为对应的簇

迭代完所有的数据样本

2、基于上面划分好的簇,在求更新质心。

3、把质心更新完了之后,我们再去迭代求所有数据样本对于这两个新的质心的距离,重新划分簇;

一直这样迭代下去,直到所有的点没有什么变化的时候,则达到最优解

## 缺点:

    k值很难确定

    复杂度与样本呈线性关系

    很难发现任意形状的簇  比如环装的簇

针对上面的k值的优化 (这样对于迭代的效率会有很好的提升)

出现了k-means++ 。

K-means距离计算的优化

在原来的迭代的过程中需要计算很多的距离,那么我可以考虑在这块进行优化,利用三角形的三条边的关系优化,这样可以减少计算的次数。

大样本进行优化操作mini batch

 

面试:讲解一下k-means的原理,算法的改进  异常值的处理

改进k-means++. 对于你初始化k的随机选择进行了优化,在选择的时候这个被选择的点尽可能的远,避免局部最优。

Mini batch kmeans: 每次只是选择一个子集作为重入类,

距离的计算:利用欧式距离来计算。但是迭代的过程中会有大量的距离计算,考虑利用三角形的边的关系进行优化。

异常值的处理:局部异常因子,多元高斯异常因子检测 (计算点的密度)

 

SVM的核函数的选择:

SVM核函数的选择对于其性能的表现有至关重要的作用,尤其是针对那些线性不可分的数据,因此核函数的选择在SVM算法中就显得至关重要。对于核技巧我们知道,其目的是希望通过将输入空间内线性不可分的数据映射到一个高纬的特征空间内使得数据在特征空间内是可分的,我们定义这种映射为ϕ(x)ϕ(x),那么我们就可以把求解约束最优化问题变为

但是由于从输入空间到特征空间的这种映射会使得维度发生爆炸式的增长,因此上述约束问题中内积ϕi⋅ϕjϕi⋅ϕj的运算会非常的大以至于无法承受,因此通常我们会构造一个核函数:

从而避免了在特征空间内的运算,只需要在输入空间内就可以进行特征空间的内积运算。通过上面的描述我们知道要想构造核函数κκ,我们首先要确定输入空间到特征空间的映射,但是如果想要知道输入空间到映射空间的映射,我们需要明确输入空间内数据的分布情况,但大多数情况下,我们并不知道自己所处理的数据的具体分布,故一般很难构造出完全符合输入空间的核函数,因此我们常用如下几种常用的核函数来代替自己构造核函数:

线性核函数:

线性核,主要用于线性可分的情况,我们可以看到特征空间到输入空间的维度是一样的,其参数少速度快,对于线性可分数据,其分类效果很理想,因此我们通常首先尝试用线性核函数来做分类,看看效果如何,如果不行再换别的。

多项式核函数:

多项式核函数可以实现将低维的输入空间映射到高纬的特征空间,但是多项式核函数的参数多,当多项式的阶数比较高的时候,核矩阵的元素值将趋于无穷大或者无穷小,计算复杂度会大到无法计算。

高斯(RBF)核函数:

高斯径向基函数是一种局部性强的核函数,其可以将一个样本映射到一个更高维的空间内,该核函数是应用最广的一个,无论大样本还是小样本都有比较好的性能,而且其相对于多项式核函数参数要少,因此大多数情况下在不知道用什么核函数的时候,优先使用高斯核函数。

Sigmoid核函数:

采用sigmoid核函数,支持向量机实现的就是一种多层神经网络。

当然,那么我们对面对线性不可分的数据的时候,如何选择合适的核函数呢?一般如果我们有一定的先验知识的话,就可以选择符合数据分布特征的核函数;如果要是不知道的话,则利用交叉验证的方式选择不同的核函数,最后选择误差最小的那个。

    1. 如果特征的数量大到和样本数量差不多,则选用LR或者线性核的SVM;
    2. 如果特征的数量小,样本的数量正常,则选用SVM+高斯核函数;

如果特征的数量小,而样本的数量很大,则需要手工添加一些特征从而变成第一种情况。

Spark的GC调优

由于Spark立足于内存计算,常常需要在内存中存放大量数据,因此也更依赖JVM的垃圾回收机制(GC)。并且同时,它也支持兼容批处理和流式处理,对于程序吞吐量和延迟都有较高要求,因此GC参数的调优在Spark应用实践中显得尤为重要。

 

在运行Spark应用时,有些问题是由于GC所带来的,例如垃圾回收时间久、程序长时间无响应,甚至造成程序崩溃或者作业失败。

 

按照经验来说,当我们配置垃圾收集器时,主要有两种策略——Parallel GC(吞吐量优先)和CMS GC(低延迟响应)。

 

前者注重更高的吞吐量,而后者则注重更低的延迟。两者似乎是鱼和熊掌,不能兼得。在实际应用中,我们只能根据应用对性能瓶颈的侧重性,来选取合适的垃圾收集器。例如,当我们运行需要有实时响应的场景的应用时,我们一般选用CMS GC,而运行一些离线分析程序时,则选用Parallel GC。

 

那么对于Spark这种既支持流式计算,又支持传统的批处理运算的计算框架来说,是否存在一组通用的配置选项呢?

 

通常CMS GC是企业比较常用的GC配置方案,并在长期实践中取得了比较好的效果。例如对于进程中若存在大量寿命较长的对象,Parallel GC经常带来较大的性能下降。因此,即使是批处理的程序也能从CMS GC中获益。不过,在从1.7开始的HOTSPOT JVM中,我们发现了一个新的GC设置项:Garbage-First GC(G1 GC)。Oracle将其定位为CMS GC的长期演进,这让我们重燃了鱼与熊掌兼得的希望!

 

 

GC算法原理

在传统JVM内存管理中,我们把Heap空间分为Young/Old两个分区,Young分区又包括一个Eden和两个Survivor分区,如下图所示。新产生的对象首先会被存放在Eden区,而每次minor GC发生时,JVM一方面将Eden分区内存活的对象拷贝到一个空的Survivor分区,另一方面将另一个正在被使用的Survivor分区中的存活对象也拷贝到空的Survivor分区内。

 

在此过程中,JVM始终保持一个Survivor分区处于全空的状态。一个对象在两个Survivor之间的拷贝到一定次数后,如果还是存活的,就将其拷入Old分区。当Old分区没有足够空间时,GC会停下所有程序线程,进行Full GC,即对Old区中的对象进行整理。注意:Full GC时,所有线程都暂停,所以这个阶段被称为Stop-The-World(STW),也是大多数GC算法中对性能影响最大的部分。

 

 

而G1 GC则完全改变了这一传统思路。它将整个Heap分为若干个预先设定的小区域块,每个区域块内部不再进行新旧分区, 而是将整个区域块标记为Eden/Survivor/Old。当创建新对象时,它首先被存放到某一个可用区块(Region)中。当该区块满了,JVM就会创建新的区块存放对象。当发生minor GC时,JVM将一个或几个区块中存活的对象拷贝到一个新的区块中,并在空余的空间中选择几个全新区块作为新的Eden分区。当所有区域中都有存活对象,找不到全空区块时,才发生Full GC。即G1 GC发生Full GC的频次要比其他GC更低,因为内存使用率很高。

而在标记存活对象时,G1使用RememberSet的概念,将每个分区外指向分区内的引用记录在该分区的RememberSet中,避免了对整个Heap的扫描,使得各个分区的GC更加独立。

 

在这样的背景下,我们可以看出G1 GC大大提高了触发Full GC时的Heap占用率,同时也使得Minor GC的暂停时间更加可控,对于内存较大的环境非常友好。因为G1 GC对于内存的使用率特别高,内存越大,此优势越明显。

 

 

 

关于Hotspot JVM所支持的完整的GC参数列表,可以参见Oracle官方的文档中对部分参数的解释。

 

Spark的内存管理

Spark的核心概念是RDD,实际运行中内存消耗都与RDD密切相关。Spark允许用户将应用中重复使用的RDD数据持久化缓存起来,从而避免反复计算的开销,而RDD的持久化形态之一就是将全部或者部分数据缓存在JVM的Heap中。当我们观察到GC延迟影响效率时,应当先检查Spark应用本身是否有效利用有限的内存空间。RDD占用的内存空间比较少的话,程序运行的heap空间也会比较宽松,GC效率也会相应提高;而RDD如果占用大量空间的话,则会带来巨大的性能损失。

 

下面从某个用户案例来说明:

这个应用其本质就是一个简单的迭代计算。而每次迭代计算依赖于上一次的迭代结果,因此每次迭代结果都会被主动持久化到内存空间中。当运行用户程序时,我们观察到随着迭代次数的增加,进程占用的内存空间不断快速增长,GC问题越来越突出。

 

造成这个问题的原因是没有及时释放掉不再使用的RDD,从而造成了内存空间不断增长,触发了更多GC执行。

 

 

小结:当观察到GC频繁或者延时长的情况,也可能是Spark进程或者应用中内存空间没有有效利用。所以可以尝试检查是否存在RDD持久化后未得到及时释放等情况。

 

选择垃圾收集器

在解决了应用本身的问题之后,我们就要开始针对Spark应用的GC调优了。

 

Spark默认使用的是Parallel GC。经调研我们发现,Parallel GC常常受困于Full GC,而每次Full GC都给性能带来了较大的下降。而Parallel GC可以进行参数调优的空间也非常有限,我们只能通过调节一些基本参数来提高性能,如各年代分区大小比例、进入老年代前的拷贝次数等。而且这些调优策略只能推迟Full GC的到来,如果是长期运行的应用,Parallel GC调优的意义就非常有限了。

 

Configuration Options   -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+PrintFlagsFinal -XX:+PrintReferenceGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy -Xms88g -Xmx88g

 

 

G1 GC的配置

Configuration Options   -XX:+UseG1GC -XX:+PrintFlagsFinal -XX:+PrintReferenceGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy -XX:+UnlockDiagnosticVMOptions -XX:+G1SummarizeConcMark -Xms88g -Xmx88g

 

大多数情况下,最大的性能下降是由Full GC导致的,G1 GC也不例外,所以当使用G1 GC时,需要根据一定的实际情况进行参数配置,这需要很丰富的工作经验和运维经验,以下仅提供一些处理思路。

 

比如G1 GC收集器在将某个需要垃圾回收的分区进行回收时,无法找到一个能将其中存活对象拷贝过去的空闲分区。这种情况被称为Evacuation Failure,常常会引发Full GC。对于这种情况,我们常见的处理办法有两种:

 

    将InitiatingHeapOccupancyPercent参数调低(默认值是45),可以使G1 GC收集器更早开始Mixed GC(Minor GC);但另一方面,会增加GC发生频率。(启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示"一直执行GC循环". 默认值为 45.)

    降低此值,会提高Minor GC的频率,但是会推迟Full GC的到来。

   

    提高ConcGCThreads的值,在Mixed GC阶段投入更多的并发线程,争取提高每次暂停的效率。但是此参数会占用一定的有效工作线程资源。

   

调试这两个参数可以有效降低Full GC出现的概率。Full GC被消除之后,最终的性能获得了大幅提升。

 

此外,可能还会遇到这样的情况:出现了一些比G1的一个分区的一半更大的对象。对于这些对象,G1会专门在Heap上开出一个个Humongous Area来存放,每个分区只放一个对象。但是申请这么大的空间是比较耗时的,而且这些区域也仅当Full GC时才进行处理,所以我们要尽量减少这样的对象产生。或者提高G1HeapRegionSize的值减少HumongousArea的创建。(G1HeapRegionSize=n    使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb.)

 

 

不过在内存比较大的时,JVM默认把这个值设到了最大(32M),此时我们只能通过分析程序本身找到这些对象并且尽量减少这样的对象产生。当然,相信随着G1 GC的发展,在后期的版本中相信这个最大值也会越来越大,毕竟G1号称是在1024~2048个Region时能够获得最佳性能。

Configuration Options   -XX:+UseG1GC -XX:+PrintFlagsFinal -XX:+PrintReferenceGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy -XX:+UnlockDiagnosticVMOptions -XX:+G1SummarizeConcMark -Xms88g -Xmx88g -XX:InitiatingHeapOccupancyPercent=35 -XX:ConcGCThread=20

 

 

总结

对于大量依赖于内存计算的Spark应用,GC调优显得尤为重要。在发现GC问题的时候,不要着急调试GC。而是先考虑是否存在Spark进程内存管理的效率问题,例如RDD缓存的持久化和释放。至于GC参数的调试,首先我们比较推荐使用G1 GC来运行Spark应用。相较于传统的垃圾收集器,随着G1的不断成熟,需要配置的选项会更少,能同时满足高吞吐量和低延迟的寻求。当然,GC的调优不是绝对的,不同的应用会有不同应用的特性,掌握根据GC日志进行调优的方法,才能以不变应万变。最后,也不能忘了先对程序本身的逻辑和代码编写进行考量,例如减少中间变量的创建或者复制,控制大对象的创建,将长期存活对象放在Off-heap中等等。 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值