spark RDD 概述用法官方权威资料(建议收藏)

spark RDD 概述用法权威资料(建议收藏)

文章目录

  • spark RDD 概述用法权威资料(建议收藏)
  • 概述
  • 与Spark的链接
  • 初始化Spark
      • 使用Shell
  • 弹性分布式数据集(RDD)
    • 并行化集合
    • 外部数据集
    • RDD操作
      • 基础知识
      • 将函数传递给Spark
      • 理解闭包
        • 示例
        • 本地模式与集群模式
        • 打印RDD的元素
      • 处理键值对
      • 转换操作
      • 行动操作
      • Shuffle操作
        • 背景
        • 性能影响
    • RDD持久化
      • 选择哪个存储级别?
      • 删除数据
  • 共享变量
    • 广播变量
    • 累加器
  • 部署到集群
  • 从Java/Scala启动Spark作业
  • 单元测试
  • 推荐资料

概述

在高层次上,每个Spark应用程序由一个驱动程序组成,该驱动程序运行用户的主函数并在集群上执行各种并行操作。Spark提供的主要抽象是弹性分布式数据集(RDD),它是一个分区在集群节点上的元素集合,可以并行操作。RDD可以通过从Hadoop文件系统(或任何其他支持Hadoop文件系统)中的文件开始创建,或者通过在驱动程序中引用现有的Scala集合并对其进行转换来创建。用户还可以要求Spark将RDD保留在内存中,以便在并行操作之间高效地重用。最后,RDD可以自动从节点故障中恢复。

Spark中的第二个抽象是共享变量,可用于并行操作。默认情况下,当Spark将函数作为一组任务并行运行时,它会将函数中使用的每个变量的副本发送到每个任务。有时需要在任务之间共享变量,或在任务和驱动程序之间共享变量。Spark支持两种类型的共享变量:广播变量,可用于在所有节点上缓存一个值;累加器,用于“累加”变量,例如计数器和总和。

本指南展示了Spark在支持的每种语言中的这些功能。如果您使用Spark的交互式shell,可以更容易地跟随本指南,其中包括Scala shell的bin/spark-shell和Python shell的bin/pyspark。

与Spark的链接

Spark 3.5.0默认使用Scala 2.12构建和分发(尽管Spark也可以与其他版本的Scala一起使用)。如果您要用Scala编写应用程序,请确保使用兼容的Scala版本(例如2.12.X)。

要编写Spark应用程序,您需要在项目中添加对Spark的Maven依赖。Spark可以通过Maven Central获取:

<dependency>
    <groupId>org.apache.sparkgroupId>
    <artifactId>spark-core_2.12artifactId>
    <version>3.5.0version>
dependency>

如果您希望访问HDFS集群,则还需要根据您使用的HDFS版本添加对hadoop-client的依赖:

<dependency>
    <groupId>org.apache.hadoopgroupId>
    <artifactId>hadoop-clientartifactId>
    <version><your-hdfs-version>version>
dependency>

在Spark程序中,您需要导入必要的类:

import org.apache.spark.SparkContext
import org.apache.spark.SparkConf

(注意:在Spark 1.3.0之前,您需要显式导入org.apache.spark.SparkContext._以启用必要的隐式转换。)

按照这些步骤,您可以使用适当的依赖项和导入开始用Scala编写Spark应用程序。

初始化Spark

创建SparkContext对象是一个Spark程序必须做的第一件事情,它告诉Spark如何访问集群。要创建SparkContext,您首先需要构建一个包含有关应用程序信息的SparkConf对象。

每个JVM只能有一个活动的SparkContext。在创建新的SparkContext之前,必须停止活动的SparkContext。

val conf = new SparkConf().setAppName(appName).setMaster(master)
val sc = new SparkContext(conf)

appName参数是您的应用程序在集群UI上显示的名称。master是一个Spark、Mesos或YARN集群的URL,或者一个特殊的“local”字符串,用于在本地模式下运行。实际上,在集群上运行时,您不希望在程序中硬编码master,而是使用spark-submit启动应用程序并在其中接收它。但是,对于本地测试和单元测试,可以传递“local”以在本地进程中运行Spark。

使用Shell

在Spark shell中,已经为您创建了一个特殊的与解释器相关的SparkContext,存储在名为sc的变量中。因此,您不能自己创建SparkContext对象。您可以使用–master参数设置上下文连接到的主节点,并可以使用–jars参数将JAR文件添加到类路径中。还可以通过将逗号分隔的Maven坐标列表传递给–packages参数来向shell会话添加依赖项(例如Spark Packages)。任何其他存储依赖项可能存在的存储库(例如Sonatype)可以通过–repositories参数传递。例如,要在4个内核上运行bin/spark-shell,请使用以下命令:

$ ./bin/spark-shell --master local[4]

或者,如果还要将code.jar添加到类路径中,请使用以下命令:

$ ./bin/spark-shell --master local[4] --jars code.jar

要使用Maven坐标包含一个依赖项:

$ ./bin/spark-shell --master local[4] --packages "org.example:example:0.1"

有关完整选项列表,请运行spark-shell --help命令。在幕后,spark-shell调用更通用的spark-submit脚本。

弹性分布式数据集(RDD)

Spark围绕着弹性分布式数据集(RDD)的概念展开,它是一个容错的元素集合,可以并行操作。创建RDD有两种方式:在驱动程序中并行化现有集合,或者引用外部存储系统中的数据集,例如共享文件系统、HDFS、HBase或任何提供Hadoop InputFormat的数据源。

并行化集合

并行化集合是通过在驱动程序中对现有集合(Scala Seq)调用SparkContext的parallelize方法来创建的。集合的元素被复制以形成一个分布式数据集,可以并行操作。例如,下面是如何创建一个包含数字1到5的并行化集合:

val data = Array(1, 2, 3, 4, 5)
val distData = sc.parallelize(data)

一旦创建完成,分布式数据集(distData)就可以被并行操作。例如,我们可以调用distData.reduce((a, b) => a + b)来对数组的元素求和。后面我们会详细介绍对分布式数据集的操作。

并行集合的一个重要参数是将数据集划分为多少个分区。Spark将在集群上为每个分区运行一个任务。通常情况下,您希望为集群中的每个CPU设置2-4个分区。通常,Spark会根据集群自动设置分区的数量。但是,您也可以通过将其作为parallelize的第二个参数手动设置(例如sc.parallelize(data, 10))。注意:代码中有些地方使用了术语slices(与partitions同义词)以保持向后兼容性。

外部数据集

Spark可以从任何由Hadoop支持的存储源(包括本地文件系统、HDFS、Cassandra、HBase、Amazon S3等)创建分布式数据集。Spark支持文本文件、SequenceFiles以及任何其他Hadoop InputFormat。

