Spark基础知识概述 - RDD

概论

较高的层次上,每个Spark应用程序都包含一个驱动程序,该程序运行用户的main功能并在集群上执行各种并行操作。Spark提供的主要抽象是弹性分布式数据集(RDD),它是跨群集节点分区的元素集合,可以并行操作。RDD是通过从Hadoop文件系统(或任何其他Hadoop支持的文件系统)中的文件或驱动程序中的现有Scala集合开始并对其进行转换来创建的。用户还可以要求Spark 在内存中保留 RDD,允许它在并行操作中有效地重用。最后,RDD会自动从节点故障中恢复。
Spark中的第二个抽象是可以在并行操作中使用的共享变量。默认情况下,当Spark并行运行一个函数作为不同节点上的一组任务时,它会将函数中使用的每个变量的副本发送给每个任务。有时,变量需要跨任务共享,或者在任务和驱动程序之间共享。Spark支持两种类型的共享变量:广播变量,可用于缓存所有节点的内存中的值; 累加器,它们是仅“添加”到的变量,例如计数器和总和。

本指南以Spark支持的每种语言显示了这些功能。如果您启动Spark的交互式shell,最简单的方法就是 - bin/spark-shell对于Scala shell或 bin/pysparkPython。

连接Spark

Spark 2.3.2的构建和分发默认情况下与Scala 2.11一起使用。(Spark也可以构建为与其他版本的Scala一起使用。)要在Scala中编写应用程序,您需要使用兼容的Scala版本(例如2.11.X)。

要编写Spark应用程序,需要在Spark上添加Maven依赖项。Spark可通过Maven Central获得:

groupId = org.apache.spark
artifactId = spark-core_2.11
version = 2.3.2

如果希望访问HDFS群集,则需要hadoop-client为您的HDFS版本添加依赖关系 。

groupId = org.apache.hadoop
artifactId = hadoop-client
version = 

最后,需要将一些Spark类导入程序中

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

初始化Spark

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

每个JVM只能激活一个SparkContext。stop()在创建新的SparkContext之前,您必须是活动的SparkContext。

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

appName参数是应用程序在群集UI上显示的名称。 master是Spark,Mesos或YARN群集URL,或以本地模式运行的特殊“本地”字符串。

使用shell

在Spark shell中,已经在名为的变量中为您创建了一个特殊的解释器感知SparkContext sc。制作自己的SparkContext将无法正常工作。可以使用--master参数设置上下文连接到的主服务器,并且可以通过将逗号分隔的列表传递给参数来将JAR添加到类路径中--jars。可以通过向参数提供以逗号分隔的Maven坐标列表,将依赖项(例如Spark包)添加到shell会话中--packages。任何可能存在依赖关系的其他存储库(例如Sonatype)都可以传递给--repositories参数。例如,要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)

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

并行化集合

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

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

一旦创建,分布式数据集(distData)可以并行操作。
并行集合的一个重要参数是将数据集切割为的分区数。Spark将为集群的每个分区运行一个任务。

外部数据集

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

文本文件RDDS可以使用创建SparkContexttextFile方法。此方法需要一个URI的文件(本地路径的机器上,或一个hdfs://s3a://等URI),并读取其作为行的集合。这是一个示例调用:

scala> val distFile = sc.textFile("data.txt")
distFile: org.apache.spark.rdd.RDD[String] = data.txt MapPartitionsRDD[10] at textFile at :26

创建后,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]方法,其中KV是文件中键和值的类型。这些应该是Hadoop的Writable接口的子类,如IntWritable和Text。此外,Spark允许您为一些常见的Writable指定本机类型; 例如,sequenceFile[Int, String]将自动读取IntWritables和文本。

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

  • RDD.saveAsObjectFile并SparkContext.objectFile支持以包含序列化Java对象的简单格式保存RDD。虽然这不像Avro这样的专用格式有效,但它提供了一种保存任何RDD的简便方法。

RDD操作

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

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

