Spark调优与调试

1 使用SparkConf配置Spark

对Spark 进行性能调优,通常就是修改 Spark应用的运行时配置选项。Spark 中最主要的配置机制是通过 SparkConf 类对 Spark进行配置。当创建出一个 SparkContext 时,就需要创建出一个 SparkConf 的实例,如下是使用python创建一个应用:

conf = SparkConf()
conf.set("spark.app.name", "My Spark App")
sc = SparkContext(conf)

SparkConf 类其实很简单:SparkConf 实例包含用户要重载的配置选项的键值对。Spark 中的每个配置选项都是基于字符串形式的键值对。要使用创建出来的 SparkConf 对象,可以调用 set() 方法来添加配置项的设置,然后把这个对象传给SparkContext的构造方法。Spark允许通过 spark-submit工具动态设置配置项。 当应用被spark-submit 脚本启动时,脚本会把这些配置项设置到运行环境中。一旦传给 SparkContext的构造方法,应用所绑定的 SparkConf 就不可变了。这意味着所有的配置项都必须在SparkContext 实例化出来之前定下来。
同一个配置项可能在多个地方被设置了,例如,某用户可能在程序代码中直接调用 了 setAppName() 方法,同时也通过 spark-submit 的 --name 标记设置了这个值。针对这种情况,Spark 有特定的优先级顺序来选择实际配置。优先级最高的是在用户代码中显式调用set()方法设置的选项。其次是通过 spark-submit 传递的参数,再次是写在配置文件中 的值,最后是系统的默认值

2 Spark执行的组成部分:作业、任务和步骤

在执行时,Spark 会把多个操作合并为一组任务,把RDD的逻辑表示翻译为物理执行计划。特定的行动操作所生成的步骤的集合被称为一个作业。我们通过类似 count() 之类的方法触发行动操作,创建出由一个或多个步骤组成的作业。
一旦步骤图确定下来,任务就会被创建出来并发给内部的调度器。该调度器在不同的部署模式下会有所不同。物理计划中的步骤会依赖于其他步骤,如 RDD谱系图所显示的那样。 因此,这些步骤会以特定的顺序执行。例如,一个输出混洗后的数据的步骤一定会依赖于进行数据混洗的那个步骤。一个物理步骤会启动很多任务,每个任务都是在不同的数据分区上做同样的事情。任务内部的流程是一样的,如下所述。

  • 从数据存储(如果该 RDD 是一个输入 RDD)或已有 RDD(如果该步骤是基于已经缓 存的数据)或数据混洗的输出中获取输入数据
  • 执行必要的操作来计算出这些操作所代表的RDD。例如,对输入数据执行 filter() 和 map() 函数,或者进行分组或归约操作。
  • 把输出写到一个数据混洗文件中,写入外部存储,或者是发回驱动器程序(如果最终 RDD 调用的是类似 count() 这样的行动操作)。
    Spark 的大部分日志信息和工具都是以步骤、任务或数据混洗为单位的。归纳一下,Spark 执行时有下面所列的这些流程:
  • 用户代码定义RDD的有向无环图:RDD 上的操作会创建出新的 RDD,并引用它们的父节点,这样就创建出了一个图。
  • 动操作把有向无环图强制转译为执行计划: 当你调用 RDD 的一个行动操作时,这个 RDD 就必须被计算出来。这也要求计算出该RDD 的父节点。Spark 调度器提交一个作业来计算所有必要的 RDD。这个作业会包含 一个或多个步骤,每个步骤其实也就是一波并行执行的计算任务。一个步骤对应有向无 环图中的一个或多个 RDD,一个步骤对应多个 RDD 是因为发生了流水线执行。
  • 任务于集群中调度并执行: 步骤是按顺序处理的,任务则独立地启动来计算出 RDD 的一部分。一旦作业的最后一 个步骤结束,一个行动操作也就执行完毕了。

在一个给定的Spark应用中,由于需要创建一系列新的 RDD,因此上述阶段会连续发生很多次。

3 查找信息

Spark 在应用执行时记录详细的进度信息和性能指标。这些内容可以在两个地方找到:
Spark的网页用户界面以及驱动器进程和执行器进程生成的日志文件中