可以使用SparkContext的textFile方法创建文本文件RDD。该方法接受文件的URI(可以是机器上的本地路径,也可以是hdfs://、s3a://等URI),并将其读取为一组行。下面是一个示例调用:

val distFile = sc.textFile("data.txt")

创建完成后,distFile可以通过数据集操作进行处理。例如,我们可以使用map和reduce操作来计算所有行的长度之和:distFile.map(s => s.length).reduce((a, b) => a + b)。

关于使用Spark读取文件的一些说明:

如果使用本地文件系统的路径,文件也必须在工作节点上的相同路径处可访问。要么将文件复制到所有工作节点上,要么使用网络挂载的共享文件系统。

Spark的基于文件的输入方法(包括textFile)都支持运行在目录、压缩文件和通配符上。例如,可以使用textFile(“/my/directory”)、textFile(“/my/directory/.txt")和textFile("/my/directory/.gz”)。当读取多个文件时,分区的顺序取决于文件从文件系统返回的顺序。例如,它可能遵循文件路径的词典排序,也可能不遵循。在一个分区内,元素的顺序按照底层文件中的顺序排列。

textFile方法还可以接受一个可选的第二个参数来控制文件的分区数。默认情况下,Spark为每个文件块创建一个分区(默认情况下HDFS块大小为128MB),但您也可以通过传递更大的值来请求更多的分区。请注意,分区数不能少于文件块数。

除了文本文件,Spark的Scala API还支持几种其他数据格式:

  • SparkContext.wholeTextFiles允许您读取包含多个小文本文件的目录,并将每个文件作为(文件名,内容)对返回。这与textFile不同,后者会返回每个文件中的每行记录。分区是由数据本地性确定的,这在某些情况下可能导致分区过少。对于这些情况,wholeTextFiles提供了一个可选的第二个参数来控制最小分区数。

  • 对于SequenceFiles,可以使用SparkContext的sequenceFile[K, V]方法,其中K和V是文件中键和值的类型。它们应该是Hadoop的Writable接口的子类,例如IntWritable和Text。此外,Spark还允许您为一些常见的Writable指定原生类型;例如,sequenceFile[Int, String]会自动读取IntWritables和Texts。

  • 对于其他Hadoop InputFormats,可以使用SparkContext.hadoopRDD方法,该方法接受一个任意的JobConf和输入格式类、键类和值类。设置方式与使用Hadoop作业时设置输入源的方式相同。还可以使用SparkContext.newAPIHadoopRDD来处理基于“新”MapReduce API(org.apache.hadoop.mapreduce)的InputFormats。

  • RDD.saveAsObjectFile和SparkContext.objectFile支持将RDD以一种简单的格式保存,其中包含序列化的Java对象。尽管这不如Avro等专门的格式高效,但它提供了一种保存任何RDD的简单方法。

RDD操作

RDD支持两种类型的操作:转换(transformations)和动作(actions)。转换操作从现有数据集创建一个新的数据集,而动作操作在数据集上运行计算,并将结果返回给驱动程序。例如,map是一种转换操作,它将每个数据集元素通过一个函数进行处理,并返回表示结果的新RDD。另一方面,reduce是一种动作操作,它使用某个函数对RDD的所有元素进行聚合,并将最终结果返回给驱动程序(尽管也有一个并行的reduceByKey操作,它返回一个分布式数据集)。

Spark中的所有转换操作都是惰性的,即它们不会立即计算结果。相反,它们只会记住应用于某个基本数据集(例如文件)的转换操作。这些转换操作只有在动作操作需要返回结果给驱动程序时才会被计算。这种设计使得Spark能够更高效地运行。例如,我们可以意识到通过map创建的数据集将在reduce中使用,并且只返回reduce的结果给驱动程序,而不是较大的映射数据集。

默认情况下,每个经过转换的RDD在每次运行动作操作时可能会重新计算。但是,您还可以使用persist(或cache)方法将RDD持久化到内存中,这样Spark会将元素保留在集群上,以便下次查询时更快地访问。还支持将RDD持久化到磁盘上,或者在多个节点之间进行复制。

基础知识

为了说明RDD的基础知识,考虑下面的简单程序:

val lines = sc.textFile("data.txt")
val lineLengths = lines.map(s => s.length)
val totalLength = lineLengths.reduce((a, b) => a + b)

第一行从外部文件定义了一个基本RDD。这个数据集并没有被加载到内存中或以其他方式进行操作:lines只是指向该文件的指针。第二行将lineLengths定义为map转换的结果。同样,由于延迟计算的原因,lineLengths不会立即计算。最后,我们运行reduce,这是一个动作。此时,Spark将计算分解为在不同机器上运行的任务,并且每台机器都运行其自己的map和本地归约,并仅将其答案返回给驱动程序。

如果我们还想稍后再次使用lineLengths,可以在reduce之前添加:

lineLengths.persist()

这将导致lineLengths在第一次计算后保存在内存中。

将函数传递给Spark

Spark的API在很大程度上依赖于将函数从驱动程序传递到集群上运行。有两种推荐的方法可以做到这一点:

  1. 匿名函数语法,可用于短小的代码片段。
  2. 全局单例对象中的静态方法。例如,您可以定义一个名为MyFunctions的对象,然后传递MyFunctions.func1,如下所示:
object MyFunctions {
  def func1(s: String): String = { ... }
}

myRdd.map(MyFunctions.func1)

请注意,虽然也可以传递类实例中方法的引用(而不是单例对象),但这需要将包含该类的对象与方法一起发送。例如,请考虑以下示例:

class MyClass {
  def func1(s: String): String = { ... }
  def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(func1) }
}

在这里,如果我们创建一个新的MyClass实例并调用其中的doStuff方法,其中的map操作引用了MyClass实例的func1方法,因此整个对象都需要发送到集群上。它类似于写rdd.map(x => this.func1(x))。

类似地,访问外部对象的字段将引用整个对象:

class MyClass {
  val field = "Hello"
  def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(x => field + x) }
}

等价于写rdd.map(x => this.field + x),它引用了整个对象。为了避免这个问题,最简单的方法是将字段复制到局部变量中而不是外部访问:

def doStuff(rdd: RDD[String]): RDD[String] = {
  val field_ = this.field
  rdd.map(x => field_ + x)
}

理解闭包

Spark中较难的一个问题是理解在集群上执行代码时变量和方法的作用域和生命周期。修改作用域外变量的RDD操作常常会引起困惑。下面的示例将使用foreach()来增加一个计数器,但类似的问题也可能发生在其他操作中。

counter = 0
rdd = sc.parallelize(data)

# 错误:不要这样做!
rdd.foreach(lambda x: counter += x)

print("Counter value: " + str(counter))

在这个例子中,我们试图通过在闭包中访问并更新计数器变量来累加RDD中的元素。然而,由于闭包被序列化并发送到集群上的执行器上执行,每个执行器只能看到计数器变量的副本,并且无法更新原始的驱动程序节点上的计数器变量。因此,打印的计数器值仍然是初始值0,无论RDD中有多少元素。

为了正确地处理这种情况,我们应该使用累加器(Accumulator)来在集群上安全地更新变量。累加器是一种分布式变量,可以在工作节点之间进行安全的增加操作。在上述示例中,我们可以使用LongAccumulator来代替计数器变量,并在累加器中进行累加操作。以下是使用累加器修复示例的代码:

