由于大多数Spark应用都是在内存中计算的,所以,Spark程序的瓶颈可能是集群中的任何资源,比如CPU,网络带宽或者内存等。本指南主要涵盖两个主题:

1.数据序列化。这对于良好的网络性能至关重要,还可以较少内存使用。

2.内存调整。

数据序列化

序列化在任意分布式应用程序的性能中都起着重要的作用。某种格式,假如将对象序列化成该格式很慢,或者该格式消耗大量的字节,那么利用这种格式的话,计算速度会很低。通常,你在优化Spark应用程序时首先应该做的就是调整序列化。Spark旨在在便利性(允许你在应用中可以使用任意Java类型)和性能之间取得平衡。它提供了两个序列化库:

1.Java序列化:默认情况下,Spark使用Java的ObjectOutputStream框架对对象进行序列化,并且可以与你创建的任意java.io.serializable接口实现类一起使用。你还可以通过扩展java.io.Externalizable接口来更细致地控制序列化的性能。Java序列化很灵活,但是通常情况下很慢,而且很多类对应的序列化格式都很大。

2.Kryo序列化:Spark还可以使用Kryo库(版本2)来更快地序列化对象。Kryo比Java序列化速度更快,更紧凑(通常速度高达10倍),但不能支持所有的Serializable接口实现类,并且需要事先注册你将在程序中使用的类,以获得最佳性能。

你可以在初始化SparkConf的时候调用conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")来切换成使用Kryo。这个设置不仅控制各个工作节点间的shuffle数据序列化格式,同时还控制RDD存到磁盘上的序列化格式。Kryo不是默认序列化的唯一原因是Kryo需要自定义注册,但是建议在任何网络密集型应用程序中尝试使用Kryo。自Spark 2.0.0以来,我们在内部使用Kryo序列化对简单类型,简单类型的数组以及字符串类型的RDD进行shuffle。

Spark automatically includes Kryo serializers for the many commonly-used core Scala classes covered in the AllScalaRegistrar from the Twitter chilllibrary.

如果你的自定义类需要使用Kryo序列化,则需要在初始化SparkConf的时候调用conf.registerKryoClasses(Class[] classes)方法先注册。其实,如果不注册的话,Kryo也能工作,不过每一个对象实例的序列化结果都会包含一份完整的类名,这有点浪费空间。

如果你的对象很大,你可能还需要增加spark.kryoserializer.buffer.max配置参数的值,这个值要要足够大,以保存你需要序列化的最大的对象。

内存调整

调整内存使用有三个注意事项:对象使用的内存量,访问这些对象的成本,以及垃圾收集的开销。

默认情况下,Java对象的访问速度很快,但是与其字段中的原始数据相比,占用多达2-5倍的空间。这是由于几个原因:

1.每一个不同的Java对象都有一个对象头,大约有16个字节,并包含诸如指向其类的指针等信息。对于数据非常少的对象(比如只有一个int字段),这可能比数据大。

2.Java字符串与原始字符串数据相比大约有40个字节的多于开销(因为它们将字符串数据存储在一个字符数组中,并保留额外的数据,如长度),并将每个字符存储为两个字节,这是由于字符串内部使用了UTF-16编码。因此一个10个字符的字符串可以很轻松地消耗60个字节。

3.常见的集合类,如HashMap和LinkedList,使用链式的数据结构,where there is a "wrapper" object for each entry (e.g. Map.Entry). 这个对象不仅有一个头,还有指向列表中下一个对象的指针(通常是8字节)。

4.基本类型(原始类型)的集合通常将它们存储为包装类型的对象,如java.lang.Integer。

本节将首先概述Spark的内存管理,然后讨论用户可以在应用程序中更有效地使用内存的具体策略。具体来说,我们将介绍如何确定对象的内存使用情况,以及如何改进它,或者通过更改数据结构,或者以序列化格式存储数据。接下来我们将介绍调整Spark的缓存大小和Java垃圾收集器。

内存管理概述

Spark中的内存使用大部分属于两类:执行和存储。执行内存是指在shuffles,joins,sorts和aggregations操作中使用的内存,而存储内存指的是用于跨集群缓存和传播内部数据的内存。在Spark中,执行和存储共享一个统一的区域(M)。当不使用执行内存时,存储就可以获取所有可用内存,反之亦然。 Execution may evict storage if necessary, but only until total storage memory usage falls under a certain threshold (R). In other words, describes a subregion within M where cached blocks are never evicted. Storage may not evict execution due to complexities in implementation.

