原文太长,提炼关键点

  • 数据序列化 (Kryo更快,使用SparkConf初始化作业并调用conf.set(“ spark.serializer”,“ org.apache.spark.serializer.KryoSerializer”)来切换为使用Kryo)
  • 内存调优
  1. 内存管理概述
  2. 确定内存消耗( 确定数据集所需的内存消耗量的最佳方法是创建一个RDD,将其放入缓存中,然后查看Web UI中的“ Storage”页面。该页面将告诉您RDD占用了多少内存。
    要估算特定对象的内存消耗,请使用SizeEstimator的估算方法。
  3. 调整数据结构
  4. 序列化RDD存储
  5. 垃圾收集优化
  • 其他注意事项
  1. 并行度
  2. 减少任务的内存使用
  3. 广播大变量
  4. 数据局部性

 

 

 

 

数据序列化
序列化在任何分布式应用程序的性能中都起着重要作用。将对象慢速序列化或占用大量字节将大大减慢计算速度。通常,这是您应该优化Spark应用程序的第一件事。 Spark旨在在便利性(允许您在操作中使用任何Java类型)和性能之间取得平衡。它提供了两个序列化库:

Java序列化:默认情况下,Spark使用Java的ObjectOutputStream框架对对象进行序列化,并且可以与您创建的实现java.io.Serializable的任何类一起使用。您还可以通过扩展java.io.Externalizable来更紧密地控制序列化的性能。 Java序列化很灵活,但是通常很慢,并且导致许多类的序列化格式很大。
Kryo序列化:Spark还可以使用Kryo库(版本4)更快地序列化对象。与Java序列化(通常多达10倍)相比,Kryo显着更快,更紧凑,但是它不支持所有Serializable类型,并且需要您预先注册要在程序中使用的类才能获得最佳性能。
您可以通过使用SparkConf初始化作业并调用conf.set(“ spark.serializer”,“ org.apache.spark.serializer.KryoSerializer”)来切换为使用Kryo。此设置配置了不仅用于在工作节点之间Shuffling数据,而且还在将RDD序列化到磁盘时使用。 Kryo不是默认值的唯一原因是由于自定义注册要求,但是我们建议在任何网络密集型应用程序中尝试使用它。从Spark 2.0.0开始,在对具有简单类型,简单类型数组或字符串类型的RDD进行Shuffling时,我们在内部使用Kryo序列化程序。

Spark自动为Twitter chill库的AllScalaRegistrar中涵盖的许多常用Scala核心类包括Kryo序列化器。

要向Kryo注册您自己的自定义类,请使用registerKryoClasses方法

 

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

 

Kryo文档介绍了更高级的注册选项,例如添加自定义序列化代码。

如果对象很大,则可能还需要增加spark.kryoserializer.buffer配置。该值必须足够大以容纳要序列化的最大对象。

最后,如果您不注册自定义类,Kryo仍然可以使用,但必须将完整的类名与每个对象一起存储,这很浪费。

 

内存调优
调整内存使用情况时需要考虑三个方面:对象使用的内存量(您可能希望整个数据集都适合内存),访问这些对象的成本以及垃圾回收的开销(如果您有很高的垃圾回收量)条款)。

默认情况下,Java对象的访问速度很快,但是与其字段中的“原始”数据相比,它们很容易消耗2-5倍的空间。这是由于以下几个原因:

每个不同的Java对象都有一个“对象头”,它大约16个字节,并包含诸如指向其类的指针之类的信息。对于其中数据很少的对象(例如一个Int字段),该对象可能大于数据。
Java字符串在原始字符串数据上有大约40个字节的开销(因为它们存储在Chars数组中并保留诸如长度之类的额外数据),并且由于String内部使用UTF-16编码,因此每个字符存储为两个字节。因此,一个10个字符的字符串可以轻松消耗60个字节。
常见的集合类(例如HashMap和LinkedList)使用链接的数据结构,其中每个条目(例如Map.Entry)都有一个“包装”对象。该对象不仅具有标题,而且还具有指向列表中下一个对象的指针(通常每个指针8个字节)。

 

内存调优
调整内存使用情况时需要考虑三个方面:对象使用的内存量(您可能希望整个数据集都适合内存),访问这些对象的成本以及垃圾回收的开销(如果您有很高的垃圾回收量)条款)。

默认情况下,Java对象的访问速度很快,但是与其字段中的“原始”数据相比,它们很容易消耗2-5倍的空间。这是由于以下几个原因:

每个不同的Java对象都有一个“对象头”,它大约16个字节,并包含诸如指向其类的指针之类的信息。对于其中数据很少的对象(例如一个Int字段),该对象可能大于数据。
Java字符串在原始字符串数据上有大约40个字节的开销(因为它们存储在Chars数组中并保留诸如长度之类的额外数据),并且由于String内部使用UTF-16编码,因此每个字符存储为两个字节。因此,一个10个字符的字符串可以轻松消耗60个字节。
常见的集合类(例如HashMap和LinkedList)使用链接的数据结构,其中每个条目(例如Map.Entry)都有一个“包装”对象。该对象不仅具有标题,而且还具有指向列表中下一个对象的指针(通常每个指针8个字节)。
基本类型的集合通常将它们存储为“装箱的”对象,例如java.lang.Integer。
本节将首先概述Spark中的内存管理,然后讨论用户可以采取的特定策略,以更有效地使用其应用程序中的内存。特别是,我们将介绍如何确定对象的内存使用情况以及如何通过更改数据结构或以串行化格式存储数据来改善对象的使用情况。然后,我们将介绍调整Spark的缓存大小和Java垃圾收集器。