3.1 Spark网页用户界面

Spark 用户界面包括几个不同的页面,具体的格式在不同的 Spark 版本间也存在区别。基本页面有如下几种组成:

  • 作业页面: 步骤与任务的进度和指标,以及更多内容,经常用来评估一个作业的性能表现。在 Spark 这样的并行数据系统中,数据倾斜是导致性能问题的常见原因之一。当看到少量 的任务相对于其他任务需要花费大量时间的时候,一般就是发生了数据倾斜。步骤页面可 以帮助我们发现数据倾斜,我们只需要查看所有任务各项指标的分布情况就可以了。我们 可以从任务的运行时间开始看。是不是有一部分任务花的时间比别的任务多得多。除了发现数据倾斜,本页面还可以用来查看任务在其生命周期的各个阶段(读取、计算、 输出)分别花费了多少时间。
  • 存储页面:已缓存的RDD的信息。存储页面包含了缓存下来的 RDD 的信息。当有人在一个 RDD 上调用了 persist() 方法, 并且在某个作业中计算了该 RDD 时,这个 RDD 就会被缓存下来。有时,如果我们缓存了 许多 RDD,比较老的 RDD 就会从内存中移出来,把空间留给新缓存的 RDD。这个页面可 以告诉我们到底各个 RDD 的哪些部分被缓存了,以及在各种不同的存储媒介(磁盘、内 存等)中所缓存的数据量。浏览这个页面并理解一些重要的数据集是否被缓存在了内存 中,对我们是很有意义的。
  • 执行器页面:应用中的执行器进程列表。页面的用处之一在于确认应用可以使用你所预期使用的全部资源量。调试问题 时也最好先浏览这个页面,因为错误的配置可能会导致启动的执行器进程数量少于我们所 预期的。
  • 环境页面:用来调试Spark配置项:本页面枚举了你的 Spark 应用所运行的环境中实际生效的配置项集合。这里显示的配置项 代表应用实际的配置情况。

3.2 驱动器进程和执行器进程的日志

在某些情况下,用户需要深入研读驱动器进程和执行器进程所生成的日志来获取更多信 息。日志会更详细地记录各种异常事件,例如内部的警告以及用户代码输出的详细异常信息。Spark 日志文件的具体位置取决于以下部署模式:

  • 在 Spark 独立模式下,所有日志会在独立模式主节点的网页用户界面中直接显示
  • 在 Mesos 模式下,日志存储在 Mesos 从节点的 work/ 目录中,可以通过 Mesos 主节点用户界面访问。
  • 在 YARN 模式下,最简单的收集日志的方法是使用 YARN 的日志收集工具(运行 yarn logs -applicationId )来生成一个包含应用日志的报告。
    Spark 的日志系统是基于广泛使用的 Java日志库log4j 实现的,使用 log4j 的配置方式进行配置。log4j 配置的示例文件已经打包在 Spark 中,具体位置是 conf/log4j.properties.template。要想自定义 Spark 的日志,首先需要把这个 示例文件复制为 log4j.properties,然后就可以修改日志行为了。当设置了满意的日志等级或格式之后,你可以通过 spark-submit 的 --Files 标记添加 log4j.properties 文件。

4 关键性能考量

4.1 并行度

RDD 的逻辑表示其实是一个对象集合。我们在本书中已经多次提到,在物理执行期间, RDD 会被分为一系列的分区,每个分区都是整个数据的子集。当 Spark调度并运行任务时,Spark会为每个分区中的数据创建出一个任务。输入 RDD 一般会根据其底层的存储系统选择并行度。例如,从 HDFS 上读数据的输入 RDD 会为数据在 HDFS 上的每个文件区块创建一个分区。从数据 混洗后的 RDD 派生下来的 RDD 则会采用与其父 RDD 相同的并行度。
首先,当并行度过低时,Spark 集群会出现资源闲置的情况。而当并行度过高时,每个分区产生的间接开销累计起来就会更大。评判并行度是否过高的标准包括任务是否是几乎在瞬间 (毫秒级)完成的,或者是否观察到任务没有读写任何数据。
Spark提供了如下两种方法来对操作的并行度进行调优:

  • 数据混洗操作时,使用参数的方式为混洗后的 RDD 指定并行度
  • 对于任何已有的 RDD,可以进行重新分区来获取更多或者更少的分区数。重新分区操作通过 repartition() 实现。该操 作会把 RDD 随机打乱并分成设定的分区数目。