这种设计确保了几个比较好的特性。首先,不使用缓存的应用程序可以使用整个空间执行,避免不必要的磁盘溢出。其次,使用缓存的应用程序可以保留一个最小的存储空间(R),使数据块不受驱逐。Lastly, this approach provides reasonable out-of-the-box performance for a variety of workloads without requiring user expertise of how memory is divided internally.

虽然有两个相关配置,但是不建议用户调整它们,因为默认值适用于大多数情况:

1.spark.memory.fraction

2.spark.memory.storageFraction

这两个配置的意思详见Spark Configuration。

确定内存消耗

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

要估计特定对象的内存消耗,可以使用SizeEstimator类的估计方法。

调整数据结构

减少内存消耗的第一种方式是避免增加开销的Java功能,例如基于指针的数据结构和包装对象。有以下方法:

1.设计数据结构时,优先选择数组和原始类型,而不是标准的Java或者Scala集合类(例如HashMap)。fastutil库为与Java标准库兼容的基本类型提供了方便的集合类。直接google fastutil即可。

2.尽可能避免使用大量小对象和指针的嵌套结构。

3.Consider using numeric IDs or enumeration objects instead of strings for keys.

4.如果RAM小于32GB,请设置JVM flag -XX:+UseCompressedOops 来使指针为4个字节而不是8个。可以在spark-en.sh中添加这些选项。

序列化的RDD存储

如果调整数据结构之后,对象仍然太大而无法进行高效存储,那么一个更简单的减少内存使用的方法就是通过使用StorageLevel中的序列化存储级别(比如StorageLevel.MEMORY_ONLY_SER)来以序列化格式存储它们。Spark会将每个RDD分区存储为一个大的字节数组。以序列化形式存储数据的唯一缺点是访问速度较慢,这是由于必须快速反序列化每个对象。强烈推荐使用Kryo,因为它比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中有关内存管理的一些基本信息:

1.Java堆空间分为新老两个区域。新区域保存存活时间短的对象,老区域保存存活时间长的对象。

2.新区域又进一步可以细分为三个区域:Eden,Survivor1,Survivor2

3.垃圾收集过程的简单描述:当Eden已满时,在Eden上运行一个小型GC,将Dden和Survivor1中存活的对象复制到Survivor2。Survivor区域交换。如果一个对象足够老或者Survivor2已满,则将其移到老区域中。最后,将老区域接近满时,调用完整的GC。

The goal of GC tuning in Spark is to ensure that only long-lived RDDs are stored in the Old generation and that the Young generation is sufficiently sized to store short-lived objects. This will help avoid full GCs to collect temporary objects created during task execution. Some steps which may be useful are:

  • Check if there are too many garbage collections by collecting GC stats. If a full GC is invoked multiple times for before a task completes, it means that there isn’t enough memory available for executing tasks.

  • If there are too many minor collections but not many major GCs, allocating more memory for Eden would help. You can set the size of the Eden to be an over-estimate of how much memory each task will need. If the size of Eden is determined to be E, then you can set the size of the Young generation using the option -Xmn=4/3*E. (The scaling up by 4/3 is to account for space used by survivor regions as well.)

  • In the GC stats that are printed, if the OldGen is close to being full, reduce the amount of memory used for caching by lowering spark.memory.fraction; it is better to cache fewer objects than to slow down task execution. Alternatively, consider decreasing the size of the Young generation. This means lowering -Xmn if you’ve set it as above. If not, try changing the value of the JVM’s NewRatio parameter. Many JVMs default this to 2, meaning that the Old generation occupies 2/3 of the heap. It should be large enough such that this fraction exceeds spark.memory.fraction.

  • Try the G1GC garbage collector with -XX:+UseG1GC. It can improve performance in some situations where garbage collection is a bottleneck. Note that with large executor heap sizes, it may be important to increase the G1 region size with -XX:G1HeapRegionSize

  • As an example, if your task is reading data from HDFS, the amount of memory used by the task can be estimated using the size of the data block read from HDFS. Note that the size of a decompressed block is often 2 or 3 times the size of the block. So if we wish to have 3 or 4 tasks’ worth of working space, and the HDFS block size is 128 MB, we can estimate size of Eden to be 4*3*128MB.

  • Monitor how the frequency and time taken by garbage collection changes with the new settings.