accumulator = sc.longAccumulator()
rdd.foreach(lambda x: accumulator.add(x))

print("Counter value: " + str(accumulator.value))

使用累加器,我们可以在集群中正确地累加RDD中的元素,并获得最终的计数器值。请注意,累加器的value属性可以用于获取最终累加的结果。

示例

考虑下面的简单RDD元素求和,它在执行时可能会根据是否发生在同一个JVM中而有不同的行为。一个常见的例子是在本地模式下运行Spark(–master=local[n])与将Spark应用程序部署到集群中(例如通过spark-submit到YARN)。

counter = 0
rdd = sc.parallelize(data)

# 错误:不要这样做!
rdd.foreach(lambda x: counter += x)

print("Counter value: " + str(counter))

上述代码试图在foreach函数中更新一个全局变量counter。然而,在分布式模式下,执行器只能看到序列化闭包中的counter变量的副本,并且对它的任何更新都不会反映在驱动程序节点上的原始counter变量上。这意味着打印的最终counter值仍然为零。

为了确保定义良好的行为,建议使用累加器(Accumulator)来安全地在集群的工作节点之间更新变量。

本地模式与集群模式

上述代码的行为是不确定的,并且可能无法按预期工作。要执行作业,Spark将RDD操作的处理分解为任务,每个任务由执行器执行。在执行之前,Spark会计算任务的闭包。闭包是执行器执行其对RDD的计算所必须可见的变量和方法(在这种情况下是foreach())的集合。闭包被序列化并发送到每个执行器。

发送到每个执行器的闭包中的变量现在是副本,因此当在foreach函数中引用counter时,它不再是驱动程序节点上的counter。驱动程序节点的内存中仍然有一个counter,但执行器看不到它!执行器只能看到序列化闭包中的副本。因此,counter的最终值仍将为零,因为所有对counter的操作都是引用序列化闭包中的值。

在本地模式下,在某些情况下,foreach函数实际上将在与驱动程序相同的JVM中执行,并引用相同的原始counter,并且可能实际更新它。

为了确保在这类场景中有定义的行为,应该使用累加器。在Spark中,累加器专门用于提供一种安全更新变量的机制,当执行在集群中的工作节点上拆分时。本指南的累加器部分详细讨论了这些内容。

通常情况下,不应使用闭包(例如循环或本地定义的方法)来改变一些全局状态。Spark不定义也不保证对闭包之外引用的对象进行修改的行为。某些可以在本地模式下工作的代码只是偶然发生的,并且这样的代码在分布式模式下不会按预期工作。如果需要进行一些全局聚合,则应使用累加器。

打印RDD的元素

另一个常见的习惯用法是尝试使用rdd.foreach(println)或rdd.map(println)打印出RDD的元素。在单机上,这将生成预期的输出并打印出所有RDD的元素。然而,在集群模式下,执行器调用的stdout输出现在写入执行器的stdout,而不是驱动程序上的stdout,因此驱动程序上的stdout不会显示这些输出!要在驱动程序上打印所有元素,可以使用collect()方法先将RDD带到驱动程序节点上:rdd.collect().foreach(println)。然而,这可能会导致驱动程序内存不足,因为collect()将整个RDD获取到单个机器上;如果只需要打印RDD的几个元素,则更安全的方法是使用take():rdd.take(100).foreach(println)。

处理键值对

虽然大多数Spark操作可以处理包含任意类型对象的RDD,但有一些特殊操作仅适用于包含键值对的RDD。最常见的是分布式“shuffle”操作,例如按键分组或聚合元素。

在Scala中,这些操作自动适用于包含Tuple2对象的RDD(语言内置的元组,通过简单地写(a, b)创建)。键值对操作可在PairRDDFunctions类中使用,该类会自动包装一个包含元组的RDD。

例如,以下代码使用reduceByKey操作对键值对进行计数,以统计文件中每行文本出现的次数:

val lines = sc.textFile("data.txt")
val pairs = lines.map(s => (s, 1))
val counts = pairs.reduceByKey((a, b) => a + b)

我们也可以使用counts.sortByKey()按字母顺序对键值对进行排序,并使用counts.collect()将它们作为对象数组带回到驱动程序。

注意:当在键值对操作中使用自定义对象作为键时,必须确保伴随有匹配的equals()方法的hashCode()方法。有关详细信息,请参阅Object.hashCode()文档中概述的契约。

转换操作

