Spark(十二)--性能调优篇
一段程序只能完成功能是没有用的,只能能够稳定、高效率地运行才是生成环境所需要的。
本篇记录了Spark各个角度的调优技巧,以备不时之需。
一、配置参数的方式和观察性能的方式
额。。。从最基本的开始讲,可能一些刚接触Spark的人不是很清楚Spark的一些参数变量到底要配置在哪里。
可以通过三种方式配置参数,任选其一皆可。
- spark-env.sh文件中配置:最近常使用的配置方式,格式可以参考其中的一些官方保留的配置。
- 程序中通过SparkConf配置:通过SparkConf对象set方法设置键值对,比较直观。
- 程序中通过System.setProperty配置:和方法二差不多。
值得一提的是一个略显诡异的现象,有些参数在spark-env.sh中配置并不起作用,反而要在程序中设置才有效果。
Spark的参数很多,一些默认的设置可以参考官网推荐的配置参数:/docs/latest/configuration.html
可以通过以下几种方式来观察Spark集群的状态和相关性能问题:
- Web UI:即8088端口进入的UI界面。
- Driver程序日志:根据程序提交方式的不同到指定的节点上观察Driver程序日志。
- logs文件夹下的日志:Spark集群的大部分信息都会记录在这里。
- works文件夹下的日志:主要记录Work节点的信息。
- Profiler工具:没有使用过。
前景交代完毕,下面进入正题:
二、调度与分区优化
1、小分区合并的问题
由于程序中过度使用filter算子或者使用不当,都会造成大量的小分区出现。
因为每次过滤得到的结果只有原来数据集的一小部分,而这些量很小的数据同样会以一定的分区数并行化分配到各个节点中执行。
带来的问题就是:任务处理的数据量很小,反复地切换任务所消耗的资源反而会带来很大的系统开销。
解决方案:使用重分区函数coalesce进行数据紧缩、减少分区数并设置shuffle=true保证任务是并行计算的
减少分区数,虽然意味着并行度降低,但是相对比之前的大量小任务过度切换的消耗,却是比较值得的。
这里也可以直接使用repartition重分区函数进行操作,因为其底层使用的是coalesce并设置Shuffle=true
2、数据倾斜问题
这是一个生产环境中经常遇到的问题,典型的场景是:大量的数据被分配到小部分节点计算,而其他大部分节点却只计算小部分数据。
问题产生的原因有很多,可能且不全部包括:
- key的数据分布不均匀
- 业务数据本身原因
- 结构化表设计问题
- 某些SQL语句会造成数据倾斜
可选的解决方案有:
- 增大任务数,减少分区数量:这种方法和解决小分区问题类似。
- 对特殊的key进行处理,如空值等:直接过滤掉空值的key以免对任务产生干扰。
- 使用广播:小数据量直接广播,大数据量先拆分之后再进行广播。
还有一种场景是任务执行速度倾斜问题:集群中其他节点都计算完毕了,但是只有少数几个节点死活运行不完。(其实这和上面的那个场景是差不多的)
解决方案:
- 设置spark.speculation=true将执行事件过长的节点去掉,重新分配任务
- spark.speculation.interval用来设置执行间隔
3、并行度调整
官方推荐每个CPU CORE分配2-3个任务。
- 任务数太多:并行度太高,产生大量的任务启动和切换开销。
- 任务数太低:并行度过低,无法发挥集群并行计算能力,任务执行慢
Spark会根据文件大小默认配置Map阶段的任务数,所以我们能够自行调整的就是Reduce阶段的分区数了。
- reduceByKey等操作时通过numPartitions参数进行分区数量配置。
- 通过spark.default.parallelism进行默认分区数配置。
4、DAG调度执行优化
DAG图是Spark计算的基本依赖,所以建议:
- 同一个Stage尽量容纳更多地算子,防止多余的Shuffle。
- 复用已经cache的数据。
尽可能地在Transformation算子中完成对数据的计算,因为过多的Action算子会产生很多多余的Shuffle,在划分DAG图时会形成众多Stage。
三、网络传输优化
1、大任务分发问题
Spark采用Akka的Actor模型来进行消息传递,包括数据、jar包和相关文件等。
而Akka消息通信传递默认的容量最大为10M,一旦传递的消息超过这个限制就会出现这样的错误:
Worker任务失败后Master上会打印“Lost TID:”
根据这个信息找到对应的Worker节点后查看SparkHome/work/目录下的日志,查看Serialized size of result是否超过10M,就可以知道是不是Akka这边的问题了。
一旦确认是Akka通信容量限制之后,就可以通过配置spark.akka.frameSize控制Akka通信消息的最大容量。
2、Broadcast在调优场景的使用
Broadcast广播,主要是用于共享Spark每个Task都会用到的一些只读变量。
对于那些每个Task都会用到的变量来说,如果每个Task都为这些变量分配内存空间显然会使用很多多余的资源,使用广播可以有效的避免这个问题,广播之后,这些变量仅仅会在每台机器上保存一份,有Task需要使用时就到自己的机器上读取就ok。
官方推荐,Task大于20k时可以使用,可以在控制台上看Task的大小。
3、Collect结果过大的问题
大量数据时将数据存储在HDFS上或者其他,不是大量数据,但是超出Akka传输的Buffer大小,通过配置spark.akka.frameSize调整。
四、序列化与压缩
1、通过序列化手段优化
序列化之前说过,好处多多,所以是推荐能用就用,Spark上的序列化方式有几种,具体的可以参考官方文档。
这里只简单介绍一下Kryo。
配置参数的时候使用spark.serializer=”org.apache.spark.serializer.KryoSerializer”配置
自定义定义可以被Kryo序列化的类的步骤:
- 自定义类extends KryoRegistrator
- 设置序列化方式conf.set(“spark.serializer”,”org.apache.spark.serializer.KryoSerializer”)
- conf.set(“spark.kyro.registrator”,”自定义的class”)
- 如果对象占用空间大,需要增加Kryo的缓冲区则配置spark.kryoserializer.buffer.mb上值默认为2M
2、通过压缩手段优化
Spark的Job大致可以分为两种:
- I/O密集型:即存在大量读取磁盘的操作。
- CPU密集型:即存在大量的数据计算,使用CPU资源较多。
对于I/O密集型的Job,能压缩就压缩,因为读磁盘的时候数据压缩了,占用空间小了,读取速度不就快了。
对于CPU密集型的Job,看具体CPU使用情况再做决定,因为使用压缩是需要消耗一些CPU资源的,如果当前CPU已经超负荷了,再使用压缩反而适得其反。
Spark支持两种压缩算法:
- LZF:高压缩比
- Snappy:高速度
一些压缩相关的参数配置:
- spark.broadcast.compress:推荐为true
- spark.rdd.compress:默认为false,看情况配置,压缩花费一些时间,但是可以节省大量内存空间
- spark.io.compression.codec:org.apache.spark.io.LZFCompressionCodec根据情况选择压缩算法
- spark.io.compressions.snappy.block.size:设置Snappy压缩的块大小
五、其他优化方式
1、对外部资源的批处理操作
如操作数据库时,每个分区的数据应该一起执行一次批处理,而不是一条数据写一次,即map=>mapPartition。
2、reduce和reduceByKey
reduce:内部调用了runJob方法,是一个action操作。
reduceByKey:内部只是调用了combineBykey,是Transformation操作。
大量的数据操作时,reduce汇总所有数据到主节点会有性能瓶颈,将数据转换为Key-Value的形式使用reduceByKey实现逻辑,会做类似mr程序中的Combiner的操作,Transformation操作分布式进行。
3、Shuffle操作符的内存使用
使用会触发Shuffle过程的操作符时,操作的数据集合太大造成OOM,每个任务执行过程中会在各自的内存创建Hash表来进行数据分组。
可以解决的方案可能有:
- 增加并行度即分区数可以适当解决问题
- 可以将任务数量扩展到超过集群整体的CPU core数