Our experience suggests that the effect of GC tuning depends on your application and the amount of memory available. There are many more tuning options described online, but at a high level, managing how frequently full GC takes place can help in reducing the overhead.

GC tuning flags for executors can be specified by setting spark.executor.extraJavaOptions in a job’s configuration.

其他考虑

并行度

Clusters will not be fully utilized unless you set the level of parallelism for each operation high enough. Spark automatically sets the number of “map” tasks to run on each file according to its size (though you can control it through optional parameters to SparkContext.textFile, etc), and for distributed “reduce” operations, such as groupByKey and reduceByKey, it uses the largest parent RDD’s number of partitions. You can pass the level of parallelism as a second argument (see the spark.PairRDDFunctions documentation), or set the config property spark.default.parallelism to change the default.一般来说,建议集群中每个CPU内核有2-3个任务。

Reduce任务的内存使用

Sometimes, you will get an OutOfMemoryError not because your RDDs don’t fit in memory, but because the working set of one of your tasks, such as one of the reduce tasks in groupByKey, was too large. Spark’s shuffle operations (sortByKeygroupByKeyreduceByKeyjoin, etc) build a hash table within each task to perform the grouping, which can often be large. 最简单的解决办法是提高并行度,使每个任务的输入集合更小。Spark能够有效支持短至200ms的任务,因为它可以在多个任务中复用一个executor JVM,并且任务启动成本较低,因此你可以安全地将并行级别提高到超过集群内核的数量。

广播变量

Using the broadcast functionality available in SparkContext can greatly reduce the size of each serialized task, and the cost of launching a job over a cluster. If your tasks use any large object from the driver program inside of them (e.g. a static lookup table), consider turning it into a broadcast variable. Spark prints the serialized size of each task on the master, so you can look at that to decide whether your tasks are too large; in general tasks larger than about 20 KB are probably worth optimizing.

数据本地性(Data Locality)

数据本地性可能会对Spark作业的性能产生重大影响。如果数据和代码在一起,那么计算就会很快。但是,如果代码和数据是分开的,就必须移动一个到另一个。通常情况下,由于代码大小比数据小得多,所以传输代码会比传输数据快。Spark builds its scheduling around this general principle of data locality.

Data locality is how close data is to the code processing it. There are several levels of locality based on the data’s current location. In order from closest to farthest:

  • PROCESS_LOCAL data is in the same JVM as the running code. This is the best locality possible
  • NODE_LOCAL data is on the same node. Examples might be in HDFS on the same node, or in another executor on the same node. This is a little slower than PROCESS_LOCAL because the data has to travel between processes
  • NO_PREF data is accessed equally quickly from anywhere and has no locality preference
  • RACK_LOCAL data is on the same rack of servers. Data is on a different server on the same rack so needs to be sent over the network, typically through a single switch
  • ANY data is elsewhere on the network and not in the same rack

Spark prefers to schedule all tasks at the best locality level, but this is not always possible. In situations where there is no unprocessed data on any idle executor, Spark switches to lower locality levels. There are two options: a) wait until a busy CPU frees up to start a task on data on the same server, or b) immediately start a new task in a farther away place that requires moving data there.

What Spark typically does is wait a bit in the hopes that a busy CPU frees up. Once that timeout expires, it starts moving the data from far away to the free CPU. The wait timeout for fallback between each level can be configured individually or all together in one parameter; see thespark.locality parameters on the configuration page for details. You should increase these settings if your tasks are long and see poor locality, but the default usually works well.

总结

这是一个简短的指南,指出调整Spark应用程序时应该了解的主要问题 - 最重要的是数据序列化和内存调整。 对于大多数程序来说,切换到Kryo序列化和以序列化形式保存数据将解决最常见的性能问题。

