翻译自Spark官方文档 Tuning Spark,由团队的月禾mm初审,以及微博上的Spark达人@crazyjvm复审,非常感谢
由于Spark大部分计算在内存中进行的特性,Spark程序会受到集群中以下任一因素制约:CPU,网络带宽,或内存。最常见的情况,如果数据和内存大小匹配的话,那么瓶颈就是网络带宽。但是有时候,你还是需要做一些调优,例如storing RDDs in serialized form来减少内存的使用。这个指引将覆盖2个大主题:数据序列化,这对好的网络性能和减少内存使用起关键性作用,以及内存调优。另外我们还会涉及几个小话题。
序列化在任何分布式应用中都扮演着一个重要的角色。那些序列化很慢,或占用大量字节的格式,都将会大大拖慢计算速度。通常,在优化Spark应用时这会是你需要调优的第一件事情。Spark致力于在方便和性能间取得平衡(允许你在操作中使用任何的Java类型),它提供了2个序列化库:
你可以通过在创建SparkContext之前调用System.setProperty(“spark.serializer”, “spark.KryoSerializer”)切换到使用Kryo。我们不使用它作为默认实现的唯一理由,是它需要用户自己注册一下。但是我们推荐使用它,在任何对网络要求较高的应用。
最后,要在Kryo中注册你的类,你需要要建立一个公共类来继承org.apache.spark.serializer.KryoRegistrator类,并设置spark.kryo.registrator的系统属性指向它,如下:
Kryo的文档,描述了更多的高级注册选项,例如添加自定义的序列化代码。
如果你的对象很大,你需要增强Spark.kryoserializer.buffer.mb的系统属性,该值默认是2,但是它需要足够大来容纳你序列化中最大的对象。
最后,如果你不注册你的类,Kryo依然可以工作,但是它会对每个对象都存储全类名,这将是非常浪费的。
内存使用调优有3个考虑点:你的对象使用的内存量(你很可能希望将你所有的数据,装入内存);访问对象的代价;集合垃圾回收的开销(如果从对象角度来说你有较高的进出)
默认来说,Java对象访问起来很快,但是会很容易就消耗比成员变量的原始数据大2-5倍的空间。这是由于如下几个原因:
本章节将会讨论如何判断你的对象的内存使用量,以及如何改进它,无论是通过改变你的数据结构,或者是通过用序列化的方式来存储数据。我们将会谈到调优Spark的缓存大小,还有Java垃圾回收器。
判断内存消耗大小的最佳方法,就是创建一个RDD,把它放进缓存,然后看SparkContext上的Driver程序的日志。日志会告诉你,每个分区消耗了多少的内存,把这些内存加起来,你就能够得到RDD的总大小。你会看到类似主要的消息:
这个代表了RDD 0 的分区 1,消耗了717.5KB的内存。
减少内存消耗的第一方法,是避免使用增加额外负担的Java特性,例如基于指针的数据结构和封装对象。这有几个方法来实现它:
当你的对象太大,以至于不管如何调优,都不能被高效的存储。一个更简单减少内存使用的方式是以序列化的方式存储它们,使用在RDD persistence API中的序列化存储级别,例如MEMORYONLYSER. Spark将会把每个RDD分区作为一个大的字节数组来存储。将数据用序列化的方式存储的唯一缺点是较慢的存取时间,由于需要在运行中反序列化每一个对象。如果你想要以序列化的形式缓存数据,我们强烈的推荐使用Kryo,因为它将比Java序列化小很多。(当然比原始的Java对象更小)
当你有大量作为RDDs的”churn“存储在你程序中时,JVM垃圾回收可能成为一个问题。(如果是对于那些读取一个RDD一次,然后运行很多的操作符的程序,这通常不会成为问题)当Java需要抛弃大量的旧对象来腾出新空间给新对象时,它需要跟踪所有的JAVA对象,来找出无用的那些。要记住的重点是垃圾回收的成本与Java对象的数目成正比,因此选择有更少对象的数据结构(比如数组用Ints而不是LinkedList)将会极大的降低开销。一个更好的办法是将对象以序列化的形式进行保存,正如上面所描述的:现在每一个RDD分块将会只有一个对象(一字节数组)。如果GC存在问题,在尝试其他技术之前,可以先尝试一下序列化缓存(serialized caching)。
由于你的工作内存和缓存在结点上的RDD的相互干扰,GC可能会成为一个问题。我们将讨论如何控制RDD缓存空间分配来缓解这个问题。
垃圾回收调优的第一步,是收集关于垃圾回收发生的的频率和GC耗费时间的统计数据。这可以通过添加-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamp到SPARK_JAVA_OPTS环境变量来完成。你下一次运行Spark Job时,会看到每一次垃圾回收时,消息在Worker的日志中打印出来。注意这些日志会在你的集群Worker节点上(它们工作目录的stdout文件),而不是在你的Driver程序上。
GC一个重要的配置参数,是缓存RDDs需要用的内存量。默认来说,Spark使用66%已配置的Executor内存(spark.executor.memory或者SPARK_MEM)来缓存RDDs。这意味这33%的内存可以用来容纳任务执行中创建的任意对象。
当你的任务变慢,而且你的JVM正在频繁的垃圾回收或者内存溢出时,降低这个值可以帮助减少内存的消耗。假设把它改到50%,你可以调用System.setProperty(“spark.storage.memoryFraction”, “0.5”).结合使用序列化缓存,使用一个更小的缓存,足以缓解大部分的垃圾回收问题。如果你对更进一步的JAVA垃圾回收感兴趣,那么请继续阅读下文。
为了更好的垃圾回收调优,我们首先必须明白一些关于JVM内存管理的基本信息:
Spark垃圾回收调优的目的,在于确保只有长生命周期的RDD被存储在年长代,而年轻代有足够的空间来存储短生命周期的对象。这可以帮助避免过多的Full GC在任务执行的期间收集临时对象。可能有用的步骤如下:
我们的经验建议,垃圾回收调优的效果,取决于你的应用和可用的内存。网上有更多的在线调优的选择,但是在一个高的层次上,控制Full GC发生的频率,可以有效的帮助降低预算。
除非你为每一步操作都设置足够高的并行度,否则集群不会得到最大化的利用。对于”map”操作,Spark会根据文件的大小自动设置在每个文件上的任务数(尽管你可以通过SparkContext.textFile的可选参数,来控制它);对于分布式的”reduce”操作,例如groupByKey和reduceByKey,它会使用最大一个父RDD分区数目作为分块数。你可以传递并行度作为第二个参数(参考spark.PairRDDFunctions文档),或者设置系统属性: spark.default.parallelism来改变默认值。总的来说,在集群中,我们推荐在每个CPU Core上分配2-3个Tasks。
有时你会遇到OutOfMemoryError,不是因为RDDs在内存中装不下了,而是因为你的tasks中有一个工作集,例如在groupByKey中一个Reduce任务太大了。Spark的Shuffle操作(sortByKey,groupByKey,reduceByKey,join,等)会,在每一个任务中建立一个哈希表来进行grouping操作,这个通常会很大。修复这个问题的最简单方式,是增加并行度,使得每个任务的输入集能够更小。因为Spark能够在一个Worker的Jvm中的所有的Task中有效的重用内存,并有一个很低的任务启动开销,它能够支持执行时间短到200ms的任务。所以你可以安全的增加并行度,即使多于你集群中的核数。
使用SparkContext中的广播功能(broadcast functionality),能够很大的降低每个任务的大小,以及在集群上启动一个任务的开销。如果你的任务,调用Driver程序中的任意大对象(例如一个静态查询表),那可以考虑将它转变为一个广播变量。Spark在Master上会打印每个Task序列化后的大小。所以你可以通过查看它,来判断你的Task到底是不是太大了。一般来说,超过20KB大小的Task可能就需要该优化了。
这是一个简单的指引,指出在你优化一个Spark应用时,需要了解的最关键点。最重要的就是数据序列化和内存调优。对于大部分的程序来说,切换到Kryo序列化和用序列化的方式持久化数据,可以解决大部分常见的性能问题。如果有任何关于调优最佳实践的问题,欢迎到Spark的邮件列表中进行询问。
Posted in 数据, 机器学习. Tagged with spark, 调优.
原博客链接http://rdc.taobao.org/?p=2034