转换操作 含义
map(func) 通过将源数据集中的每个元素传递给函数func,返回一个新的分布式数据集。
filter(func) 通过选择源数据集中函数func返回true的元素,返回一个新的数据集。
flatMap(func) 类似于map,但是每个输入项可以映射到0个或多个输出项(因此func应返回一个Seq而不是单个项)。
mapPartitions(func) 类似于map,但是在RDD的每个分区(块)上单独运行(因此func必须是类型为Iterator => Iterator的函数,其中T是RDD的类型)。
mapPartitionsWithIndex(func) 类似于mapPartitions,但是还向func提供表示分区索引的整数值(因此func必须是类型为(Int, Iterator) => Iterator的函数,其中T是RDD的类型)。
sample(withReplacement, fraction, seed) 使用给定的随机数生成器种子,以有放回或无放回的方式对数据进行抽样,并返回抽样的一部分数据。
union(otherDataset) 返回一个包含源数据集和参数数据集中元素的并集的新数据集。
intersection(otherDataset) 返回一个包含源数据集和参数数据集中元素的交集的新RDD。
distinct([numPartitions])) 返回一个包含源数据集中不重复元素的新数据集。
groupByKey([numPartitions]) 对(K, V)键值对类型的数据集进行分组,返回一个包含(K, Iterable)键值对类型的数据集。
reduceByKey(func, [numPartitions]) 对(K, V)键值对类型的数据集进行分组,并使用给定的reduce函数func对每个键的值进行聚合,返回一个包含(K, V)键值对类型的数据集。
aggregateByKey(zeroValue)(seqOp, combOp, [numPartitions]) 对(K, V)键值对类型的数据集进行分组,并使用给定的组合函数和初始值zeroValue对每个键的值进行聚合,返回一个包含(K, U)键值对类型的数据集。
sortByKey([ascending], [numPartitions]) 对具有Ordered实现的(K, V)键值对类型的数据集按键进行升序或降序排序,并返回一个包含(K, V)键值对类型的新数据集,根据布尔值ascending指定排序顺序。
join(otherDataset, [numPartitions]) 对类型为(K, V)和(K, W)的数据集进行连接,返回一个包含所有匹配元素的(K, (V, W))键值对类型的数据集。
cogroup(otherDataset, [numPartitions]) 对类型为(K, V)和(K, W)的数据集进行连接,返回一个包含(K, (Iterable, Iterable))元组类型的数据集。
cartesian(otherDataset) 对类型为T和U的数据集进行笛卡尔积操作,返回一个包含(T, U)键值对类型的数据集(即所有元素的配对)。
pipe(command, [envVars]) 将RDD的每个分区通过shell命令(如Perl或bash脚本)进行处理,并将输出作为字符串RDD返回。
coalesce(numPartitions) 将RDD的分区数减少到numPartitions。
repartition(numPartitions) 重新随机重分区RDD的数据,创建更多或更少的分区,并在它们之间平衡数据。
repartitionAndSortWithinPartitions(partitioner) 根据给定的分区器重新分区RDD,并在每个结果分区内按键对记录进行排序。

行动操作

操作 含义
reduce(func) 使用函数func(接受两个参数并返回一个参数)对数据集的元素进行聚合。该函数应满足可交换性和结合性,以便可以正确地并行计算。
collect() 将数据集的所有元素作为数组返回给驱动程序。这通常在过滤或其他返回数据子集的操作之后非常有用。
count() 返回数据集中的元素数量。
first() 返回数据集的第一个元素(类似于take(1))。
take(n) 返回包含数据集的前n个元素的数组。
takeSample(withReplacement, num, [seed]) 返回包含数据集中随机抽样的num个元素的数组,可以选择是否替换,还可以预先指定随机数生成器种子。
takeOrdered(n, [ordering]) 使用自然顺序或自定义比较器返回RDD的前n个元素。
saveAsTextFile(path) 将数据集的元素以文本文件(或一组文本文件)的形式写入到本地文件系统、HDFS或任何其他Hadoop支持的文件系统中。Spark会调用每个元素的toString方法将其转换为文件中的一行文本。
saveAsSequenceFile(path) (Java和Scala)将数据集的元素以Hadoop SequenceFile的形式写入到本地文件系统、HDFS或任何其他Hadoop支持的文件系统中。这仅适用于实现了Hadoop的Writable接口的键值对RDD。在Scala中,也可以将隐式可转换为Writable的类型(Spark包含了基本类型如Int、Double、String等的转换)。
saveAsObjectFile(path) (Java和Scala)以简单的格式使用Java序列化写入数据集的元素,然后可以使用SparkContext.objectFile()加载。
countByKey() 仅适用于类型为(K, V)的RDD。返回一个包含每个键的计数的哈希映射(K, Int)对。
foreach(func) 对数据集的每个元素运行函数func。通常用于更新累加器或与外部存储系统进行交互等副作用操作。
注:在foreach()之外修改除了累加器之外的变量可能会导致未定义的行为。有关详细信息,请参阅理解闭包。

