由于大多数spark的计算都是内存计算,因此在spark集群中有以下瓶颈:CPU,网络带宽还有内存。如果内存足够的话,主要可能就是网络带宽。目前主要有两种调优方案:数据序列化和内存调整。
序列化在任何分布式应用中都有举足轻重的作用,如果对象被序列化很慢或者序列化后的格式很大,会大大的降低计算的性能。通常来说,这应该是你调优spark应用性能首先要做的事情。spark 提供了两种序列化方式:
ObjectOutputStream
框架来序列化对象,对于你的任何实现了java.io.Serializable
接口的java类都管用。你也可以通过扩展java.io.Externalizable
来提升序列化的性能。Java的序列化比较灵活,但是通常比较慢,它产生的数据格式往往也比较大。你可以通过配置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也能工作,但是会造成极大的浪费。
内存调优主要考虑三个方面:数据对象所占的空间、访问对象的消耗、垃圾回收的消耗。
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.5spark.memory.fraction
一般可以调整以用来适应不同java 版本JVM 堆栈空间大小。
测试数据集所需要占的内存最好的方法就是创建一个RDD,然后cache,在spark UI页面(通常是4040端口)查看 storage
对于一个特定对象的内存消耗,可以使用SizeEstimator
的estimate
方法
减少内存占用首先要考虑的就是去调整数据结构,尽量避免java的基于指针的数据结构和包装器对象。
fastutil
库为与Java标准库兼容的基本类型提供了方便的集合类-XX:+UseCompressedOops
用来将8个字节的指针压缩成4个字节的指针。这个option可以在spark-env.sh
中进行修改。如果调整数据结构依然很大,那可以在RDD 的persistence API 里选择MEMORY_ONLY_SER
或者MEMORY_AND_DISK_SER
,来将对象序列化存储,但是缺点是访问时间会长点,因为需要动态反序列化。如果采用这种方式,强烈建议使用Kryo
序列化方式。这种序列化方式会将每个RDD partition变成一个二进制数组
以上只对java 和 scala 有效,python还是弃疗吧
当程序中有大量RDD流失(用了就不用了),JVM垃圾回收会占用一定的时间,当java需要做gc时,它需要跟踪所有对象并找到未使用的对象。gc的代价与Java 对象个数是成正比的,所以尽量使用少的对象来减少这个开销。当然更好的方法是上面提到的序列化存储,因为会将每个RDD partition变成一个二进制数组对象,这样对象数量大大减少。在尝试其它方式前,建议先尝试这种方式
在gc调优前,首先要分析gc的频率。可以在Java options 里面加上-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
,这样spark 在gc时就会log出来。注意的是如果log到文件,文件会在work nodes下的目录
进一步的gc调优,需要了解一些关于JVM内存管理的基本知识。
gc 调优的目标就是要让长周期的对象在Old区域,Young区域有足够的空间存储短周期的对象。以下是几点建议:
-Xmn=4/3*E
来增大Young区域空间。spark.memory.fraction
降低cache所用的内存,减少cache的对象比减慢程序运行速度要好。还有一种方法是降低Young的内存,也就是减小Xmn
的值。也可以调整JVM的 NewRatio
的值,很多JVM的这个默认值是2,意思是OldGen:YoungGen=2:1,增大这个比值可以增大OldGen的空间。-XX:+UseG1GC
使用G1GC垃圾回收器,它在超大堆的时候表现更加出色,注意,当堆空间很大的时候,也要增加G1区域的空间-XX:G1HeapRegionSize
如果不把spark的并行程度调到足够大的话,集群是不会被完全利用的,可以通过设置spark.default.parallelism
来调整默认的并行度,也可以在具体的一些操作中设置,建议每个cpu 核上运行2 ~ 3个task
有时候,你的程序报OOM异常不是因为你的RDD放不下内存,而是因为你的任务中的某个工作集,比如其中一个groupByKey 的reduce任务
,所占的内存太大。Spark的shuffle操作(sortByKey,groupByKey,reduceByKey,join等等)会创建在每个任务中构建一个hash表来完成分组任务,这个表会很大。一个简单的修复方法是增加并行程度,这样每个任务可以更小。spark可以有效的支持短至200ms的任务,并且多个任务公用一个JVM程序,任务的启动速度很快。所以你可以安全的将任务的并行程度超过集群的核心数。
使用sparkContext中的广播功能可以大大减小序列化任务的大小以及任务的启动时间。如果在任务使用了驱动程序中的大的对象(例如静态查找表),可以将其变成广播变量。spark会在master节点上打印各个task的大小,如果task的大小超过20KB就值得调优了
数据位置对性能有着比较大的影响。如果数据和代码在同一个节点上的同一个excutor上会很快。但是如果不在一起的话,要么移动代码,要么移动数据。一般来说spark会移动代码,因为代码的大小一般远小于数据。数据的位置,从近到远可以分为以下几个等级:
spark会尽可能的使用更近的位置,但是如果某些空着的执行程序上没有数据要处理,而繁忙的服务器上有数据,那有两种策略:1.等待那个繁忙的服务器CPU空闲,在那个服务器上继续跑任务。2.在空闲的服务器上新起个任务,把数据转移过去执行。
spark会等一小段时间,如果超过这个时间CPU还没空闲下来就采用第二种策略。这个等待的时间是可以调整的,通过spark.local
配置进行修改。默认的配置表现一般都还不错
前面提到的最重要的调整策略就是数据序列化和内存调优,最最通用的方法是使用Kryo序列化的方式,它通常能解决大部分的问题。