spark性能调优

2 篇文章 0 订阅
1 篇文章 0 订阅

Spark性能调优

spark程序可能被集群中任何资源托慢速度,比如CPU, 网络带宽,内存。 如果数据能全部放入内存, 那么瓶颈很可能就会是带宽。但是有的时候,你需要用序列化(serialization)的形式存储RDD, 这样可以节约内存, 本文关注两个主题, 数据序列化(对提高网络性能和降低内存都非常重要)  和 内存调优。

Data Serialization

序列化对分布式应用的性能有重要影响。不已序列化 或者 占用很多字节的 格式 会显著脱慢计算。一般来说,如果你向调优spark应用,那么你首先应该调优序列化。spark 试图在方便和性能间平衡,spark提供两个序列化库:

  • Java serialization:默认情况下,spark使用java的ObjectOutputStream框架来序列化对象, 只要你实现了java.io.Serializable接口。你也可以实现java.io.Externalizable来更加精确的控制性能。java serialization 非常灵活,但是有点慢,而且对很多类会产生很大的序列化格式。
  • Kryo serialization: spark支持Kryo 库。kryo比java 序列化更加快,而且更加紧凑(经常超过10倍)。但是不支持所有的类型,而且对要在程序中使用的类要进行注册。

你可以在初始化你的job时 切换成使用kryo序列化,使用的方法是调用

conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
此项配置不仅对工作节点间传输数据生效, 同时对序列化RDD到磁盘也生效。之所以Kryo不是默认选项,完全是因为需要注册(custom registration requirement)。如果你的应用是网络密集型,我们推荐你使用Kryo 序列化。

对大多数经常使用的核心scala 类,spark 都包括了他们的Kryo serializers, 你可以在Twitter chill 中找到他们

要想使用Kryo注册你自己的类, 只需要实现org.apache.spark.serializer.KryoRegistrator ,然后设置spark.kryo.registrator属性。如下所示:

import com.esotericsoftware.kryo.Kryo
import org.apache.spark.serializer.KryoRegistrator

class MyRegistrator extends KryoRegistrator {
  override def registerClasses(kryo: Kryo) {
    kryo.register(classOf[MyClass1])
    kryo.register(classOf[MyClass2])
  }
}

val conf = new SparkConf().setMaster(...).setAppName(...)
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
conf.set("spark.kryo.registrator", "mypackage.MyRegistrator")
val sc = new SparkContext(conf)

Kryo 文档  描述了其他的注册选项,包括如何添加自己的序列化代码

如果你的对象很大, 你需要增加 spark.kryoserializer.buffer.mb属性来放置你的对象, 这个属性默认是2。

最后,如果你不注册你的类, Kryo仍然能工作, 不过会存储每个对象的类全名, 有点浪费。

Memory Tuning

在内存调优时有3点考虑: 你的所有对象要使用的内存和,访问对象的花费, 内存回收的开销。

默认情况下,访问java对象是很快的, 但是可能会消耗raw 数据的2到5倍空间,这是因为:

  • 每个java对象都有一个对象头(object header), 这是一个16字节的数据,包含了指向类的指针等数据。对那种只有很少数据的对象(比如一个Int),对象头可能比数据还要大
  • Java String 比 raw 数据多40个字节,而且用2个字节存储每个字符(因为String 使用UTF-16编码)。比如一个10个字符的String消耗60字节
  • 普通的容器类,比如HashMap, LinkedList, 使用链接数据结构, 对每个数据进行封装。这中对象不仅有header, 而且有指向下一个对象的指针(8个字节)。
  • 原生类型的容器也会使用封装存储数据, 比如java.lang.Integer

本节会讨论如何确定你的对象的内存用量, 以及如何优化。 可以通过改变数据结构, 或者用序列化格式存储数据。 本节包含了spark缓存调优 和 Java垃圾回收。

Determining Memory Consumption

测量你的数据集内存消耗的最好方法是, 创造一个RDD, 放入cache, 然后看你的驱动程序的SparkContext的日志。日志会告诉你每个partition使用了多少内存, 你可以累加partition数量来算出RDD的总内存需求。就向下面一样:

INFO BlockManagerMasterActor: Added rdd_0_1 in memory on mbk.local:50311 (size: 717.5 KB, free: 332.3 MB)

这表示,RDD0的partition 1消耗了717.5KB。

Tuning Data Structures

减少内存消耗的第一部是避免使用增加开销的Java 特性, 比如基于指针(pointer-based)的数据结构和封装对象。

  1. 尽量使用对象数组和原始类型,不要使用Java 和Scala容器(比如HashMap),fastutil 库提供了方便使用的原始类型容器类,而且和Java标准库兼容。
  2. 避免使用包含很多小对象和指针的嵌套数据结构
  3. 使用数字ID或者枚举类型来表示key, 而不要使用string
  4. 如果你的RAM低于32GB, 设置JVM flag -XX:+UseCompressedOops 来保证指针长度为4而不是8。 你可以在spark-env.sh中添加这个选项。

Serialized RDD Storage

如果你安装以上内容调优还是无法有效存储你的对象, 那么一个简单的方法就是将对象以序列化的形式存储。方法是在RDD persistence API 中使用序列化存储级(serilizedStorageLevels),例如 MEMORY_ONLY_SER。Spark会把每个RDD partition 以字节数组的形式存储。采用此方法的唯一坏处是会降低访问效率。我们强烈建议使用Kryo库来序列化数据,因为他比Java序列化节省更多的空间。

Garbage Collection Tuning

