spark计算的瓶颈可能有很多,比如cpu,带宽,内存等等. 本文主要从序列化以及内存优化两方面来谈.
数据序列化
- 序列化在任何分布式应用程序的性能中起着重要的作用
- 将对象序列化为消耗大量字节的格式会大大减慢计算速度
- spark提供了两个序列化库,java序列化和kryo序列化
- spark默认使用java序列化,但是java序列化比较慢,且序列化后的对象体积大
- kryo序列化比较快,但缺点是并不支持所有的类型.
- kryo使用自定义类需要注册,注册方法如下
val conf = new SparkConf().setMaster(...).setAppName(...)
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))
val sc = new SparkContext(conf)
内存调整
- 主要考虑三方面:
存储对象的内存用量,
访问这些对象的开销,
垃圾回收的开销(如果对象周转率很高的话)
默认情况下,java对象访问很快,但是会占用比原始数据2-5倍的空间.原因如下
**每个不同的 Java 对象都有一个"对象标头"**,该对象大约为 16 字节,包含指向其类的指针等信息。对于数据很少的对象(例如一个Int字段),这可能大于数据。
**Java 在原始字符串数据上具有大约 40 字节的开销**(因为它们将其存储在`StringCharString` 数组中,并保留额外的数据(如长度),并且由于 UTF-16 编码的内部使用,将每个字符存储为两个字节。 因此,10 个字符的字符串可以轻松地消耗 60 字节。
常见的集合类(如 和)使用链接的数据结构,其中每个条目都有一个"包装器"对象(例如 HashMapLinkedListMap.Entry)。此对象不仅具有标头,而且指向列表中的下一个对象的指针(通常每个字节 8 个字节)。
**基元类型的集合通常将它们存储为"装箱"对象**,如 。`java.lang.Integer`
为了高效使用内存,请看下面
内存管理概述
- spark内存分两部分,执行内存和存储内存
- 执行内存是之执行
shuffle,join,sort 和aggregation
等计算时的内存 - 存储内存是指用于缓存和跨集群传播数据的内存
- 这两块内存是共享的,就是可以互相占用
- 如有必要,执行内存可以驱逐共享内存
- 该设计可以提高内存的使用效率
有两个参数可以调整这个,但是普通的用户一般都不需要调整.
spark.memory.fraction表示 的大小为 (JVM 堆空间 - 300MiB) 的大小(默认 0.6)。其余空间 (40%)为用户数据结构、Spark 中的内部元数据以及在稀疏和异常大型记录的情况下防止 OOM 错误保留。
spark.memory.storageFraction表示 的大小为 分数(默认 0.5)。 是缓存块不受执行驱逐的存储空间。
确定内存消耗
调整数据集所需的内存消耗量的最佳方法是创建 RDD,将它放入缓存中,并查看 Web UI 中的"存储"页。该页将告诉您 RDD 占用的内存量。
若要估计特定对象的内存消耗,请使用 SizeEstimatorestimate’的方法。这可用于试验不同的数据布局以修剪内存使用情况,以及确定广播变量在每个执行器堆上将占用的空间量。
调整数据结构
减少内存消耗的第一个方法是避免添加开销的 Java 功能,例如基于指针的数据结构和包装器对象。有几种方法可以做到这一点:
将数据结构设计为首选对象数组和基元类型,而不是标准 Java 或 Scala 集合类(例如HashMap )。快速利用库为与 Java 标准库兼容的基元类型提供了方便的集合类。
尽可能避免使用大量小对象和指针的嵌套结构。
请考虑使用数字 ID 或枚举对象,而不是字符串作为key
如果 RAM 小于32 GIB,请将 JVM 标志设置为指针为 4 个字节而不是 8 个字节。您可以在 "spark-env.sh 中添加这些选项。-XX:+UseCompressedOops
序列化 RDD 存储
- 如果对象仍然太大,无法有效存储,就最好以序列化的形式存储
MEMORY_ONLY_SER
- 但是也有缺点,就是访问速度变慢,因为要反序列化
- 最好使用Kryo序列化,因为大小比java小的多
GC优化
- 垃圾回收的成本和对象的数量成正比
- 如果GC有问题,首选的解决方法是以序列化的形式存储对象
测量 GC 的影响
GC 调优的第一步是收集有关垃圾回收发生的频率和花费 GC 时间量的统计信息。这可以通过添加到Java选项来完成。
高级 GC 调优
为了进一步调整垃圾回收,我们首先需要了解一些有关 JVM 内存管理的基础知识:
Java 堆空间分为两个区域"年轻"和"旧"。年轻一代意在持有短期对象,而老一代则用于具有较长寿命的对象。
年轻一代被进一步分为三个区域[伊登,幸存者1,幸存者2]。
垃圾回收过程简体说明:当 Eden 已满时,在 Eden 上运行一个次要 GC,并且从 Eden 和幸存者 1 中活动的对象将复制到幸存者 2。幸存者区域被交换。如果对象已足够旧或幸存者 2 已满,则将移动到"旧"。最后,当 Old 接近满时,将调用一个完整的 GC。
Spark 中的 GC 调优的目标是确保老一代中只存储长寿命的 RDD,并且年轻一代有足够的大小来存储短寿命的对象。这将有助于避免使用完整的 GC 来收集任务执行期间创建的临时对象。一些可能有用的步骤包括:
通过收集 GC 统计信息检查垃圾回收是否太多。如果在任务完成之前多次调用完整 GC,则意味着没有足够的内存可用于执行任务。
如果有太多的次要集合,但主要的 GCs 不是很多,为 Eden 分配更多的内存会有所帮助。您可以将 Eden 的大小设置为高估每个任务所需的内存量。如果伊甸园的大小被确定为 ,那么你可以设置年轻一代的大小使用的选项。(增加 4/3 也考虑到幸存者区域使用的空间。E-Xmn=4/3*E
在打印的 GC 统计信息中,如果 OldGen 接近已满,则通过降低 ;最好缓存较少的对象,而不是减慢任务执行速度。或者,考虑减少年轻一代的规模。这意味着如果已设置为上述,将降低。如果没有,请尝试更改 JVM 参数的值。许多 JVM 默认为 2,这意味着老一代占用了堆的 2/3。它应该足够大,以使这个分数超过。spark.memory.fraction-XmnNewRatiospark.memory.fraction
使用 尝试 G1GC 垃圾回收器。在某些情况下,垃圾回收是瓶颈,它可以提高性能。请注意,对于大型执行器堆大小,增加G1 区域大小可能非常重要。-XX:+UseG1GC-XX:G1HeapRegionSize
例如,如果任务正在从 HDFS 读取数据,则可以使用从 HDFS 读取的数据块的大小来估计任务使用的内存量。请注意,解压缩块的大小通常是块大小的 2 或 3 倍。因此,如果我们希望有 3 或 4 个任务的工作空间,并且 HDFS 块大小为 128 MiB,我们可以估计伊甸园的大小为 。43128MiB
监视垃圾回收的频率和时间如何随着新设置而变化。
我们的经验表明,GC 调优的效果取决于您的应用程序和可用内存量。在线描述的调优选项更多,但在高级别上,管理完整 GC 发生的频率有助于减少开销。
可以通过设置或在作业配置中指定执行器的 GC 调优标志。spark.executor.defaultJavaOptionsspark.executor.extraJavaOptions
其他注意事项
并行度
- spark会依据文件大小自动设置并行度,当然也可以自定义
- 对于reduce操作,会继承父RDD的最大分区数
- 建议每个
core
可以执行2-3
个任务
读取文件时的并行度
如果您的工作使用 Hadoop 输入格式在 RDD 上工作(例如,通过 ),则并行性通过spark.hadoop.mapreduce.input.fileinputformat.list-status.num-threads
(当前默认值为 1)进行控制。
对于具有基于文件的数据源的 Spark SQL,您可以调整和改进列表并行性。有关详细信息,请参阅 Spark SQL 性能调优指南。spark.sql.sources.parallelPartitionDiscovery.thresholdspark.sql.sources.parallelPartitionDiscovery.parallelism
reduce任务的内存使用情况
- 如果某个task过大,可能会内存溢出
- spark的shuffle操作会创建一个
hash table
执行聚合,就可能会比较大(groupByKey sortByKey groupByKey reduceByKey join
) - 解决这个问题的办法是提高并行度,这样每个task处理的数据量就比较小了
- 因为
spark
是线程机制,启动一个任务消耗比较小,所以不必担心像mr那样启动比较慢
广播大变量
使用中的可用广播功能可以大大减少每个序列化任务的大小,以及通过群集启动作业的成本。如果任务使用其内部驱动程序程序中的任何大型对象(例如静态查找表),请考虑将其转换为广播变量。Spark 打印主机上每个任务的序列化大小,以便查看该大小以确定任务是否太大;在一般任务大于 20 KiB 可能值得优化。SparkContext
数据本地化
- 如果数据和操作它的代码在一块,就会比较快
- 如果数据和操作它的代码是分开的,一个必须移动到另一个,一般移动代码
数据本地化有几个级别:
PROCESS_LOCAL数据与正在运行的代码位于相同的 JVM 中。这是最好的地方
NODE_LOCAL数据位于同一节点上。示例可能在同一节点上的 HDFS 中,或在同一节点上的另一个执行器中。这比数据必须在进程之间传输要慢一些PROCESS_LOCAL
NO_PREF数据从任何地方访问同样快速,并且没有本地化选项
RACK_LOCAL数据位于同一服务器机架上。数据位于同一机架上的不同服务器上,因此需要通过网络(通常通过单个交换机)发送
ANY数据位于网络的其他位置,而不是在同一机架中
Spark 倾向于在最佳本地化级别安排所有任务,但这并不总是可能的。
当有cpu空闲时,spark会将数据从很远的地方移动到空闲的 CPU。
总结
这是一个简短的指南,指出在调整 Spark
应用程序时您应该知道的主要问题 - 最重要的是,数据序列化和内存调优。对于大多数程序,切换到 Kryo
序列化和以序列化形式保留数据将解决最常见的性能问题。
官网原文参考
https://spark.apache.org/docs/latest/tuning.html