默认情况下,每次对其执行操作时,都可以重新计算每个转换后的RDD。但是,您也可以使用(或)方法在内存中保留 RDD ,在这种情况下,Spark会在群集上保留元素,以便在下次查询时更快地访问。还支持在磁盘上保留RDD,或在多个节点之间复制。persistcache

基本操作

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将计算分解为在不同机器上运行的任务,并且每台机器都运行其部分映射和本地缩减,仅返回其对驱动程序的答案。

如果我们lineLengths以后想再次使用,我们可以添加:

lineLengths.persist()

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

将函数传递给Spark

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

  • 匿名函数语法,可用于短片代码。
  • 全局单例对象中的静态方法。例如, 可以定义`object
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里面有引用的 func1方法是的MyClass实例,所以需要发送到群集的整个对象。它类似于写作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),它引用了所有this。要避免此问题,最简单的方法是复制field到本地变量而不是从外部访问它:

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

了解闭包

Spark的一个难点是在跨集群执行代码时理解变量和方法的范围和生命周期。修改其范围之外的变量的RDD操作可能经常引起混淆。在下面的示例中,我们将查看foreach()用于递增计数器的代码,但其他操作也可能出现类似问题。

var counter = 0
var rdd = sc.parallelize(data)

// Wrong: Don't do this!!
rdd.foreach(x => counter += x)

println("Counter value: " + counter)

本地与群集模式

上述代码的行为未定义,可能无法按预期工作。为了执行作业,Spark将RDD操作的处理分解为任务,每个任务由执行程序执行。在执行之前,Spark计算任务的关闭。闭包是那些变量和方法,它们必须是可见的,以便执行者在RDD上执行计算(在这种情况下foreach())。该闭包被序列化并发送给每个执行者。

发送给每个执行程序的闭包内的变量现在是副本,因此,当在函数内引用计数器时foreach,它不再是驱动程序节点上的计数器。驱动程序节点的内存中仍然有一个计数器,但执行程序不再可见!执行程序只能看到序列化闭包中的副本。因此,计数器的最终值仍然为零,因为计数器上的所有操作都引用了序列化闭包内的值。
在本地模式下,在某些情况下,该`foreach`函数实际上将在与驱动程序相同的JVM中执行,并将引用相同的原始**计数器**,并且可能实际更新它。

为了确保在这些场景中明确定义的行为,应该使用[`Accumulator`](http://spark.apache.org/docs/latest/rdd-programming-guide.html#accumulators)。Spark中的累加器专门用于提供一种机制,用于在跨集群中的工作节点拆分执行时安全地更新变量。本指南的“累加器”部分更详细地讨论了这些内容。

通常,闭包 - 类似循环或本地定义的方法的构造不应该用于改变某些全局状态。Spark没有定义或保证从闭包外部引用的对象的突变行为。执行此操作的某些代码可能在本地模式下工作,但这只是偶然的,并且此类代码在分布式模式下不会按预期运行。如果需要某些全局聚合,请使用累加器。

打印RDD的元素

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

使用键值对

虽然大多数Spark操作都适用于包含任何类型对象的RDD,但一些特殊操作仅适用于键值对的RDD。最常见的是分布式“随机”操作,例如通过密钥对元素进行分组或聚合。

在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)

转换

下表列出了Spark支持的一些常见转换。有关详细信息,请参阅RDD API文档(Scala, Java, Python, R)并配对RDD函数doc(Scala,Java)。

| Transformation | Meaning |
| map(func) | Return a new distributed dataset formed by passing each element of the source through a function func. |
| filter(func) | Return a new dataset formed by selecting those elements of the source on which funcreturns true. |
| flatMap(func) | Similar to map, but each input item can be mapped to 0 or more output items (so funcshould return a Seq rather than a single item). |
| mapPartitions(func) | Similar to map, but runs separately on each partition (block) of the RDD, so func must be of type Iterator => Iterator when running on an RDD of type T. |
| mapPartitionsWithIndex(func) | Similar to mapPartitions, but also provides func with an integer value representing the index of the partition, so func must be of type (Int, Iterator) => Iterator when running on an RDD of type T. |
| sample(withReplacement, fraction, seed) | Sample a fraction fraction of the data, with or without replacement, using a given random number generator seed. |
| union(otherDataset) | Return a new dataset that contains the union of the elements in the source dataset and the argument. |
| intersection(otherDataset) | Return a new RDD that contains the intersection of elements in the source dataset and the argument. |
| distinct([numPartitions])) | Return a new dataset that contains the distinct elements of the source dataset. |
| groupByKey([numPartitions]) | When called on a dataset of (K, V) pairs, returns a dataset of (K, Iterable) pairs.
Note: If you are grouping in order to perform an aggregation (such as a sum or average) over each key, using reduceByKey or aggregateByKey will yield much better performance.
Note: By default, the level of parallelism in the output depends on the number of partitions of the parent RDD. You can pass an optional numPartitions argument to set a different number of tasks. |
| reduceByKey(func, [numPartitions]) | When called on a dataset of (K, V) pairs, returns a dataset of (K, V) pairs where the values for each key are aggregated using the given reduce function func, which must be of type (V,V) => V. Like in groupByKey, the number of reduce tasks is configurable through an optional second argument. |
| aggregateByKey(zeroValue)(seqOp, combOp, [numPartitions]) | When called on a dataset of (K, V) pairs, returns a dataset of (K, U) pairs where the values for each key are aggregated using the given combine functions and a neutral "zero" value. Allows an aggregated value type that is different than the input value type, while avoiding unnecessary allocations. Like in groupByKey, the number of reduce tasks is configurable through an optional second argument. |
| sortByKey([ascending], [numPartitions]) | When called on a dataset of (K, V) pairs where K implements Ordered, returns a dataset of (K, V) pairs sorted by keys in ascending or descending order, as specified in the boolean ascending argument. |
| join(otherDataset, [numPartitions]) | When called on datasets of type (K, V) and (K, W), returns a dataset of (K, (V, W)) pairs with all pairs of elements for each key. Outer joins are supported through leftOuterJoin, rightOuterJoin, and fullOuterJoin. |
| cogroup(otherDataset, [numPartitions]) | When called on datasets of type (K, V) and (K, W), returns a dataset of (K, (Iterable, Iterable)) tuples. This operation is also called groupWith. |
| cartesian(otherDataset) | When called on datasets of types T and U, returns a dataset of (T, U) pairs (all pairs of elements). |
| pipe(command, [envVars]) | Pipe each partition of the RDD through a shell command, e.g. a Perl or bash script. RDD elements are written to the process's stdin and lines output to its stdout are returned as an RDD of strings. |
| coalesce(numPartitions) | Decrease the number of partitions in the RDD to numPartitions. Useful for running operations more efficiently after filtering down a large dataset. |
| repartition(numPartitions) | Reshuffle the data in the RDD randomly to create either more or fewer partitions and balance it across them. This always shuffles all data over the network. |
| repartitionAndSortWithinPartitions(partitioner) | Repartition the RDD according to the given partitioner and, within each resulting partition, sort records by their keys. This is more efficient than calling repartition and then sorting within each partition because it can push the sorting down into the shuffle machinery. |

操作

下表列出了Spark支持的一些常见操作。请参阅RDD API文档(Scala, Java, Python, R)

并配对RDD函数doc(Scala, Java)以获取详细信息。

| reduce(func) | Aggregate the elements of the dataset using a function func (which takes two arguments and returns one). The function should be commutative and associative so that it can be computed correctly in parallel. |
| collect() | Return all the elements of the dataset as an array at the driver program. This is usually useful after a filter or other operation that returns a sufficiently small subset of the data. |
| count() | Return the number of elements in the dataset. |
| first() | Return the first element of the dataset (similar to take(1)). |
| take(n) | Return an array with the first n elements of the dataset. |
| takeSample(withReplacement, num, [seed]) | Return an array with a random sample of num elements of the dataset, with or without replacement, optionally pre-specifying a random number generator seed. |
| takeOrdered(n, [ordering]) | Return the first n elements of the RDD using either their natural order or a custom comparator. |
| saveAsTextFile(path) | Write the elements of the dataset as a text file (or set of text files) in a given directory in the local filesystem, HDFS or any other Hadoop-supported file system. Spark will call toString on each element to convert it to a line of text in the file. |
| saveAsSequenceFile(path)
(Java and Scala) | Write the elements of the dataset as a Hadoop SequenceFile in a given path in the local filesystem, HDFS or any other Hadoop-supported file system. This is available on RDDs of key-value pairs that implement Hadoop's Writable interface. In Scala, it is also available on types that are implicitly convertible to Writable (Spark includes conversions for basic types like Int, Double, String, etc). |
| saveAsObjectFile(path)
(Java and Scala) | Write the elements of the dataset in a simple format using Java serialization, which can then be loaded usingSparkContext.objectFile(). |
| countByKey() | Only available on RDDs of type (K, V). Returns a hashmap of (K, Int) pairs with the count of each key. |
| foreach(func) | Run a function func on each element of the dataset. This is usually done for side effects such as updating an Accumulator or interacting with external storage systems.
Note: modifying variables other than Accumulators outside of the foreach() may result in undefined behavior. See Understanding closures for more details. |

随机操作

Spark中的某些操作会触发称为shuffle的事件。随机播放是Spark的重新分配数据的机制,因此它可以跨分区进行不同的分组。这通常涉及跨执行程序和机器复制数据,使得混洗成为复杂且昂贵的操作。

背景

为了理解在洗牌过程中发生的事情,我们可以考虑reduceByKey操作的例子 。该reduceByKey操作生成一个新的RDD,其中单个键的所有值都组合成一个元组 - 键和对与该键关联的所有值执行reduce函数的结果。挑战在于,并非单个密钥的所有值都必须位于同一个分区,甚至是同一个机器上,但它们必须位于同一位置才能计算结果。

在Spark中,数据通常不跨分区分布,以便在特定操作的必要位置。在计算过程中,单个任务将在单个分区上运行 - 因此,为了组织reduceByKey执行单个reduce任务的所有数据,Spark需要执行全部操作。它必须从所有分区读取以查找所有键的所有值,然后将各个值组合在一起以计算每个键的最终结果 - 这称为shuffle

尽管新洗牌数据的每个分区中的元素集将是确定性的,并且分区本身的排序也是如此,但这些元素的排序不是。如果在随机播放后需要可预测的有序数据,则可以使用:

  • mapPartitions 例如,使用以下方式对每个分区进行排序 .sorted
  • repartitionAndSortWithinPartitions 在重新分区的同时有效地对分区进行排序
  • sortBy 制作全局有序的RDD

可以导致混洗的操作包括重新分区操作,如 repartitioncoalesce'ByKey操作(除了计数)之类的groupByKeyreduceByKey,以及 连接操作,如cogroupjoin

所述随机播放是昂贵的操作,因为它涉及的磁盘I / O,数据序列,和网络I / O。为了组织shuffle的数据,Spark生成多组任务 - 映射任务以组织数据,以及一组reduce任务来聚合它。这个术语来自MapReduce,并不直接与Spark mapreduce操作相关。

在内部,各个地图任务的结果会保留在内存中,直到它们无法适应。然后,这些基于目标分区进行排序并写入单个文件。在reduce方面,任务读取相关的排序块。

某些shuffle操作会消耗大量的堆内存,因为它们使用内存中的数据结构来在传输记录之前或之后组织记录。具体而言, reduceByKeyaggregateByKey创建在地图上侧这样的结构,和'ByKey操作产生这些上减少侧。当数据不适合内存时,Spark会将这些表溢出到磁盘,从而导致磁盘I / O的额外开销和垃圾收集增加。

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

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

你可能感兴趣的:(Spark基础知识概述 - RDD)