Spark RDD API还公开了一些操作的异步版本,例如foreachAsync替代了foreach,它会立即返回一个FutureAction给调用者,而不会阻塞在操作完成上。这可以用于管理或等待操作的异步执行。

Shuffle操作

某些Spark操作会触发一种称为shuffle的事件。Shuffle是Spark重新分布数据的机制,以在分区之间以不同方式进行分组。这通常涉及复制数据到执行器和机器之间,使得shuffle成为一种复杂且耗费资源的操作。

背景

为了理解shuffle期间发生的情况,我们可以以reduceByKey操作为例。reduceByKey操作生成一个新的RDD,其中将所有具有相同键的值合并为一个元组(键和对与该键关联的所有值执行reduce函数后的结果)。挑战在于,并非所有具有相同键的值都驻留在同一个分区甚至同一台机器上,但它们必须在同一位置才能计算结果。

在Spark中,数据通常未按照特定操作所需的位置进行分布。在计算过程中,单个任务仅在单个分区上运行。因此,为了使具有相同reduceByKey操作的所有数据能够执行,Spark需要执行全局操作。它必须从所有分区中读取以找到所有键的所有值,然后跨分区汇总值以计算每个键的最终结果,这就是shuffle操作。

尽管新洗牌数据集的每个分区中的元素集合是确定性的,分区本身的排序也是确定性的,但元素的顺序却不是确定的。如果需要在shuffle之后获得确定顺序的数据,可以使用以下方法:

  • 使用mapPartitions对每个分区进行排序,例如使用.sorted方法。
  • 使用repartitionAndSortWithinPartitions同时重新分区和排序,以高效地对分区进行排序。
  • 使用sortBy创建全局有序的RDD

导致shuffle的操作包括重新分区操作(如repartition和coalesce)、'ByKey操作(除了count)(如groupByKey和reduceByKey)以及连接操作(如cogroup和join)。

性能影响

Shuffle是一种昂贵的操作,因为它涉及磁盘I/O、数据序列化和网络I/O。为了组织shuffle所需的数据,Spark生成一组任务 - map任务用于组织数据,reduce任务用于聚合数据。这个命名法来自于MapReduce,并不直接相关于Spark的map和reduce操作。

内部上,单个map任务的结果会在内存中保留,直到无法容纳为止。然后,根据目标分区对其进行排序,并写入单个文件。在reduce端,任务会读取相关的排序块。

某些shuffle操作可能会消耗大量堆内存,因为它们使用内存数据结构在传输之前或之后对记录进行组织。具体而言,reduceByKey和aggregateByKey在map端创建这些数据结构,而’ByKey操作在reduce端生成这些数据结构。当数据无法容纳在内存中时,Spark将这些表溢出到磁盘,从而产生额外的磁盘I/O开销和增加的垃圾回收。

Shuffle还会在磁盘上生成大量的中间文件。从Spark 1.3版本开始,这些文件会保留,直到相应的RDD不再使用并进行垃圾回收。这样做是为了避免如果重新计算血统时不需要重新创建洗牌文件。垃圾回收可能需要很长时间,如果应用程序保留对这些RDD的引用或者GC不经常启动。这意味着长时间运行的Spark作业可能会占用大量磁盘空间。临时存储目录由配置Spark上下文时的spark.local.dir参数指定。

可以通过调整各种配置参数来调整Shuffle的行为。请参阅Spark配置指南中的“Shuffle Behavior”部分。

RDD持久化

在Spark中,最重要的功能之一是将数据集(RDD)持久化(或缓存)到内存中,跨操作进行重用。当您对一个RDD进行持久化时,每个节点都会将其计算的任何分区存储在内存中,并在该数据集(或从该数据集派生的数据集)上的其他操作中重用它们。这样可以使未来的操作速度更快(通常快10倍以上)。缓存是迭代算法和快速交互使用的关键工具。

您可以使用persist()或cache()方法将RDD标记为持久化。在首次执行操作时,它将在节点的内存中保留。Spark的缓存是容错的 - 如果丢失了RDD的任何分区,它将使用最初创建它的转换重新计算。

此外,每个持久化的RDD可以使用不同的存储级别存储,例如将数据集持久化到磁盘、将其作为序列化的Java对象(以节省空间)持久化到内存中,或在节点之间复制数据集。可以通过将StorageLevel对象(Scala、Java、Python)传递给persist()方法来设置这些级别。cache()方法是使用默认存储级别(StorageLevel.MEMORY_ONLY,将反序列化对象存储在内存中)的简写。

