Spark的一些优化点

因为spark的计算都是基于内存的,他的瓶颈有:cpu,带宽(network bandwidth),memory。通常情况下,如果数据是在内存里面的,瓶颈就在带宽上面,你也可以做一些其他优化,如RDD序列化(减少内存的使用)。


Data Serialization数据序列化

序列化在我们的分布式应用中扮演了一个非常重要的角色。
默认使用JAVA serialization,比较灵活但是比较慢而且会导致一些类的大型序列化格式。
在这里插入图片描述

Kryo,速度更快,更紧凑(小),比java快十个数量级,但并不是支持序列化类型,需要注册class。
注册: sparkConf.set(“spark.serializer”, “org.apache.spark.serializer.KryoSerializer”)。建议写在spark-default里面,也可使用./spark-submit --conf x=y

在这里插入图片描述


内存调整

内存调整有三个方面的考虑:
1、对象占用了多少内存(你可能想把真个数据集撞到内存里面去)
2、访问这些对象的成本
3、垃圾回收
默认情况下,Java对象访问速度很快,但与其字段中的“原始”数据相比,会占用2-5倍的空间。这是由于以下几个原因:

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

内存管理概述
Spark中的内存使用大致属于以下两种类别之一:执行和存储。执行内存是指用于在shuffle,join,sorts和aggregations中进行计算的内存,而存储内存是指用于在集群中缓存和传播内部数据的内存。在Spark中,执行和存储共享一个统一的region(M)。当没有使用执行内存时,存储可以获取所有可用内存,反之亦然。如有必要,执行可以驱逐存储内存,但仅限于总存储内存使用量低于某个阈值(R)。换句话说,R描述了M从不驱逐缓存块的子区域。由于实施的复杂性,存储可能不会剔除执行端的内存。

该设计确保了几种理想的特性。首先,不使用缓存的应用程序可以使用整个空间进行执行计算,从而避免不必要的磁盘读写。其次,使用缓存的应用程序可以保留最小存储空间(R),其中数据块不受驱逐。最后,这种方法为各种工作负载提供了合理的开箱即用性能,而无需用户内部划分内存的专业知识。

##在sparkenv.scala中有如下源码
 val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false)
    val memoryManager: MemoryManager =
      if (useLegacyMemoryManager) {## 1.5及以下版本使用的内存管理器。
        new StaticMemoryManager(conf, numUsableCores)
      } else { ##1.6版本开始的新的内存管理器
        UnifiedMemoryManager(conf, numUsableCores)
      }

旧内存管理器的执行原理。

##StaticMemoryManager的构造器源码
def this(conf: SparkConf, numCores: Int) {
    this(
      conf,
      StaticMemoryManager.getMaxExecutionMemory(conf),
      StaticMemoryManager.getMaxStorageMemory(conf),
      numCores)
### getMaxExecutionMemory(conf)方法的源码:
/**
 * Return the total amount of memory available for the execution region, in bytes.
 * 返回给我们执行端用的总的可用内存,单位是字节
 */
private def getMaxExecutionMemory(conf: SparkConf): Long = {
  val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)

  if (systemMaxMemory < MIN_MEMORY_BYTES) {## 判断系统内存是否小于最小要求的内存32*1024*1024
    throw new IllegalArgumentException(s"System memory $systemMaxMemory must " +
      s"be at least $MIN_MEMORY_BYTES. Please increase heap size using the --driver-memory " +
      s"option or spark.driver.memory in Spark configuration.")
  }
  if (conf.contains("spark.executor.memory")) {## spark的执行内存
    val executorMemory = conf.getSizeAsBytes("spark.executor.memory")
    if (executorMemory < MIN_MEMORY_BYTES) {
      throw new IllegalArgumentException(s"Executor memory $executorMemory must be at least " +
        s"$MIN_MEMORY_BYTES. Please increase executor memory using the " +
        s"--executor-memory option or spark.executor.memory in Spark configuration.")
    }
  }
  val memoryFraction = conf.getDouble("spark.shuffle.memoryFraction", 0.2)
  val safetyFraction = conf.getDouble("spark.shuffle.safetyFraction", 0.8)
  // 10G*0.2*0.8=1.6G
  (systemMaxMemory * memoryFraction * safetyFraction).toLong
}
/**
  * Return the total amount of memory available for the storage region, in bytes.
  */
 private def getMaxStorageMemory(conf: SparkConf): Long = {
   val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
   val memoryFraction = conf.getDouble("spark.storage.memoryFraction", 0.6)
   val safetyFraction = conf.getDouble("spark.storage.safetyFraction", 0.9)
   //10G*0.6*0.9=5.4G
   (systemMaxMemory * memoryFraction * safetyFraction).toLong
 } 

