弹性分布式数据集(RDD)

Spark围绕弹性分布式数据集(RDD)的概念展开,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)

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会尝试根据您的集群自动设置分区数。但是,您也可以通过将其作为第二个参数传递来进行手动设置以进行并行化(例如sc.parallelize(data, 10)))。注意:代码中的某些地方使用术语切片(分区的同义词)来保持向后兼容性。

外部数据集

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

可以使用SparkContext的textFile方法创建文本文件RDD。这个方法需要文件的URI作为参数(机器上的本地路径、或hdfs://s3n://等URI),并将其读取为行的集合。如下是一个示例调用:

scala> val distFile = sc.textFile("data.txt")
distFile: org.apache.spark.rdd.RDD[String] = data.txt MapPartitionsRDD[10] at textFile at :26
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读取文件的一些注意事项:

  • 如果使用本地文件系统路径,则必须在工作节点上的使用相同路径去访问该文件。要么将文件复制给所有的worker 节点,要么使用网络安装的共享文件系统。
  • Spark的所有基于文件的输入方法,包括textFile,都支持传入文件的目录、压缩文件以及通配符,例如,您可以使用textFile("/my/directory")textFile("/my/directory/*.txt")textFile("/my/directory/*.gz")
  • textFile方法还带有一个可选的第二个参数,用于控制文件的分区数。默认情况下,Spark为文件的每个块创建一个分区(HDFS中块的默认大小为128MB),但是您也可以通过传递更大的值来请求更多数量的分区。请注意,分区的数量不能少于块的数量。

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

  • SparkContext.wholeTextFiles方法可让您读取包含多个小文本文件的目录,并对每一个小文本文件以(filename, content)对的形式返回。这与textFile方法相反,textFile方法将在每个文件的每一行返回一条记录。
  • 对于SequenceFiles,请使用SparkContext的sequenceFile[K,V]方法,其中K和V是文件中key和value的类型。key和value的类型需要实现Hadoop中的Writable接口,例如IntWritable和Text。此外,Spark允许您为一些常见的Writables指定本地类型。例如,sequenceFile [Int,String]将自动读取IntWritables和Texts。
  • 对于其他Hadoop InputFormat,你可以使用SparkContext.hadoopRDD方法,该方法需要输入任意的JobConf、input format类型、键类型和值类型。如何设置这些参数是与对输入源进行Hadoop作业的方式相同。您还可以基于新的MapReduce API(org.apache.hadoop.mapreduce)SparkContext.newAPIHadoopRDD用于InputFormats。
  • RDD.saveAsObjectFileSparkContext.objectFile支持以包含序列化Java对象的简单格式来保存RDD。尽管这不像Avro这样的专用格式有效,但它提供了一种保存任何RDD的简便方法。

RDD操作

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

Spark中的所有transformations操作都是懒加载的,因为它们不会立即计算出结果。相反,他们只记得应用于某些基本数据集(例如文件)的transformations操作。仅当action操作被执行才会计算transformations操作并将计算结果返回给驱动程序。这种设计使Spark可以更高效地运行。例如,我们可以知道到通过map创建的数据集将用于reduce中,并且仅将reduce的结果返回给驱动程序,而不是将较大的做过map计算的数据集返回给驱动程序。

默认情况下,每次在执行操作时,可能会重新计算每个转换后的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)

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

第一行从外部文件定义了一个基本RDD。该数据集不会加载到内存中或没有其他的作用:行仅仅是文件的引用。第二行将mapLengths定义为map转换的结果。同样,由于懒加载,不会立即计算出lineLengths的结果。最后,我们运行reduce,这是一个action算子。此时,Spark将计算分解为任务并在不同的机器上运行,并且每台机器都运行map计算的一部分以及在每台机器上进行reduce计算,仅将计算的结果返回给驱动程序。

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

lineLengths.persist()

lineLengths.persist()

在执行reduce算子之前,这将会使在第一次计算之后将lineLengths保存在内存中。

将函数传递给Spark

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

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

myRdd.map(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) }
}

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

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

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

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

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

理解闭包

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

案例

思考下面计算普通RDD元素的总和,执行结果可能会有所不同,具体取决于是否在同一个JVM虚拟机中执行。一个常见的例子是在本地模式下运行Spark(--master = local [n])而不是将Spark应用程序部署到集群上(例如,通过将spark-submit提交给YARN):

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

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

println("Counter value: " + counter)

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函数中引用counter变量时,它不再是驱动程序节点上的counter变量。驱动程序节点的内存中仍然存在一个counter 变量,但是该counter 变量不再对执行器可见!执行器仅从序列化的闭包中看到副本。因此,因为对counter 变量的所有操作都引用了序列化闭包内的值,所以最终counter 变量的值将仍然为零。

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

为了确保在这种情况下的执行结果明确,应该使用累加器。Spark中的累加器专门用于提供一种机制,用于在集群中的各个工作节点之间拆分执行时安全地更新变量。本指南的累加器部分将详细讨论这些内容。

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

打印RDD的元素

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



hadoop和spark使用场景 spark和hadoop结合_Hadoop