完整的存储级别如下:

存储级别 含义
MEMORY_ONLY 在JVM中以反序列化的Java对象形式存储RDD。如果RDD不适合内存,某些分区将不会被缓存,并且每次需要时都会动态重新计算。这是默认级别。
MEMORY_AND_DISK 在JVM中以反序列化的Java对象形式存储RDD。如果RDD不适合内存,将不适合内存的分区存储在磁盘上,并在需要时从磁盘读取。
MEMORY_ONLY_SER (Java和Scala)以序列化的Java对象形式存储RDD(每个分区一个字节数组)。这通常比反序列化对象更节省空间,特别是当使用快速序列化器时,但读取时需要更多的CPU计算。
MEMORY_AND_DISK_SER (Java和Scala)类似于MEMORY_ONLY_SER,但将不适合内存的分区溢出到磁盘,而不是每次需要时动态重新计算。
DISK_ONLY 仅在磁盘上存储RDD的分区。
MEMORY_ONLY_2、MEMORY_AND_DISK_2等 与上述级别相同,但将每个分区复制到两个集群节点上。
OFF_HEAP(实验性) 类似于MEMORY_ONLY_SER,但将数据存储在堆外内存中。这需要启用堆外内存。

注意:在Python中,存储的对象将始终使用Pickle库进行序列化,因此选择序列化级别无关紧要。Python中可用的存储级别包括MEMORY_ONLY、MEMORY_ONLY_2、MEMORY_AND_DISK、MEMORY_AND_DISK_2、DISK_ONLY、DISK_ONLY_2和DISK_ONLY_3。

Spark还会自动将某些shuffle操作(例如reduceByKey)中的一些中间数据持久化,即使用户没有调用persist。这样做是为了避免在shuffle期间节点失败时重新计算整个输入数据。我们仍然建议用户对生成的RDD调用persist,以便在后续重用。

选择哪个存储级别?

Spark的存储级别旨在在内存使用和CPU效率之间提供不同的权衡。我们建议按照以下步骤选择存储级别:

如果您的RDD可以舒适地适应默认存储级别(MEMORY_ONLY),则保持不变。这是最CPU高效的选项,允许对RDD的操作尽可能快速地运行。

如果不适合,默认存储级别,请尝试使用MEMORY_ONLY_SER,并选择一个快速的序列化库,使对象更节省空间,但仍然可以合理快速地访问。(适用于Java和Scala)

除非计算数据集的函数开销很大,或者它们过滤了大量数据,否则不要溢出到磁盘。否则,重新计算分区可能与从磁盘读取它们一样快。

如果需要快速故障恢复(例如,如果使用Spark为Web应用程序提供请求服务),请使用复制的存储级别。所有存储级别都通过重新计算丢失的数据来提供完全的容错性,但复制级别允许您继续在RDD上运行任务,而无需等待重新计算丢失的分区。

删除数据

Spark会自动监控每个节点上的缓存使用情况,并以最近最少使用(LRU)的方式删除旧的数据分区。如果您想手动删除一个RDD而不是等待它从缓存中移除,请使用RDD.unpersist()方法。请注意,默认情况下,此方法不会阻塞。要阻塞直到释放资源,请在调用此方法时指定blocking=true。

共享变量

通常情况下,当传递给Spark操作(如map或reduce)的函数在远程集群节点上执行时,它会对函数中使用的所有变量的副本进行操作。这些变量被复制到每个机器上,并且对远程机器上变量的更新不会传播回驱动程序。支持跨任务共享一般读写变量将效率低下。然而,Spark为两种常见用法模式提供了两种有限的共享变量类型:广播变量和累加器。

广播变量

广播变量允许程序员将只读变量缓存在每个机器上,而不是将其与任务一起发送的副本。它们可以用于以高效的方式使每个节点都拥有大型输入数据集的副本。Spark还尝试使用高效的广播算法分发广播变量,以减少通信成本。

Spark操作通过一组阶段来执行,阶段之间由分布式的“shuffle”操作分隔。Spark会自动广播在每个阶段的任务中需要的公共数据。以这种方式广播的数据以序列化形式缓存,并在运行每个任务之前进行反序列化。这意味着,仅当多个阶段的任务需要相同的数据或缓存数据以反序列化形式非常重要时,才有必要显式创建广播变量。

广播变量通过调用SparkContext.broadcast(v)从变量v创建。广播变量是v的包装器,可以通过调用value方法访问其值。以下代码展示了这一点:

broadcastVar = sc.broadcast([1, 2, 3])
broadcastVar.value

创建广播变量后,在集群上运行的函数中应使用广播变量而不是值v,以便将v只发送给节点一次。此外,在广播之后不应修改对象v,以确保所有节点都获得相同的广播变量值(例如,如果稍后将变量发送到新节点)。

