Spark 调优

(1) 从序列化的角度

Spark 提供了两种序列化库,一种是 java 的序列化,另一种是 Kryo 序列化。Java 的序列化框架相对来说性能较慢,如果在网络密集型的应用中不太合适。因此可以将序列化的方式调整为 Kryo 的序列化方式,Kryo 序列化比 java 序列化在速度上更快(一般在 10 倍左右)缺点就是不支持所有 Serializable 类,并且要求您提前在程序中注册您所需的类以获得最佳性能。序列化的作用不仅用在了各个节点之间的 shuffle 数据传输,而且还作用在了 RDD 序列化到磁盘。

(2) 从内存管理的角度

内存优化有三个方面的考虑 : 对象所占用的内存,访问对象的消耗以及垃圾回收所占用的开销。

Spark 的内存管理参数:

在 spark 中内存使用大部分属于两类:execution 和 storage。 execution 内存是指用于以 shuffles, joins, sorts andaggregations 计算的内存,而 storage 内存是指用于在集群中缓存和传播内部数据的内存。在 Spark 中,execution 和storage 共享一个统一的区域(M)。当没有使用 execution 内存时,storage 可以获取所有可用的内存,反之亦然。如果需要,execution 可以驱逐 storage,但是 storage 不得驱逐 execution 。

有两个参数可以调整:

spark.memory.fraction : 表示配置当前的内存管理器的最大内存使用比例,(默认 0.6)剩余 40% 部分被保留用于用户数据的结构,在 Spark 内部元数据,保障 OOM 错误,在异常大而稀疏的记录情况。

spark.memory.storageFraction : 表示配置用于配置 rdd 的 storage 与 cache 的默认分配的内存池大小(默认值 0.5)。

确定内存消耗

计算数据集所需的内存消耗量的最佳方式是创建 RDD,将其放入缓存中,并查看 Web UI 中的“Storage”页面。 该页面将告诉您 RDD 占用多少内存。要估计特定对象的内存消耗,请使用 SizeEstimator 的估计方法这对于尝试使用不同的数据布局来调整内存使用情况以及确定广播变量在每个执行程序堆中占用的空间量非常有用。

数据结构优化

减少内存消耗的首要方式是避免了 Java 特性开销,如基于指针的数据结构和二次封装对象。有几个方法可以做到这一点 :

1. 使用对象数组以及原始类型数组来设计数据结构,以替代 Java 或者 Scala 集合类(eg : HashMap)fastutil 库提供了原始数据类型非常方便的集合类,同时兼容 Java 标准类库。

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

3. 采用数字 ID 或者枚举类型以便替代 String 类型的主键。

4. 假如内存少于 32GB,设置 JVM 参数 -XX:+UseCompressedOops(开启 JVM 压缩指针) 以便将 8 字节指针修改成 4 字节。将这个配置添加到 spark-env.sh 中。

序列化 RDD 存储

当上面的优化都尝试过了对象同样很大。那么,还有一种减少内存的使用方法“以序列化形式存储数据”。序列化带来的唯一不足就是会降低访问速度,因为需要将对象反序列化(using Kryo)。如果需要采用序列化的方式缓存数据,我们强烈建议采用 Kryo,Kryo 序列化结果比 Java 标准序列化的更小(某种程度,甚至比对象内部的 raw 数据都还要小)。

垃圾回收优化

估算 GC 的影响

优化内存回收的第一步是获取一些统计信息,包括内存回收的频率、内存回收耗费的时间等。为了获取这些统计信息,我们可以把参数-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 添加到 java 选项中(配置向导里面有关于传 java 选项参数到 Spark job 的信息)。设置完成后,Spark 作业运行时,我们可以在日志中看到每一次内存回收的信息。注意,这些日志保存在集群的工作节点(在他们工作目录下的 stout 文件中)而不是你的驱动程序(driverprogram )。

确定调优步骤

Spark 内存回收优化的目标是确保只有长时间存活的 RDD 才保存到老生代区域;同时,新生代区域足够大以保存生命周期比较短的对象。这样,在任务执行期间可以避免执行 full GC 。下面是一些可能有用的执行步骤 :

l 通过收集 GC 信息检查内存回收是不是过于频繁。如果在任务结束之前执行了很多次 full GC ,则表明任务执行的内存空间不足。

l 如果有过多的 minor GC 而不是 full GC,那么为 Eden 分配更大的内存是有益的。你可以为 Eden 分配大于任务执行所需要的内存空间。如果 Eden 的大小确定为 E,那么可以通过 -Xmn=4/3*E 来设置新生代的大小(将内存扩大到 4/3 是考虑到 survivor 所需要的空间)。