当你程序中存在大量不会使用的RDD时,JVM垃圾回收器会成为一个问题。当JAVA 需要弹出旧对象来为新对象腾出空间时, 它会追踪所有的JAVA对象来寻找不使用的对象。你要搞明白,垃圾回收的花费正比与JAVA对象的数量, 所以不要用有很多小对象的数据结构(使用Int数组, 而不要使用LinkedList)。更好的方法是以序列化的形式来存储对象,这样垃圾回收的时候每个partition就只有一个对象了(因为整个partition被存为一个大数组)。在尝试其他技术来解决垃圾回收器之前,最好县使用serialized caching

当你程序的工作内存 和 缓存到节点的RDD 间有内存竞争时,垃圾回收也会成为一个问题。我们会讨论如何控制分配给RDD的内存来缓解这个问题。

Measuring the Impact of GC

垃圾回收调优的第一步是统计垃圾回收的频率和垃圾回收花费的时间。 你可以在你的SPARK_JAVA_OPTS环境变量中添加-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 来统计。运行你的spark任务时,你会在你的每个工作节点上找到关于垃圾回收的日志。

Cache Size Tuning

对垃圾回收来说一个重要的配置是RDD缓存内存量, 默认情况下,Spark使用60%的配置执行内存(spark.executor.memory)来缓存RDD, 也就是说工作程序有40%的内存来存放所需要的对象。

如果你的程序很慢,或者你发现JVM经常回收内存,或者经常耗尽内存。你可以降低RDD缓存内存量来降低RDD对内存的消耗。比如说降低到50%, 你可以通过调用conf.set("spark.storage.memoryFraction", "0.5")来完成。使用以上技术,可以缓解大部分垃圾回收问题。以下时垃圾回收进阶调优。

Advanced GC Tuning

为了进一步垃圾回收调优, 我们需要理解JVM中内存管理的基本信息:

  • Java 堆 被分成 两个区域, 新区域存放短暂的对象,而旧区域存放长生存时间的对象。
  • 新区域又被分为3个区域 [Eden, Survivor1, Survivor2]。
  • 垃圾回收进程的一个简单描述:当Eden满了时,一个次级垃圾回收器会在Eden中调用,那些生存下来的对象和Survivor1中的对象被拷贝到Survivor2。然后两个Survivor区域交换,如果一个对象存活了足够的时间,或者Survivor2满了,它会被移动到旧区。 最后当旧区快要满的时候,一个完整的垃圾回收器会被调用。

Spark 的垃圾回收调优的目的时保证只有长时间生存的RDD会被存放在旧区,而新区要足够容纳短暂的对象。这避免了完整垃圾回收器区收集任务执行过程中产生的对象。以下步骤可能有用:



  • 收集垃圾回收的数据,检查是否进行了过多的垃圾回收。如果任务完成前,垃圾回收被调用很多次,那么说明没有足够的内存运行任务。
  • 检查垃圾回收的数据,如果旧区域接近满的话(即没满),降低缓存RDD的内存量。你可以使用spark.storage.memoryFraction来修改。因为少缓存几个对象总比拖慢任务执行来的好!
  • 如果有过多的次级回收,却不是很多完整回收。你可以为Eden分配更多的内存。你可以设置Eden的大小为略微超过任务执行所需要的内存。如果任务执行需要E, 那么你可以设置新区的大小通过选项-Xmn=4/3*E
  • 如果你需要从HDFS中读数据,可以用从HDFS中读到的数据快大小来估计任务所需的内存。一般,解压过的块会占据2到3倍原始快的内存。所以如果你有3到4个任务要使用工作内存, 你可以估计Eden的大小为 4*3*64MB, 每个HDFS的块约等于64MB。
  • 监视新的设置后内存回收的频率和花费。

经验告诉我, 内存回收调用取决于你的应用以及可用的内存。网上有很多调优的方法, 总的来说,管理内存回收的次数可以有效降低开销。

Other Considerations

Level of Parallelism

调高操作的并行性可以有效利用集群。Spark根据文件大小设置每个文件"map"任务的数量(你也可以通过SparkContext.textFile的选项来手动控制)。对于"reduce"操作,比如groupByKey, reduceByKey。Spark根据父亲RDD最大的partition数来设置"reduce"任务。你可以传递操作的第二个参数来控制并行数(参考spark.PairRDDFunctions),或者设置spark.default.parallelism来改变默认并行数。我们推荐每个CPU 2到3个任务。

Memory Usage of Reduce Tasks

有时,你得到OutOfMemoryError并不是因为你的RDD无法放入内存,而是因为你的一个任务(比如groupByKey)太大。Spark的混洗(shuffle)操作(sortByKey,groupByKey, reduceByKey, join等)会建立一个供group使用的哈希表,这个哈希表有时会很大。最简单的解决办法是提高并行级,这样每个任务的输入集就会比较小。 因为每个工作节点的所有任务都重复利用JVM, 以至于Spark Spark能有效支持200ms级的任务,因此你可以放心的提高并行等级哪怕超过你集群的核数。

Broadcasting Large Variables

利用好SparkContext的广播功能可以有效降低序列化的大小, 和开启一个job的开销。如果你的任务使用驱动程序中的大对象(比如静态查找表),你应该考虑把它装入广播变量。Spark会在驱动程序中打印每个任务的序列化大小,你可以通过这个来判断你的任务是否过大。总的来说, 超过20KB的任务值得优化。

Summary

对大多数程序,使用Kryo序列化以及以序列化的方式存储数据足够解决一般的性能问题。如果你还有问题,可以发邮件到 Spark mailing list来提问。


### 本文中的序列化指 serialization

### 本文来自于 http://spark.apache.org/docs/latest/tuning.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值