来自官网翻译
内存调优
- 每个独立的对象都有一个16bytes的对象头,包含一些例如类的指针等这些信息。如果对象数据本身很小,但是由于携带了对象头,就会占用更多的空间
- java String类型比原始字符串数据多占用40bytes(因为数据是存在一个数组里,并且保存了例如长度等的额外信息),String类型的内部使用UTF-16编码,所以每个字符占用2bytes。例如对于一个10个字符的字符串要消耗60bytes。
- 常用的集合类,比如 HashMap和LinkedList,通常使用linked node这样的数据结构,这样对每个数据节点就存在一个“wrapper”对象(外层包裹数据的对象)。这个对象不仅有一个对象头,还有存储指向下一个节点的指针。
- 原生数据类型的集合通常需要装箱存储,例如java.lang.Integer。
内存管理概述
spark主要在两个地方使用内存:执行和存储。执行时的内存指的是在shuffle,join,sort,aggregation操作时需要的内存,存储内存指用来在集群中缓存和传播数据。执行和存储共享同一块区域(M)。当执行操作占用一部分内存后,存储就可以使用剩下的全部内存,反之亦然。必要时,执行可以驱逐存储占用的内存,直到总存储内存使用量低于某个阈值(R)。换句话说,R 描述了M中的一个子区域,其中缓存的数据从来不会被驱逐。存储永远不会驱逐执行。
尽管有两个相关的配置,但是默认值也能满足大部分应用的需求
spark.memory.fraction 代表M 的大小(默认值是0.6),指占用jvm堆空间的比例,剩下的40%的空间是给用户自己的数据和spark原数据保留的。
spark.memory.storageFraction 代表R的大小(默认值是0.5),指占用M空间的比例
确定内存占用量
确定一个数据集内存消耗量的最好的方式是创建一个RDD,然后把它放到cache中,在web UI中看一个"Storage"页面。这个页面会告诉你这个RDD占用了多少内存。
可以使用SizeEstimator的estimate方法估计特定对象的内存使用量。这对于试验不同的数据布局以减少内存使用,以及确定广播变量在每个执行程序堆上占用的空间大小非常有用。
优化数据结构
减少内存消耗的第一步是避免java对象的额外开销,例如基于指针的数据结构和wrapper对象。下面是一些实践方法:
- 优先使用数组和原生类型,而不是java或scala里面的标准集合类。fastutil库对原生类型提供了一些和java标准库兼容的集合类。
- 尽可能避免使用大量小对象和指针的嵌套结构。
- 使用数字id或枚举对象而不是字符串作为键。
- 如果你的内存小于32GB,设置jvm参数-XX:+UseCompressedOops 可以让指针占用4bytes而不是8bytes,这个参数可以添加在spark-env.sh中。
序列化的RDD存储
尽管经过优化,你的对象仍然因为太大而无法有效存储,那就可以使用StorageLevels以序列化的方式把RDD存起来,例如MEMORY_ONLY_SER。spark会把每个RDD分区存到一个大数组里。序列化存储的唯一缺点是访问时间较慢,因为必须动态地反序列化才能使用。
推荐使用Kyro,它比原生java序列化占用空间更小。
优化垃圾回收
总体来说就查看是否经常fullGC,可以根据hdfs文件块大小调整Eden大小(倍数),使用G1收集器。如果老年代要满了可以调节spark.memory.fraction降低缓存使用量,或者降低新生代的比例
其他注意事项
并行度
spark会根据文件的大小自动设置要在每个文件上运行的map任务的数量(也可以通过SparkContext的可选参数来控制它)。对于reduce类操作,例如groupByKey和reduceByKey,会使用最大的父RDD的分区数量,也可以把并行度作为第二个参数传进入(详见 spark.PairRDDFunctions 文档)。spark.default.parallelism可以设置默认的并行度,推荐每个cpu核心运行2-3个task
reduce task的内存使用情况
有时候报OutOfMemoryError 可能是由于任务的工作集太大了。spark shuffle操作(sortByKey,groupByKey,reduceByKey,join等)在每个task中会构建一个哈希表来执行分组,分组通常会比较大。简单的解决办法就是增加并行度来,这个每个任务的数据数据集就变小了。spark可以有效的支持各种200ms时间需求的任务,因为它可以让多个task重用executor 的jvm,启动task成本很低,因此可以安全的增加并行度到集群的核心数。
广播大的变量
在SparkContext中 使用broadcast functionality 可以大大减少每个序列化task的大小,以及在集群上启动作业的成本。如果在driver中使用了任何较大的对象,可以考虑使用广播变量来优化。可以在master看到spark 打印的每个序列化task的大小,大于20KB的任务可能就需要优化。
数据本地性
数据本地性可以对Spark作业的性能产生重大影响
数据本地性有几个级别:
· PROCESS_LOCAL 数据与运行的代码在同一个jvm中,这是最好的情况
· NODE_LOCAL 数据和任务在相同的节点
· NO_PREF 数据在别的可以快速访问的地方,但不在本地
· RACK_LOCAL 数据在相同的机架上,因此需要走网络,通常要经过交换机
spark优先按照数据本地性原则分配任务,但有时候却做不到。有些情况下,有的空闲的executor没有要处理的数据,spark会降低本地性级别。这就有两个选项:a) 等待直到该数据所在节点的任务跑完然后跑新数据的任务。2)立即在空闲的节点起一个新任务把数据拉过来。
通常spark会先等一会儿,一旦到了超时时间,它就会在空闲的节点起一个新任务。每个级别的等待超时时间可以单独配置,也可以都放在一个参数里;详细可以看下configuration page 中关于spark.locality 的参数。如果任务运行时间很长且本地性不佳,可以修改这个写参数调节,但是默认的参数也还可以。