l 在打印的内存回收信息中,如果老生代接近消耗殆尽,那么减少用于缓存的内存空间。可这可以通过属性 spark.storage.memoryFraction 来完成。减少缓存对象以提高执行速度是非常值得的。

l 尝试设置 -XX:+UseG1GC 来使用垃圾回收器 G1GC 。在垃圾回收是瓶颈的场景使用它有助改善性能。当 executor 的 heap 很大时,使用 -XX:G1HeapRegionSize 增大 G1 区大小很有必要。

l 举一个例子,如果任务从 HDFS 读取数据,那么任务需要的内存空间可以从读取的 block 数量估算出来。注意,解压后的 blcok 通常为解压前的 2-3 倍。所以,如果我们需要同时执行 3 或 4 个任务,block 的大小为 64M,我们可以估算出 Eden 的大小为 4*3*64MB。

l 监控内存回收的频率以及消耗的时间并修改相应的参数设置。总体而言有效控制内存回收的频率非常有助于降低额外开销。executor 的 GC 调优标志位可以在 job 的配置中设置 spark.executor.extraJavaOptions 来指定。

(3) 其他优化

并行度级别

Spark 自动根据文件大小设置运行在每个文件上的 map 任务的数量,而且对于分布式 reduce 操作,例如 groupByKey 和 reduceByKey ,它使用最大父 RDD 的分区数。你可以通过第二个参数传入并行度(阅读文档 spark.PairRDDFunctions)或者通过设置系统参数 spark.default.parallelism 来改变默认值。通常来讲,在集群中,我们建议为每一个 CPU 核( core )分配 2-3 个任务。

Reduce Tasks 的内存使用

有时,你会碰到 OutOfMemory 错误,这不是因为你的 RDD 不能加载到内存,而是因为 task 执行的数据集过大,例如正在执行 groupByKey 操作的 reduce 任务。Spark 的 shuffle 操作(sortByKey 、groupByKey 、reduceByKey 、join 等)为了实现 group 会为每一个任务创建哈希表,哈希表有可能非常大。最简单的修复方法是增加并行度,这样,每一个 task 的输入会变小。Spark 能够非常有效的支持短的 task(例如 200ms),因为他会复用一个 executor 的 JVM 来执行多个 task,这样能减小 task 启动的消耗,所以你可以放心的增加任务的并行度到大于集群的 CPU 核数。

广播大的变量

使用 SparkContext 的广播功能 可以有效减小每个序列化的 task 的大小以及在集群中启动 job 的消耗。如果 task 使用 driver program 中比较大的对象(例如静态查找表),考虑将其变成广播变量。Spark 会在 master 打印每一个 task 序列化后的大小,所以你可以通过它来检查 task 是不是过于庞大。通常来讲,大于 20KB 的 task 可能都是值得优化的。

数据本地化

数据本地化可能会对 Spark job 的性能产生重大影响。如果数据和在其上操作的代码在一起,则计算往往是快速的。但如果代码和数据分开,则必须移动到另一个。通常,代码大小远小于数据,因此将数据代码从一个地方寄送到另一个地方比一大块数据更快。 Spark 围绕数据局部性的一般原则构建其调度。

数据本地化是指数据和代码处理有多近。根据数据的当前位置有几个地方级别。从最近到最远的顺序:

l PROCESS_LOCAL 数据与运行代码在同一个 JVM 中。这是可能的最好的地方

l NODE_LOCAL 数据在同一个节点上。示例可能在同一节点上的 HDFS 或同一节点上的另一个执行程序中。这比

PROCESS_LOCAL 因为数据必须在进程之间移动慢一些

l NO_PREF 数据从任何地方同样快速访问,并且没有本地偏好

l RACK_LOCAL 数据位于同一机架上的服务器上。数据位于同一机架上的不同服务器上,因此需要通过网络发送,

通常通过单个交换机发送

l ANY 数据在网络上的其他地方,而不在同一个机架中

Spark 喜欢将所有 task 安排在最佳的本地级别,但这并不总是可能的。在任何空闲 executor 中没有未处理数据的情况下, Spark 将切换到较低的本地级别。有两个选项: a )等待一个繁忙的 CPU 释放在相同服务器上的数据上启动任务,或者 b )立即在更远的地方启动一个新的任务,需要在那里移动数据。Spark 通常做的是等待一个繁忙的 CPU 释放的希望。一旦超时,它将开始将数据从远处移动到可用的 CPU 。每个级别之间的回退等待超时可以在一个参数中单独配置或全部配置; 有关详细信息,请参阅配置页面 spark.locality 上的 参数。如果您的 task 很长,并且本地化很差,您应该增加这些设置,但默认值通常会很好。

你可能感兴趣的:(Spark 调优)