spark调优(是一个动态的过程,不能一劳永逸):
一. 数据倾斜调优(https://www.cnblogs.com/DT-Spark/p/5878827.html,join类的比较难处理,聚合类相对简单)
数据倾斜是指reduce阶段需要将相同key的数据拉取shuffle到某个节点的一个task处理,某个key的数据量特别大
(比如大部分key对应10条数据,但是个别key却对应了100万条数据),导致有的task早早执行完,而有的task执行几个小时(甚至内存溢出),
整个stage由运行时间最长的task决定;
如何定位数据倾斜?由于一定发生了shuffle,所以要找导致shuffle的算子,如groupByKey、reduceByKey、aggregateByKey等;(以及sparksql的group by等)
具体根据log或Web UI找到各个stage,查看每个task分配的数据量,以及每个task花费的时间,从运行时间就能看出了;
找到stage后,根据stage划分原理,推断出代码位置(肯定包含一个shuffle类算子);
解决方案一:直接过滤掉少数导致倾斜的key(如sparksql利用where过滤掉,或sparkcore利用filter算子过滤掉),但场景有限,大部分情况导致倾斜的key还是很多的,并不是少数几个;
解决方案二:提高shuffle操作的并行度,如对sparksql增加spark.sql.shuffle.partitions的值,对sparkcore增加shuffle算子如reduceByKey(1000)的并行度;
(原理是让更多task处理相同数量的数据,每个task更快完成;但只是缓解,没有彻底根除问题,效果有限;对于100万数据,无论怎么增加也没用;)
解决方案三:两阶段聚合(局部聚合+全局聚合),把key打上随机数(如10以内),先进行局部聚合(执行reduceByKey或group by等),然后把前缀去掉,再进行全局聚合;
(仅仅适用于聚合类的shuffle操作,对join类的shuffle操作无能为力)
解决方案四:将reduce join转为map join,即利用Broadcast变量与map类算子代替join算子实现join操作,进而完全规避掉shuffle类操作;
(针对join类的shuffle操作,且只适用于一个大表和一个小表的情况;)
解决方案五:采样倾斜key并分拆join操作;对不正常RDD将倾斜的key独立出来形成独立RDD,并打上随机前缀;对另一个RDD倾斜key对应的数据独立出来并膨胀成n倍(每条数据加上n内的前缀,不需要全局扩容哦);
对两个独立出来的RDD进行join,另外两个正常的RDD进行join;最后对两个结果union即能得到最终的join结果;
(针对join类的shuffle操作,且适用于两个都是大表,且一个RDD有key倾斜一个RDD正常的情况;可看成方案三和四的结合体;但对成千上万的倾斜key仍无法使用)
解决方案六:使用随机前缀和扩容RDD进行join;和方案五类似,但对不正常RDD每条数据打上随机前缀,对正常RDD进行n倍扩容,两个处理后的RDD进行join即可;
(针对join类的shuffle操作,且适用于两个都是大表,且一个RDD有key倾斜一个RDD正常的情况;可看成方案五的暴力求解;能处理成千上万倾斜key情况;缓解数据倾斜,而不是彻底避免数据倾斜)
解决方案七:对以上方案进行组合;
二. shuffle调优(https://www.cnblogs.com/DT-Spark/p/5878827.html,主要是改变一些配置项)
shuffle环节包含了大量的磁盘IO、序列化、网络数据传输等操作;注意shuffle调优只在整个Spark性能调优占到一小部分而已;
当使用HashShuffleManager时,可以将spark.shuffle.consolidateFiles设置为true,即开启consolidate机制,允许不同的task复用同一批磁盘文件,将多个task的磁盘文件进行一定程度上的合并,从而大幅度减少磁盘文件的数量,提升shuffle write的性能;
SortShuffleManager的运行机制主要分成两种,一种是普通运行机制,另一种是bypass运行机制;
可以通过以下参数进行调优:spark.shuffle.file.buffer、spark.reducer.maxSizeInFlight、spark.shuffle.io.maxRetries、spark.shuffle.manager、spark.shuffle.sort.bypassMergeThreshold等;
三. 开发调优(https://blog.csdn.net/u012102306/article/details/51322209,比较简单,主要体现在代码中)
开发调优和资源调优是所有Spark作业都需要注意和遵循的一些基本原则;
- 避免创建重复的RDD(即不能创建多个RDD来代表同一份数据);
- 尽可能复用同一个RDD;
- 对多次使用的RDD进行持久化(保证一个RDD被多次使用时只被计算一次);调用cache()和persist()即可;选择最合适的持久化策略;可采用序列化的方式,如MEMORY_AND_DISK_SER,减小数据内存占用量,避免频繁GC;若内存足够大,使用MEMORY_ONLY即可;
- 尽量避免使用shuffle类算子;(如用Broadcast变量与map类算子代替join算子)
- 使用map-side预聚合的shuffle操作;(如用reduceByKey或者aggregateByKey算子替代groupByKey算子,因为groupByKey算子不会进行预聚合;)
- 使用高性能的算子;如使用mapPartitions替代普通map(内存足够时),使用foreachPartitions替代foreach,使用filter之后进行coalesce操作减少partition数量等;
- 广播大变量;(对于外部大变量,保证每个Executor的内存中,只驻留一份变量副本;而Executor中的task共享该变量副本)
- 使用Kryo优化序列化性能;(比Java序列化类库的性能要高10倍左右,但需要注册自定义类型,比较麻烦)
- 优化数据结构;(使用字符串替代对象,使用原始类型替代字符串,使用数组替代集合类型;减少内存占用、降低GC频率;但考虑到代码的可维护性,比较难以平衡;)
四. 垃圾回收调优
GC调优目标:确保只有长生命力的RDD保存在老年代,同时新生代有足够的空间存储短生命力的对象,这样可以避免引起full GC;
把-XX:+PrintGCDetails添加到spark环境变量;
根据情况调整spark.storage.memoryFraction的大小(如任务变慢,就需要减小这个值);
估计每个任务需要多少内存,设置Eden的大小,进而设置Xmn的大小;
观察和控制GC频率;
五. 资源调优(https://blog.csdn.net/u012102306/article/details/51637366,没有一个固定的值,看实际情况)
主要是指spark-submit命令中的参数设置;
num-executors一般设置50100个左右;executor-memory设置4G8G;num-executorsexecutor-memory,就代表你的Spark作业申请到的总内存量;
executor-cores设置为2~4个较为合适;num-executorsexecutor-cores,就代表你的Spark作业申请到的总核数;
driver-memory设置1G左右或不设置(使用collect算子必须保证Driver内存足够大);
spark.default.parallelism表示每个stage的默认task数量;(500~1000个较为合适,不设置会根据底层HDFS的block数量来设置task数量,会导致前面设置好的Executor的参数前功尽弃;设置为该Executor的总CPU core数的2~3倍较为合适)
spark.storage.memoryFraction(默认是0.6,若有较多RDD持久化操作,该参数可以适当提高一些;观察spark web ui,当task内存不够用时,调低这个值)
spark.shuffle.memoryFraction(默认是0.2,shuffle操作进行聚合时,如果发现使用内存超出了20%的限制,会溢写到磁盘,就会极大地降低性能;)
六. 数据本地化调优
按照数据与代码的远近程度,由近及远分为:
PROCESS_LOCAL代表同一个JVM;NODE_LOCAL代表同一个节点;No_Pref代表无位置偏好;Rack_local代表同一个机架;Any代表不同机架;
主要通过以下参数控制:spark.locality.wait、spark.locality.wait.process、spark.locality.wait.node、spark.locality.wait.rack;