spark 调优

由于大多数spark的计算都是内存计算,因此在spark集群中有以下瓶颈:CPU,网络带宽还有内存。如果内存足够的话,主要可能就是网络带宽。目前主要有两种调优方案:数据序列化和内存调整。

1. 数据序列化

序列化在任何分布式应用中都有举足轻重的作用,如果对象被序列化很慢或者序列化后的格式很大,会大大的降低计算的性能。通常来说,这应该是你调优spark应用性能首先要做的事情。spark 提供了两种序列化方式:

  1. Java serialization:spark 默认使用java的ObjectOutputStream框架来序列化对象,对于你的任何实现了java.io.Serializable接口的java类都管用。你也可以通过扩展java.io.Externalizable来提升序列化的性能。Java的序列化比较灵活,但是通常比较慢,它产生的数据格式往往也比较大。
  2. Kryo serialization:spark也可以使用Kryo 序列化器去快速序列化对象,它相对Java 序列化器在速度和压缩性能上都有明显的优势(10x),但是它并不支持所有可序列化的类型,并且还要你去提前注册你所需要序列化的类以提升性能。

你可以通过配置SparkConf 切换到Kryo序列化器,调用conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer").这个设置会在work nodes 之间shuffle data 和序列化RDD到磁盘的时候使用Kyro序列化器。Kryo之所以不是默认的序列化器是因为它需要自定义的注册过程,但是官方推荐尝试这种序列化方式,尤其是在网络通讯密集型的应用中。从spark2.0.0开始,官方已经在spark内部在shuffle RDD过程中使用Kryo去对简单的数据类型进行序列化了。
为了注册你所需要序列化的类型,可以使用SparkConf的registerKryoClasses方法:

val conf = new SparkConf().setMaster(...).setAppName(...)
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))
val sc = new SparkContext(conf)

如果你的序列化对象很大,还需要增加spark.kryoserializer.buffer 这个值,它必须比你序列化的最大对象所占的空间大
如果不注册的话,Kryo也能工作,但是会造成极大的浪费。

2. 内存调优

内存调优主要考虑三个方面:数据对象所占的空间、访问对象的消耗、垃圾回收的消耗。

2.1. 内存管理概览

spark里的内存使用主要是这两个部分:执行和存储。执行内存主要是在shuffles,joins,sorts,aggregations,而存储内存主要是在cache和在集群节点中传播数据。在spark中,执行和存储共用一块内存M,如果执行没有使用内存,存储就可以占用全部这部分的内存,反之亦然。必要的时候执行可以将存储所用的内存赶出去,但是会有一个R值,这是个阈值,意味着这个阈值范围内如果有存储内存是不能被赶出去的,而存储是不能赶出执行内存的。
这样的设计有几个好处,一方面执行和存储公用内存可以防止不必要的内存浪费,另一方面设置了阈值R可以防止这个阈值内的存储内存被赶出去,最后,这种方法提供了在绝大多数工作负载下开箱即用的良好性能,用户不需要去关心这两个内存带怎么分配。
相关的配置有两个,一般的用户不需要对其进行调整,因为默认配置已经足够好了。

  • spark.memory.fraction:表示M占用JVM 堆栈的空间(默认300M),这个值默认0.6,也就是240M。剩下的40%的空间用来存储元数据、数据结构等,在稀疏或异常大的记录下防止OOM错误。
  • spark.memory.storageFraction这个代表阈值R,默认0.5

spark.memory.fraction一般可以调整以用来适应不同java 版本JVM 堆栈空间大小。

2.2. 确定内存消耗

测试数据集所需要占的内存最好的方法就是创建一个RDD,然后cache,在spark UI页面(通常是4040端口)查看 storage
对于一个特定对象的内存消耗,可以使用SizeEstimatorestimate 方法

2.3. 调整数据结构

减少内存占用首先要考虑的就是去调整数据结构,尽量避免java的基于指针的数据结构和包装器对象。

  1. 设计数据结构优先考虑对象数组和基本数据类型,而不是Java或者Scala的集合类,fastutil库为与Java标准库兼容的基本类型提供了方便的集合类
  2. 避免很多小对象和指针的嵌套结构
  3. 使用数字ID或者枚举而不是String类型作为键值
  4. 如果你的内存RAM小于32GB,设置JVM -XX:+UseCompressedOops用来将8个字节的指针压缩成4个字节的指针。这个option可以在spark-env.sh中进行修改。

2.4. 序列化RDD存储

如果调整数据结构依然很大,那可以在RDD 的persistence API 里选择MEMORY_ONLY_SER或者MEMORY_AND_DISK_SER,来将对象序列化存储,但是缺点是访问时间会长点,因为需要动态反序列化。如果采用这种方式,强烈建议使用Kryo 序列化方式。这种序列化方式会将每个RDD partition变成一个二进制数组

以上只对java 和 scala 有效,python还是弃疗吧

2.5. gc调优

当程序中有大量RDD流失(用了就不用了),JVM垃圾回收会占用一定的时间,当java需要做gc时,它需要跟踪所有对象并找到未使用的对象。gc的代价与Java 对象个数是成正比的,所以尽量使用少的对象来减少这个开销。当然更好的方法是上面提到的序列化存储,因为会将每个RDD partition变成一个二进制数组对象,这样对象数量大大减少。在尝试其它方式前,建议先尝试这种方式