新的内存管理器底层原理

 def apply(conf: SparkConf, numCores: Int): UnifiedMemoryManager = {
    val maxMemory = getMaxMemory(conf) //(10G-300M)*0.6
    new UnifiedMemoryManager(
      conf,
      maxHeapMemory = maxMemory,
      //(10G-300M)*0.6*50% 给我们的存储内存,执行端就是剩下的
      onHeapStorageRegionSize =
        (maxMemory * conf.getDouble("spark.memory.storageFraction", 0.5)).toLong,
      numCores = numCores)
  }
 /**
   * Return the total amount of memory shared between execution and storage, in bytes.
   */
  private def getMaxMemory(conf: SparkConf): Long = {
    val systemMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)##系统总内存
    val reservedMemory = conf.getLong("spark.testing.reservedMemory",##预留内存
      if (conf.contains("spark.testing")) 0 else RESERVED_SYSTEM_MEMORY_BYTES)## 300*1024*1024
    val minSystemMemory = (reservedMemory * 1.5).ceil.toLong ##450M
    if (systemMemory < minSystemMemory) {
      throw new IllegalArgumentException(s"System memory $systemMemory must " +
        s"be at least $minSystemMemory. Please increase heap size using the --driver-memory " +
        s"option or spark.driver.memory in Spark configuration.")
    }
    // SPARK-12759 Check executor memory to fail fast if memory is insufficient
    if (conf.contains("spark.executor.memory")) {
      val executorMemory = conf.getSizeAsBytes("spark.executor.memory")
      if (executorMemory < minSystemMemory) {
        throw new IllegalArgumentException(s"Executor memory $executorMemory must be at least " +
          s"$minSystemMemory. Please increase executor memory using the " +
          s"--executor-memory option or spark.executor.memory in Spark configuration.")
      }
    }
    //10G-300M
    val usableMemory = systemMemory - reservedMemory
    //
    val memoryFraction = conf.getDouble("spark.memory.fraction", 0.6)
    //(10G-300M)*0.6
    (usableMemory * memoryFraction).toLong
  }

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

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中的“存储”页面。该页面将告诉您RDD占用多少内存。

为了估计特定对象的内存消耗,使用SizeEstimator的estimate方法(传入文件路径)。这对于尝试使用不同的数据布局来调整内存使用情况以及确定广播变量在每个执行程序堆上占用的空间量非常有用。
在这里插入图片描述

序列化RDD存储:
尽管进行了这种调整,但是当对象仍然太大而无法有效存储时,减少内存使用的一种更简单的方法是使用RDD持久性API中的序列化StorageLevels以序列化形式存储它们,例如MEMORY_ONLY_SER。然后,Spark将每个RDD分区存储为一个大字节数组。由于必须动态地反序列化每个对象,因此以序列化形式存储数据的唯一缺点是访问时间较慢。如果您希望以序列化形式缓存数据,我们强烈建议您使用Kryo,因为它导致的尺寸比Java序列化小得多(当然比原始Java对象更小)。

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

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

测量GC的影响

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

高级GC调整

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

Java堆空间分为Young和Old两个区域。Young代表意味着持有短命的物体,而老一代则用于生命周期较长的物体。

Young代进一步分为三个区域[Eden,Survivor1,Survivor2]。

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

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

通过收集GC统计数据来检查是否有太多垃圾收集。如果在任务完成之前多次调用完整的GC,则意味着没有足够的内存可用于执行任务。

如果有太多次要集合而不是很多主要GC,那么为Eden分配更多内存会有所帮助。您可以将Eden的大小设置为高估每个任务所需的内存量。如果确定Eden的大小E,则可以使用该选项设置Young代的大小-Xmn=4/3*E。(按比例增加4/3是为了考虑幸存者地区使用的空间。)

在打印的GC统计信息中,如果OldGen接近满,则通过降低来减少用于缓存的内存量spark.memory.fraction; 缓存更少的对象比减慢任务执行速度更好。或者,考虑减小Young代的尺寸。-Xmn如果您按上述设置,这意味着降低。如果没有,请尝试更改JVM NewRatio参数的值。许多JVM将此默认为2,这意味着旧一代占据堆的2/3。它应该足够大,使得该分数超过spark.memory.fraction。

尝试使用G1GC垃圾收集器-XX:+UseG1GC。在垃圾收集成为瓶颈的某些情况下,它可以提高性能。请注意,大执行人堆大小,可能重要的是增加了G1区域大小 与-XX:G1HeapRegionSize

例如,如果您的任务是从HDFS读取数据,则可以使用从HDFS读取的数据块的大小来估计任务使用的内存量。请注意,解压缩块的大小通常是块大小的2或3倍。因此,如果我们希望有3或4个任务的工作空间,并且HDFS块大小为128 MB,我们可以估计Eden的大小43128MB。

监视垃圾收集所用频率和时间如何随新设置而变化。

我们的经验表明GC调整的效果取决于您的应用程序和可用内存量。有更多的微调选项在线描述,但在较高的水平,管理完整的GC如何经常发生可以减少开销帮助。

可以通过设置spark.executor.extraJavaOptions作业的配置来指定执行程序的GC调整标志。


其他考虑因素

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

减少任务的内存使用情况
有时候,你会得到一个OutOfMemoryError,不是因为你的RDD不适合内存,而是因为你的一个任务的工作集,例如其中一个reduce任务groupByKey,太大了。spark的shuffle操作(sortByKey,groupByKey,reduceByKey,join,等)建立每个任务中的哈希表来进行分组,而这往往是大的。这里最简单的解决方法是 增加并行度,以便每个任务的输入集更小。Spark可以有效地支持短至200毫秒的任务,因为它在许多任务中重用了一个执行程序JVM,并且它具有较低的任务启动成本,因此您可以安全地将并行度提高到超过群集中的核心数。

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

数据本地性Data Locality

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

数据本地性是数据与处理它的代码的接近程度。根据数据的当前位置,有多个级别的本地性。从最近到最远的顺序:

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

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

http://spark.apache.org/docs/latest/tuning.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值