spark第十八篇:Tuning Spark 调优的更多相关文章

  1. Spark学习之路 (八)SparkCore的调优之开发调优

    摘抄自:https://tech.meituan.com/spark-tuning-basic.html 前言 在大数据计算领域,Spark已经成为了越来越流行.越来越受欢迎的计算平台之一.Spark ...

  2. Spark学习之路 (八)SparkCore的调优之开发调优[转]

    前言 在大数据计算领域,Spark已经成为了越来越流行.越来越受欢迎的计算平台之一.Spark的功能涵盖了大数据领域的离线批处理.SQL类处理.流式/实时计算.机器学习.图计算等各种不同类型的计算操作 ...

  3. Python之路【第十八篇】:Web框架们

    Python之路[第十八篇]:Web框架们   Python的WEB框架 Bottle Bottle是一个快速.简洁.轻量级的基于WSIG的微型Web框架,此框架只由一个 .py 文件,除了Pytho ...

  4. Egret入门学习日记 --- 第十八篇(书中 8.5~8.7 节 内容)

    第十八篇(书中 8.5~8.7 节 内容) 其实语法篇,我感觉没必要写录入到日记里. 我也犹豫了好久,到底要不要录入. 这样,我先读一遍语法篇的所有内容,我觉得值得留下的,我就录入日记里. 不然像昨天 ...

  5. 【Spark篇】---Spark中内存管理和Shuffle参数调优

    一.前述 Spark内存管理 Spark执行应用程序时,Spark集群会启动Driver和Executor两种JVM进程,Driver负责创建SparkContext上下文,提交任务,task的分发等 ...

  6. Spark学习之路 (十二)SparkCore的调优之资源调优

    摘抄自:https://tech.meituan.com/spark-tuning-basic.html 一.概述 在开发完Spark作业之后,就该为作业配置合适的资源了.Spark的资源参数,基本都 ...

  7. Spark学习之路 (十二)SparkCore的调优之资源调优[转]

    概述 在开发完Spark作业之后,就该为作业配置合适的资源了.Spark的资源参数,基本都可以在spark-submit命令中作为参数设置.很多Spark初学者,通常不知道该设置哪些必要的参数,以及如 ...

  8. 大数据笔记(二十八)——执行Spark任务、开发Spark WordCount程序

    一.执行Spark任务: 客户端 1.Spark Submit工具:提交Spark的任务(jar文件) (*)spark提供的用于提交Spark任务工具 (*)example:/root/traini ...

  9. Spark 官网提到的几点调优

    1. 数据序列化 默认使用的是Java自带的序列化机制.优点是可以处理所有实现了java.io.Serializable 的类.但是Java 序列化比较慢. 可以使用Kryo序列化机制,通常比Java ...

随机推荐

  1. NSString 对象保存在哪? @“xxx”和 stringWithFormat:@"xxx" 区别?

    NSString *str1=@"string";//这种是保存在常量池 NSString *str2=@"string"; NSLog(@"str1 ...

  2. Sharepoint2013搜索学习笔记之自定义结果精简分类(八)

    搜索结果页左边的结果精简分类是可以根据搜索结果自定义的,在搜索的部门日志结果集页面上我搜索测试关键字,左边分类导航在默认分类的基础上增加了一个日志类型的分类,如下图: 要实现这个效果,导航到之前定义的 ...

  3. C# 可变参数

    class Program { static void Main(string[] args) { //常规使用方法 Console.WriteLine(Add(, , , , , , , , })) ...

  4. C#验证身份证号码正确性

    18位号码: private static bool CheckIDCard18(string Id) { ; ), , ) || '), out n) == false) { return fals ...

  5. Kotlin 在kotlin内使用Java的一些注意(长篇)

    首先呢,大部分的java在kotlin内是可以使用的. 但是有些java的关键字和kotlin的一样,而意义不一样就需要转义.(单引号括起来的)这一点需要注意. 这是一个长篇 我会不断更新.毕竟我也在 ...

  6. CentOS 6.9下PXE+Kickstart无人值守安装操作系统

    一.简介 1.1 什么是PXE PXE(Pre-boot Execution Environment,预启动执行环境)是由Intel公司开发的最新技术,工作于Client/Server的网络模式,支持 ...

  7. django中博客后台将图片上传作为用户头像

    添加上传目录 # 如果不添加上传目录,仍然可以上传成功,默认为project目录,如果models.py定义了upload_to="目录名称",则会上传到"project ...

  8. linux centos 宝塔主机控制面板安装和安全狗安装过程记录

    linux 宝塔控制面板 安装过程yum install -y wget && wget -O install.sh http://103.224.251.79:5880/instal ...

  9. 加密模块(md5)

    一.md5加密 import hashlib s = ' print(s.encode()) m = hashlib.md5(s.encode())# 必须得传一个bytes类型的 print(m.h ...

  10. Django REST framework序列化

    一.简介 Django REST framework是基于Django实现的一个RESTful风格API框架,能够帮助我们快速开发RESTful风格的API. 官网:https://www.djang ...