内存管理概述
Spark中的内存使用情况大体上属于以下两类之一:执行和存储。执行内存是指用于shuffles,联接,排序和聚合中的计算的内存,而存储内存是指用于在集群中缓存和传播内部数据的内存。在Spark中,执行和存储共享一个统一的区域(M)。当不使用执行内存时,存储可以获取所有可用内存,反之亦然。如果有必要,执行可能会驱逐存储,但只有在总存储内存使用率下降到某个阈值(R)以下时,才可以执行该操作。换句话说,R描述了M内的一个子区域,在该子区域中永远不会逐出缓存的块。由于实现的复杂性,存储可能无法退出执行。

这种设计确保了几种理想的性能。首先,不使用缓存的应用程序可以将整个空间用于执行,从而避免了不必要的磁盘溢出。其次,确实使用缓存的应用程序可以保留最小的存储空间(R),以免其数据块被逐出。最后,这种方法可为各种工作负载提供合理的即用即用性能,而无需用户了解如何在内部划分内存。

尽管有两种相关的配置,但典型用户无需调整它们,因为默认值适用于大多数工作负载:

spark.memory.fraction将M的大小表示为(JVM堆空间-300MB)的分数(默认值为0.6)。其余空间(40%)保留用于用户数据结构,Spark中的内部元数据,并在记录稀疏和异常大的情况下防止OOM错误。
spark.memory.storageFraction将R的大小表示为M的分数(默认值为0.5)。 R是M内的存储空间,其中的高速缓存块不受执行驱逐。
应该设置spark.memory.fraction的值,以便在JVM的旧生代中舒适地适应堆空间量。有关详细信息,请参见下面有关高级GC调整的讨论

确定内存消耗
确定数据集所需的内存消耗量的最佳方法是创建一个RDD,将其放入缓存中,然后查看Web UI中的“ Storage”页面。该页面将告诉您RDD占用了多少内存。

要估算特定对象的内存消耗,请使用SizeEstimator的估算方法。这对于尝试使用不同的数据布局以减少内存使用量以及确定广播变量将在每个执行程序堆上占用的空间量很有用。

调整数据结构
减少内存消耗的第一种方法是避免使用Java功能,这些功能会增加开销,例如基于指针的数据结构和包装对象。做这件事有很多种方法:

设计数据结构以使用对象数组和原始类型,而不是标准Java或Scala集合类(例如HashMap)。 fastutil库为与Java标准库兼容的原始类型提供了方便的collection类。
尽可能避免使用带有许多小对象和指针的嵌套结构。
考虑使用数字ID或枚举对象代替键的字符串。
如果您的RAM少于32 GB,则将JVM标志-XX:+ UseCompressedOops设置为使指针为四个字节而不是八个字节。您可以在spark-env.sh中添加这些选项。
序列化RDD存储
当您的对象仍然太大而无法进行优化存储时,减少内存使用的一种更简单的方法是使用RDD持久性API中的序列化StorageLevel(例如MEMORY_ONLY_SER)以序列化形式存储它们。然后,Spark将每个RDD分区存储为一个大字节数组。由于必须动态地反序列化每个对象,因此以串行形式存储数据的唯一缺点是访问时间较慢。如果您想以序列化形式缓存数据,我们强烈建议使用Kryo,因为它导致的大小比Java序列化(当然也比原始Java对象)小。

垃圾收集优化
如果您在程序存储的RDD方面有较大的“搅动”,则JVM垃圾回收可能会成为问题。 (在只读取RDD一次然后对其执行许多操作的程序中,这通常不是问题。)当Java需要逐出旧对象为新对象腾出空间时,它将需要遍历所有Java对象并查找未使用的。这里要记住的要点是,垃圾回收的成本与Java对象的数量成正比,因此,使用对象较少的数据结构(例如,使用Ints数组而不是LinkedList的数据结构)会大大降低此成本。更好的方法是如上所述以序列化的形式持久化对象:现在,每个RDD分区只有一个对象(字节数组)。在尝试其他技术之前,如果GC有问题,首先要尝试使用序列化缓存。

由于任务的工作内存(运行任务所需的空间量)与节点上缓存的RDD之间的干扰,GC也会成为问题。我们将讨论如何控制分配给RDD缓存的空间以减轻这种情况。

衡量GC的影响