假设我们从 S3 上读取了大量数据,然后马上进行 filter() 操作筛选掉数据集 中的绝大部分数据。默认情况下,filter() 返回的 RDD 的分区数和其父节点一样,这样 可能会产生很多空的分区或者只有很少数据的分区。在这样的情况下,可以通过合并得到 分区更少的 RDD 来提高应用性能。如下是合并分区过多的RDD:

input = sc.textFile("./*log")
input.getNumPartitions()
# 排除掉大部分数据
lines = input.filter(lambda line: line.startswith("2014-10-17"))
lines.getNumPartions()  #分区的数量和父节点的分区数量一致
#合并
lines = lines.coalesce(5).chache()
lines.getNumPartitions()
#合并后的RDD上进行后续分析

4.2 序列化格式

当Spark 需要通过网络传输数据,或是将数据溢写到磁盘上时,Spark 需要把数据序列化为二进制格式。序列化会在数据进行混洗操作时发生,此时有可能需要通过网络传输大量数据。默认情况下,Spark会使用Java 内建的序列化库。Spark 也支持使用第三方序列化库 Kryo(https://github.com/EsotericSoftware/kryo),可以提供比 Java 的序列化工具更短的 序列化时间和更高压缩比的二进制表示,但不能直接序列化全部类型的对象。

4.3 内存管理

内存对 Spark 来说有几种不同的用途,理解并调优 Spark 的内存使用方法可以帮助优化Spark的应用。在各个执行器进程中,内存有以下所列几种用途:

  • RDD存储:当调用 RDD 的 persist() 或 cache() 方法时,这个 RDD 的分区会被存储到缓存区中。 Spark 会根据 spark.storage.memoryFraction 限制用来缓存的内存占整个 JVM 堆空间的 比例大小。如果超出限制,旧的分区数据会被移出内存。
  • 数据混洗与聚合的缓存区:当进行数据混洗操作时,Spark 会创建出一些中间缓存区来存储数据混洗的输出数据。这 些缓存区用来存储聚合操作的中间结果,以及数据混洗操作中直接输出的部分缓存数据。
  • 用户代码:Spark 可以执行任意的用户代码,所以用户的函数可以自行申请大量内存。例如,如果 一个用户应用分配了巨大的数组或者其他对象,那这些都会占用总的内存。
    在默认情况下,Spark 会使用 60%的空间来存储 RDD,20% 存储数据混洗操作产生的数据,剩下的 20% 留给用户程序。用户可以自行调节这些选项来追求更好的性能表现。如果 用户代码中分配了大量的对象,那么降低 RDD 存储和数据混洗存储所占用的空间可以有 效避免程序内存不足的情况。

4.4 硬件供给

提供给 Spark 的硬件资源会显著影响应用的完成时间。影响集群规模的主要参数包括分配给每个执行器节点的内存大小、每个执行器节点占用的核心数、执行器节点总数,以及用 来存储临时数据的本地磁盘数量
在各种部署模式下,执行器节点的内存都可以通过 spark.executor.memory 配置项或者 spark-submit 的 --executor-memory 标记来设置。而执行器节点的数目以及每个执行器 进程的核心数的配置选项则取决于各种部署模式。在 YARN 模式下,你可以通过 spark. executor.cores 或 --executor-cores 标记来设置执行器节点的核心数,通过 --num- executors 设置执行器节点的总数。在调整集群规模时,需要额外考虑的 方面还包括是否在计算中把中间结果数据集缓存起来。如果确实要使用缓存,那么内存中 缓存的数据越多,应用的表现就会越好。
除了内存和 CPU核心,Spark 还要用到本地磁盘来存储数据混洗操作的中间数据,以及溢写到磁盘中的 RDD 分区数据。切记,“越多越好”的原则在设置执行器节点内存时并不一定适用。使用巨大的堆空间可能会导致垃圾回收的长时间暂停,从而严重影响 Spark 作业的吞吐量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值