Spark内存调优

由于Spark是基于内存计算的,所以集群中资源(比如CPU、带宽、内存)都会成为瓶颈。当集群内存够用时,网络带宽往往成为瓶颈。内存优化主要从两个方面考虑:一个是数据序列化(提升网络性能,减少内存使用等),一个是内存优化

数据序列化

序列化在分布式应用程序中占用重要地位。Spark提供两种序列化库:

  • Java serialization 拿来就用,但是太慢了。
  • Kryo serialization 比Java serialization更快,压缩效果更好。缺点是不支持所有Serializable 类型,需要另外把自定义的序列化类进行注册。

启用Kryo 时,需在SparkConf 中设置参数

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

设置的序列化器不仅仅应用于worker之间的数据shuffle,也用于序列化RDDs到磁盘。
官方推荐在网络敏感的应用中使用Kryo

从Spark2.0.0开始,Kryo序列化器在shuffle中已经对基础数据类型、基础数据类型的数组类型、字符串做了支持。只不过你自己定义的Kryo类需要使用registerKryoClasses 来注册:

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

Kryo document链接
如果你的对象很大,需要设置一下spark.kryoserializer.buffer参数,使缓存能够装下你的对象。当然也可以不注册定义类,Kryo照样能用,但是它必须为每一个对象记住全类名,这是很浪费的。

内存优化

众所周知,java对象一般大小一般是原始数据大小的2-5倍,因为:

  • 每一个java对象都有一个16字节的对象头(包含很多信息,比如指向class对象的指针)
  • java字符串要比原始数据多出约40字节,用于被保存其他数据,比如长度。而且每个char使用2字节来保存,因为内部使用UTF-16来编码。这样10个char组成的字符串要占60字节的空间。
  • 其他常用集合类,比如HashMap、LinkedList使用了链表结构,每一个wrapper对象对应每一个entry,比如Map.Entry,这些对象不仅含有对象头,还包含8个字节的指针指向连表的下一个对象。
  • 基础类型的集合类中的基础类型数据通常是包装类

Spark的内存主要使用在两个地方:
计算内存:用于shuffle、连接、排序、聚合等中间过程
存储内存:用于cache、集群中分发数据比如广播变量

计算内存与存储内存共享一个区域(M),如果计算内存不用,则存储内存会贪婪地耗尽所有内存,反之亦然。计算内存在必要的时候踢出一些存储内存,知道存储内存降低到一个阈值(R),也就是说存储内存至少有R的内存可以用,而计算内存无法使用。如果不用cache,应用程序将使用所有内存用于计算,避免不必要的磁盘溢写。这里有几个相关的配置,大多数情况下保持默认是比较不错的选择:

  • spark.memory.fraction 代表M占整个堆空间的比例,默认0.6。剩下的40%用于用户数据结构、spark内部元数据、防范突如其来的大数据集造成OOM。
  • spark.memory.storageFraction 代表R占M的比例,默认0.5。

spark.memory.fraction 应设置与JVM 老年代和永久代占用空间适配。

怎么确定一个对象的大小呢?
确定一个RDD的大小时,可以将它cache一下,然后再UI上查看Storege页面。
确定一个普通对象大小事,可以使用Spark提供的伴生对象SizeEstimator的estimate 方法,可用来测量广播变量的大小。
怎么优化数据结构呢?
首先避免使用java特性过了头,比如基于指针的数据结构、包装类等。
1.设计数据结构,优先使用数组、基础类型,而不是java或scala集合类。官网推荐fastutil库来提供基础类型的集合类,并且它与java标准集合类兼容。
2.避免使用很多小对象、指针引用的嵌套结构
3.尽量使用数值ID或枚举来代替字符串型key
4.如果机器内存小于32GB,设置JVM参数-XX:+UseCompressedOops 开启压缩指针(指针从8字节压缩到4字节)。通常在spark-env.sh中添加。
如果以上优化还不能高效利用内存,可考虑在内存中序列化对象
比如RDD的持久化中选择序列化的级别,比如MEMORY_ONLY_SER,当然也相应降低了一些访问速度。推荐使用Kryo。

Java垃圾回收优化

垃圾回收的代价跟java对象的数量成正比。使用的对象数目越少,代价越小。比如前面提到的,将数据对象序列化以后成为一个字节数组以后,有且仅有一个对象。

GC优化的第一步就是收集gc频率以及gc耗时数据。可以通过给java虚拟机参数添加-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps。详情可参考配置。这样就可以在worker上看到job的gc情况。而调优的对象一般是Executor,不是Driver。

GC优化的目标就是保证长期存活RDDs被存储在老年代,而新生代有足够的的空间来存放短命对象。这样就可以避免收集临时对象时出现full GC。我们可以这么做:
1.通过收集的GC数据,检查是否有大量GC出现。如果一些Task完成前,出现很多次full GC,说明堆内存空间不足。
2.如果有很多minor GC 而没有很多major GC,分配多一些内存给Eden将有帮助,比如配置成原来的4 /3倍,同样也可以成比例的给survivor。
3.如果老年代快满了,那么可以调低spark.memory.fraction参数,最好再减少cache的对象数目,或者减少年轻代内存也是可选项。如果还不行,可以调整虚拟机NewRatio,大多数虚拟机默认为2(老年代占2/3的堆空间),使其大到超过spark.memory.fraction。
4.试用G1(-XX:+UseG1GC),在某些场景下可以提升性能。
5.如果从HDFS上读取数据,可根据HDFS的block大小计算内存用量。比如压缩的block经过解压缩以后,大小是原来的2 ~ 3倍,并且有3 ~ 4个Task需要运行,那么Eden的大小应该为4 * 3 * 128MB

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值