2.5.1 度量gc的影响

在gc调优前,首先要分析gc的频率。可以在Java options 里面加上-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps ,这样spark 在gc时就会log出来。注意的是如果log到文件,文件会在work nodes下的目录

2.5.2 高级gc调优

进一步的gc调优,需要了解一些关于JVM内存管理的基本知识。

  • Java 堆空间分为两个区域:Old和Young。Old区域存储生命周期长的对象,Young区域存储生命周期短的对象。
  • Young区域进一步分为三个区域:Eden,Survivor1,Survivor2
  • gc过程简单叙述如下:当Eden满了的时候,迷你版的GC会运行在Eden上,在Eden和Survivor1上的存活下来的对象会被拷贝到Survivor2上去。这时候两个Survivor区域交换。如果某个对象生命周期足够长或者Survivor2满了,就会移动到Old区域,如果Old区域也满了,完整的gc将会运行。

gc 调优的目标就是要让长周期的对象在Old区域,Young区域有足够的空间存储短周期的对象。以下是几点建议:

  • 看看gc统计信息中是否有太多的垃圾回收次数,如果一个task完成前多次调用了完整的gc,那意味着就没有足够的内存空间来执行任务。
  • 如果有很多的迷你gc,但是完整gc较少,那可以调大Eden空间。如果Eden空间大小为E,可以设置-Xmn=4/3*E来增大Young区域空间。
  • 在gc统计信息中,如果OldGen接近满的状态,通过调低spark.memory.fraction降低cache所用的内存,减少cache的对象比减慢程序运行速度要好。还有一种方法是降低Young的内存,也就是减小Xmn的值。也可以调整JVM的 NewRatio的值,很多JVM的这个默认值是2,意思是OldGen:YoungGen=2:1,增大这个比值可以增大OldGen的空间。
  • 尝试通过设置-XX:+UseG1GC使用G1GC垃圾回收器,它在超大堆的时候表现更加出色,注意,当堆空间很大的时候,也要增加G1区域的空间-XX:G1HeapRegionSize
  • 如果你的数据是在HDFS上读取的,那么可以根据在HDFS上的所占的空间估计,大约是2到三倍的大小。
  • 监控新设置对于GC在时间和频率上的影响。

3. 其它的考虑

3.1. 并行程度

如果不把spark的并行程度调到足够大的话,集群是不会被完全利用的,可以通过设置spark.default.parallelism来调整默认的并行度,也可以在具体的一些操作中设置,建议每个cpu 核上运行2 ~ 3个task

3.2. reduce任务的内存使用

有时候,你的程序报OOM异常不是因为你的RDD放不下内存,而是因为你的任务中的某个工作集,比如其中一个groupByKey 的reduce任务
,所占的内存太大。Spark的shuffle操作(sortByKey,groupByKey,reduceByKey,join等等)会创建在每个任务中构建一个hash表来完成分组任务,这个表会很大。一个简单的修复方法是增加并行程度,这样每个任务可以更小。spark可以有效的支持短至200ms的任务,并且多个任务公用一个JVM程序,任务的启动速度很快。所以你可以安全的将任务的并行程度超过集群的核心数。

3.3. 广播大的变量

使用sparkContext中的广播功能可以大大减小序列化任务的大小以及任务的启动时间。如果在任务使用了驱动程序中的大的对象(例如静态查找表),可以将其变成广播变量。spark会在master节点上打印各个task的大小,如果task的大小超过20KB就值得调优了

3.4 数据位置

数据位置对性能有着比较大的影响。如果数据和代码在同一个节点上的同一个excutor上会很快。但是如果不在一起的话,要么移动代码,要么移动数据。一般来说spark会移动代码,因为代码的大小一般远小于数据。数据的位置,从近到远可以分为以下几个等级:

  • PROCESS_LOCAL 数据和代码在同一个JVM中,这是最好的情况
  • NODE_LOCAL 数据和代码在同一个节点的不同excutor中,这需要进程间的通信
  • NO_PREF 数据在任何节点上的访问速度是一样的
  • RACK_LOCAL 数据在同一个机架上的不同服务器上,这需要通过一个交换机来进行网络传输
  • ANY数据在不同机架的服务器上,也是最差的情况

spark会尽可能的使用更近的位置,但是如果某些空着的执行程序上没有数据要处理,而繁忙的服务器上有数据,那有两种策略:1.等待那个繁忙的服务器CPU空闲,在那个服务器上继续跑任务。2.在空闲的服务器上新起个任务,把数据转移过去执行。
spark会等一小段时间,如果超过这个时间CPU还没空闲下来就采用第二种策略。这个等待的时间是可以调整的,通过spark.local配置进行修改。默认的配置表现一般都还不错

4. 总结

前面提到的最重要的调整策略就是数据序列化和内存调优,最最通用的方法是使用Kryo序列化的方式,它通常能解决大部分的问题。

你可能感兴趣的:(大数据,spark)