要释放广播变量在执行器上复制的资源,调用.unpersist()方法。如果之后再次使用广播变量,它将被重新广播。要永久释放广播变量使用的所有资源,请调用.destroy()方法。在那之后无法使用广播变量。请注意,默认情况下,这些方法不会阻塞。要阻塞直到释放资源,请在调用时指定blocking=true。

累加器

累加器是仅通过可交换和可结合操作进行“添加”的变量,因此可以在并行中高效地支持。它们可以用于实现计数器(如MapReduce中的计数器)或求和等功能。Spark原生支持数值类型的累加器,并且程序员可以为其他类型添加支持。

作为用户,您可以创建命名或未命名的累加器。如下图所示,命名累加器(在此示例中为counter)将显示在修改该累加器的阶段的Web界面中。Spark会在“Tasks”表中显示每个任务对累加器的修改的值。
spark RDD 概述用法官方权威资料(建议收藏)_第1张图片

在UI中跟踪累加器可用于了解正在运行的阶段的进度(注意:Python目前不支持此功能)。

accum = sc.longAccumulator("My Accumulator")
data.foreach(lambda x: accum.add(x))
print(accum.value)

尽管此代码使用了内置的长整型累加器,但程序员还可以通过扩展AccumulatorV2来创建自己的类型。AccumulatorV2抽象类有几种方法需要覆盖:reset用于将累加器重置为零,add用于向累加器添加另一个值,merge用于将另一个相同类型的累加器合并到当前累加器中。其他必须覆盖的方法包含在API文档中。例如,假设我们有一个表示数学向量的MyVector类,我们可以编写以下代码:

class VectorAccumulatorV2 extends AccumulatorV2[MyVector, MyVector] {

  private val myVector: MyVector = MyVector.createZeroVector

  def reset(): Unit = {
    myVector.reset()
  }

  def add(v: MyVector): Unit = {
    myVector.add(v)
  }
  ...
}

// 然后,创建此类型的累加器:
val myVectorAcc = new VectorAccumulatorV2
// 然后,将其注册到Spark上下文中:
sc.register(myVectorAcc, "MyVectorAcc1")

请注意,当程序员定义自己的AccumulatorV2类型时,生成的类型可以与添加的元素的类型不同。

对于仅在操作内部执行的累加器更新,Spark保证每个任务对累加器的更新仅应用一次,即重新启动的任务不会更新值。在转换中,如果任务或作业阶段被重新执行,用户应该意识到每个任务的更新可能会多次应用。

累加器不会改变Spark的延迟评估模型。如果它们在RDD上的操作中进行更新,则只有当RDD作为操作的一部分计算时,它们的值才会更新。因此,当在类似map()的惰性转换中进行更新时,并不能保证立即执行累加器更新。以下代码片段演示了这个特性:

accum = sc.longAccumulator()
data.map(lambda x: (accum.add(x), x))
# 这里,accum仍然为0,因为没有操作导致map操作被计算。

部署到集群

应用程序提交指南描述了如何将应用程序提交到集群。简而言之,将应用程序打包成JAR文件(Java/Scala)或一组.py或.zip文件(Python),使用bin/spark-submit脚本将其提交到支持的任何集群管理器。

从Java/Scala启动Spark作业

org.apache.spark.launcher包提供了使用简单的Java API作为子进程启动Spark作业的类。

单元测试

Spark可以与任何流行的单元测试框架进行友好的单元测试。只需在测试中创建一个带有设置为local的master URL的SparkContext,运行操作,然后调用SparkContext.stop()来停止它。确保在finally块或测试框架的tearDown方法中停止上下文,因为Spark不支持在同一程序中同时运行两个上下文

推荐资料

Spark性能权威调优指南 - 小军李的文章 - 知乎
https://zhuanlan.zhihu.com/p/660579229

Spark提交任务官网权威指南详解(建议收藏) - 小军李的文章 - 知乎
https://zhuanlan.zhihu.com/p/660550632

Spark数据类型官网权威详解 - 小军李的文章 - 知乎
https://zhuanlan.zhihu.com/p/660544353

Spark 集群模式概述 - 小军李的文章 - 知乎
https://zhuanlan.zhihu.com/p/660537832

在YARN上启动Spark任务原理用法官方权威资料 - 小军李的文章 - 知乎
https://zhuanlan.zhihu.com/p/660536199

Spark on k8s如何在Kubernetes运行官方权威资料 - 小军李的文章 - 知乎
https://zhuanlan.zhihu.com/p/667436952

你可能感兴趣的:(spark,大数据,spark,大数据,分布式)