GC调整的第一步是收集有关垃圾收集发生频率和花费GC时间的统计信息。这可以通过在Java选项中添加-verbose:gc -XX:+ PrintGCDetails -XX:+ PrintGCTimeStamps来完成。 (有关将Java选项传递给Spark作业的信息,请参阅配置指南。)下一次运行Spark作业时,每次发生垃圾回收时,您都会在工作日志中看到打印的消息。请注意,这些日志将位于群集的工作节点上(位于其工作目录的stdout文件中),而不位于驱动程序上。

高级GC调整

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

Java Heap空间分为Young和Old两个区域。年轻一代用于保存寿命短的对象,而老一代则用于寿命更长的对象。

年轻一代又分为三个区域[Eden,Survivor1,Survivor2]。

垃圾收集过程的简化描述:当Eden已满时,将在Eden上运行次要GC,并将来自Eden和Survivor1的活动对象复制到Survivor2。幸存者区域被交换。如果对象足够旧或Survivor2已满,则将其移到“旧”。最后,当Old接近满时,将调用完整的GC。

在Spark中进行GC调整的目标是确保在旧生代中仅存储长寿命的RDD,并确保新生代具有足够的大小以存储短寿命的对象。这将有助于避免完整的GC收集任务执行期间创建的临时对象。可能有用的一些步骤是:

通过收集GC统计信息检查是否有太多垃圾回收。如果在任务完成之前多次调用一个完整的GC,则意味着执行程序没有足够的内存

 

其他注意事项
并行度 Level of Parallelism
除非您为每个操作设置足够高的并行度,否则群集将无法充分利用。 Spark会根据文件的大小自动设置要在每个文件上运行的“映射”任务的数量(尽管您可以通过SparkContext.textFile等可选参数来控制它),并可以进行分布式的“ reduce”操作(例如groupByKey和reduceByKey),它使用最大的父RDD分区数。您可以将并行性级别作为第二个参数传递(请参见spark.PairRDDFunctions文档),或设置config属性spark.default.parallelism来更改默认值。通常,我们建议集群中每个CPU内核执行2-3个任务。

减少任务的内存使用
有时,您会收到OutOfMemoryError的原因不是因为您的RDD不能容纳在内存中,而是因为您的一项任务(例如groupByKey中的某个reduce任务)的工作集太大。 Spark的随机操作(sortByKey,groupByKey,reduceByKey,join等)会在每个任务中建立一个哈希表来执行分组,而分组通常会很大。此处最简单的解决方法是提高并行度,以使每个任务的输入集更小。 Spark可以高效地支持短至200 ms的任务,因为它可以在多个任务中重用一个执行器JVM,并且任务启动成本低,因此您可以安全地将并行级别提高到集群中核心的数量以上。

广播大变量
使用SparkContext中可用的广播功能可以大大减少每个序列化任务的大小,以及在集群上启动作业的成本。如果您的任务使用驱动程序中的任何大对象(例如静态查找表),请考虑将其变成广播变量。 Spark在主服务器上打印每个任务的序列化大小,因此您可以查看它来确定任务是否太大;通常,大约20 KB以上的任务可能值得优化。

数据局部性
数据局部性可能会对Spark作业的性能产生重大影响。如果数据和对其进行操作的代码在一起,则计算速度往往会很快。但是,如果代码和数据是分开的,那么一个必须移到另一个。通常,从一个地方到另一个地方传送序列化代码要比块数据更快,因为代码大小比数据小得多。 Spark围绕此数据局部性一般原则构建其调度。

数据局部性是数据与处理它的代码之间的接近程度。根据数据的当前位置,可以分为多个级别。从最远到最远的顺序:

PROCESS_LOCAL数据与正在运行的代码位于同一JVM中。这是最好的位置
NODE_LOCAL数据在同一节点上。示例可能在同一节点上的HDFS中,或者在同一节点上的另一执行程序中。这比PROCESS_LOCAL慢一点,因为数据必须在进程之间传输
NO_PREF数据可以从任何地方快速访问,并且不受位置限制
RACK_LOCAL数据位于同一服务器机架上。数据位于同一机架上的其他服务器上,因此通常需要通过单个交换机通过网络发送
任何数据都在网络上的其他位置,而不是在同一机架中
Spark倾向于在最佳位置级别安排所有任务,但这并不总是可能的。在任何空闲执行器上没有未处理数据的情况下,Spark会切换到较低的本地级别。有两种选择:a)等待忙碌的CPU释放以在同一服务器上的数据上启动任务,或b)立即在需要将数据移动到更远的地方启动新任务。

Spark通常要做的是稍等一下,以期释放繁忙的CPU。一旦超时到期,它将开始将数据从很远的地方移到空闲的CPU中。每个级别之间的回退等待超时可以单独配置,也可以一起配置在一个参数中。有关详细信息,请参见配置页面上的spark.locality参数。如果您的任务很长并且位置不佳,则应该增加这些设置,但是默认设置通常效果很好。

摘要
这是一个简短的指南,指出了在调整Spark应用程序时应了解的主要问题-最重要的是数据序列化和内存调整。对于大多数程序,切换到Kryo序列化并以序列化形式保留数据将解决大多数常见的性能问题。随时在Spark邮件列表中询问其他调优最佳实践。

 

草稿箱里放了太久了,看了一下有些内容过时了