Spark学习三:RDD介绍及编程

Overview(总览)

Spark提供的主要抽象就是弹性分布式数据集 - - RDD(resilient distributed dataset),它是跨集群节点的分区元素的集合(RDD是有分区的),是可以并行操作的。
RDD的创建有两种方式:(一)从Hadoop文件系统(或任何其他Hadoop支持的文件系统)的文件读取创建 (二)从driver program的已存在的scala集合中创建并转化。
用户可以要求Spark将RDD持久化到内存中,这样在并行操作时就能有效地重用它。最后RDD可以从节点故障中自动恢复。

Spark的第二个抽象就是在并行操作中使用的共享变量shared variables。当Spark执行一个并行操作的时候,会将函数中使用到的变量复制到每一个task里。有时候,一个变量需要在多个task之间、或者是task和driver program之间进行共享。
Spark支持两种类型的共享变量:(一)broadcast variables(广播变量),能将一个变量缓存到所有节点的内存中 (二)accumators(累加器),只能用来做"added"追加操作,例如计数和求和操作。

Linking with Spark

添加maven依赖,可以参考Spark学习一:安装、IDEA编写代码

<properties>
    <!--解决maven打包时编码不对的问题-->
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spark.version>2.4.5</spark.version>
    <scala.version>2.11</scala.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-core_${scala.version}</artifactId>
        <version>${spark.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-streaming_${scala.version}</artifactId>
        <version>${spark.version}</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Initializing Spark

一个spark program首先要做的就是创建一个SparkContext对象,它告诉spark如何访问一个集群。为了创建一个SparkContext对象首先要做的就是创建一个SparkConf对象,SparkConf对象包含了application的信息。
每个JVM只会有一个active的SparkContext,在创建新的SparkContext对象之前必须要执行SparkContext.stop()来停止active的SparkContext

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

appName参数是显示在ui界面的application的名字,master参数是application选择哪个cluster manager(spark standalone、mesos、yarn)或者本地模式local。
在正式环境下一般都不会在代码里对master做硬编码,会在使用spark-submit提交作业时用参数--master来指定。

Using the Shell

当你启动 spark-shell 的时候,会自动的创建一个 SparkContext 对象,叫 “sc”,如果你打算手动创建一个 SparkContext,它是不会起作用的。可以用 --master 参数来告诉 SparkContext 连接到哪个master上。可以用 --jars 参数(多个jar用逗号隔开)来将一些jar包导入到driver program和executor的classpath里。可以用--packages--repositories来导入更复杂的依赖。
可以通过 spark-shell --help 来查询更详细的信息,下图是截取的部分信息
在这里插入图片描述

spark-shell --master local[*] --jars code.jar

Resilient Distributed Datasets (RDDs)

弹性分布式数据集RDD 是具有高容错性且能并行执行的数据集合。有两种方式去创建RDDS:(1) 对driver program程序上的已存在的集合做parallelizing化; (2) 引用外部存储系统的数据集来创建,例如HDFS、HBase、或任何Hadoop支持的存储系统。

Parallelized Collections(并行化集合)

Parallelized collections是通过在driver program程序中对已存在的collection调用SparkContext.parallelize方法来创建的。集合的元素会被复制称为一个可以并行操作的分布式数据集RDD。如下面所示,根据1-5的数据集合来创建一个Parallelized collections即RDD

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

在这里插入图片描述
一旦创建,分布式数据集RDD就能够并行操作了。例如我们可以做累加操作distData.reduce((a, b) => a + b)。具体对RDD可以做哪些操作后面有详细的介绍。
在这里插入图片描述
对于parallel collections一个很重要的参数就是将数据集分成多少分区即the number of partitionsSpark会对集群上的每个partition都会执行一个task,即partition数决定了task的并行度。通常而言,集群中每个CPU对应2-4个partition。正常情况下,Spark会自动根据你的集群来设置分区数。你也可以通过传递第二个参数手动地设置分区数(如sparkContext.textFile(path=path,minPartitions = 3))。注意:代码里有些地方用了术语slices(和partitions是一样的意思)来保持向后兼容。

扫描二维码关注公众号,回复: 9645660 查看本文章

External Datasets(外部数据集)

Spark可以从任何Hadoop支持的存储系统来创建弹性分布式数据集RDD,包括本地文件系统、HDFS、Cassandra、HBase、 Amazon S3等。Spark支持text files、SequenceFIles和任意的Hadoop InputFormat
可以使用sparkContext.textFile方法来创建Text file RDDs。该方法以文件的URI路径(如本地路径、hdfs://、s3d://等)作为参数,然后将文件的内容按行读取成一个集合即RDD。如下所示
默认读取的就是hdfs路径

scala> val path = "/user/root/input/words.txt"
path: String = /user/root/input/words.txt

scala> val textFileRDD = sc.textFile(path)
textFileRDD: org.apache.spark.rdd.RDD[String] = /user/root/input/words.txt MapPartitionsRDD[1] at textFile at <console>:26

获取到RDD后就可以进行操作了,例如可以看下文件有多少个字符,先做map操作获取每行的字符数,然后做reduce操作进行累加。

scala> textFileRDD.map(s=>s.length).reduce(_+_)
res2: Int = 186

用Spark读取文件时需要注意以下几点

  • 如果使用的是本地文件系统,那么文件的路径必须在所有的worker node上都是可访问的(因为是分布式计算任务)。要么拷贝文件到所有的worker node上,要么使用网络挂载的共享文件系统。
  • Spark 所有读取文件的方法 ,包括textFile,同样支持对文件夹directory、压缩文件compressed files的读取,还允许读取的路径中带有通配符,也可以用逗号分隔读取多个文件。例如textFile("/my/directory"), textFile("/my/directory/*.txt"), and textFile("/my/directory/*.gz"), textFile("/my/directory1,/my/directory2")
  • textFile方法也提供一个可选的第二个参数来控制分区数。默认情况下,Spark会为文件的每个block创建一个partition(HDFS的block size默认是128MB) ,但是你也可以通过传入一个更大的值,来指定更多的 partitions。注意不能指定比block数还要小的分区数

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

  • SparkContext.wholeTextFiles 会读取目录里所有的文件(建议都是小文件),然后返回给一个元素类型为Tuple:(文件路径, 文件内容)的RDD。这个方法与textFile是完全不同的,textFile是按行读取内容成为一个元素类型为String的RDD。分区由数据位置data locality决定,在某些情况下,可能导致分区太少。对于这些情况,wholeTextFiles也提供了一个可选的第二个参数来控制分区的最小数量。
  • 对于 SequenceFiles,可以使用 SparkContext.sequenceFile[K, V](path: String,keyClass: Class[K],valueClass: Class[V]): RDD[(K, V)]方法创建。keyClass/valueClass必须是 Hadoop 的 Writable interface 的子类,例如IntWritable 和 Text 。另外,对于一些通用的 Writable,Spark 允许你指定原生类型native types来替代。如:sequencFile[Int, String] 将会自动读取 IntWritablesTextssc.sequenceFile[Int,String]("hdfs:///path")
  • 对于其他的Hadoop InputFormats,可以使用SparkContext.hadoopRDD方法。最好是使用SparkContext.newAPIHadoopRDD,因为这个是基于新的MapReduce API (org.apache.hadoop.mapreduce)
    可以参考下面的例子
val path = "/user/root/input/words.txt"
val conf = new Configuration()
conf.set(FileInputFormat.INPUT_DIR,path);
//注意newAPIHadoopRDD方法里注释给的提示,在使用返回的RDD前最好是用map做下复制
//因为Hadoop的RecordReader类为每个记录重用相同的可写对象
val kvRDD = sc.newAPIHadoopRDD(conf,
  //如果不加上asSubclass会报错 do not conform to method newAPIHadoopRDD's type parameter bounds [K,V,F <: org.apache.hadoop.mapreduce.InputFormat[K,V]]
  // https://stackoverflow.com/questions/37938695/spark-cannot-compile-newapihadooprdd-with-mongo-hadoop-connectors-bsonfileinput
  classOf[TextInputFormat].asSubclass(classOf[InputFormat[LongWritable,Text]]),
  classOf[LongWritable],
  classOf[Text])

//注意返回给driver之前需要做序列化,如果不做map这个过程会报错
//Job aborted due to stage failure: Task 0.0 in stage 1.0 (TID 1) had a not serializable result: org.apache.hadoop.io.LongWritable
// https://stackoverflow.com/questions/28159185/streaming-from-hbase-using-spark-not-serializable?r=SearchResults
kvRDD.map(x=>x._2.toString()).collect().foreach(println)
val wcRDD = kvRDD.map(x=>x._2.toString()).flatMap(x=>x.split(" ")).map(x=>(x,1)).reduceByKey(_+_)
wcRDD.collect().foreach(println)
  • RDD.saveAsObjectFileSparkContext.objectFile 支持以由序列化Java对象组成的简单格式保存RDD。虽然这不如Avro这样的专门格式有效,但是它提供了一种简单的方法来保存任何RDD。其实RDD.saveAsObjectFile底层就是将RDD的数据保存为Hadoop的SequenceFile,然后SparkContext.objectFile就是通过调用SparkContext.sequenceFile来读取文件。也可以关注下RDD.saveAsTextFile方法,将RDD数据以元素的string形式保存为文本文件,也支持压缩方法,然后用SparkContext.textFile方法读取时如果是压缩的也会自动解压缩
    在这里插入图片描述
    在这里插入图片描述

RDD Operations(RDD操作)

RDD支持两种操作:
(1)transformation,用于从一个已存在的RDD来创建新的RDD
(2)action,对RDD数据进行计算然后返回值给driver program
例如,map就是一个transformation,会将RDD的每个元素都经过function(map的参数,外部传过来的)然后返回一个表示结果的新的RDD。另一方面,reduce就是一个action,会用一个function(reduce的参数,外部传过来的)来聚合RDD里的所有元素,然后把最终结果返回给driver program(注意:有一个并行的reduceByKey会返回一个RDD,不过这是一个transformation)

Spark里所有的transformation都是lazy的,它们并不会立即计算它们的结果。然而,它们仅仅记录应用到RDD的transformation。仅当一个action需要结果返回给driver programtransformation才会真正的开始计算。这种设计让Spark运行的更加高效。例如,从map过来的RDD会在reduce中用到并且仅需要返回reduce的结果给driver,而不是更大的mapper数据集。

默认情况下,每次有在执行一个action时每个ransformed RDD都会被重新计算。然而,你也可以通过persist (or cache)方法来在内存里保存RDD,这样的话Spark就会将数据保存到集群上,那么下次你需要再次用到这个RDD时就不需要重算就能工作的更快。也支持持久化RDD到磁盘上,也支持给RDD在节点间创建副本。

Basics(基础)

看下面的程序段来理解RDD基础

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

第一行就是从外部文件来创建一个RDD。该数据集是没有加载到内存上的:lines仅仅只是一个对这个文件的pointer。第二行就是获取每行的长度作为map transformation的结果。同样地,lineLengths由于laziness也不会立刻计算。最后,我们执行reduce这个action。此时,Spark将计算分解为在不同的机器上运行的task,每台机器都运行其part of the map and a local reduction,只向driver program返回其答案。
如果我们后面程序段会再次用到lineLengths,可以在执行reduce操作前通过lineLengths.persist()来持久化lineLengths这个RDD,这样会在lineLengths第一次被计算时会持久化到内存里。

Passing Functions to Spark(传递function给Spark)

Spark的API严重依赖于driver program中传递的函数去在集群上运行。有两个比较建议的方式去传递function:

  • 匿名函数(Anonymous function syntax),用在代码段量小的地方
  • 单例object的静态方法(scala是通过object关键字来定义单例对象的)。看下面的例子,定义了object MyFunctions然后传递MyFunctions.func1
object MyFunctions {
  def func1(s: String): String = { ... }
}

myRdd.map(MyFunctions.func1)

注意尽管也可以通过传递class实例(和singleton object做比较)中方法的引用,但这需要传递包含这个方法的class的实例对象。考虑下面的场景:

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

这里如果我们创建一个新的MyClass实例对象并且调用它的doStuff方法,doStuff方法里面的map引用了该MyClass实例对象的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),因此也需要传递整个MyClass实例对象给集群。
为了避免这种情况,最简单的方法是将字段复制到一个局部变量中,而不是从外部访问它,如:

def doStuff(rdd: RDD[String]): RDD[String] = {
  val field_ = this.field
  rdd.map(x => field_ + x)
}
Understanding closures(理解闭包)

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

Example

考虑下面简单的RDD元素求和,它的结果可能会根据是否在相同的JVM中执行而有所不同。
比较通用的例子就是以本地模式(--master = local[n])运行Spark和以Spark on yarn(spark-submit to YARN)的模式运行Spark来做比较。

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

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

println("Counter value: " + counter)
Local vs. cluster modes(本地模式VS集群模式)

上面代码的结果可能并不像我们想象中那样累计求和。为了执行作业,Spark将RDD操作分解为tasks,每个task由一个executor执行。在执行之前,Spark会先计算task的closure(闭包)。closure是那些在RDD上执行其计算时必须可见的变量和方法(在上面的例子中为foreach())。这个closure被序列化并发送给每个executor

发送给每个executorclosure里的变量其实是copies(副本)。因此在foreach函数中引用counter时,它不再是driver program节点上的counter。在driver program节点的内存里仍然存在一个counter,但是对executors是不可见的!!!executor仅仅能看到序列化的closure里的副本。因此,counter的最终值仍然是零,因为counter上的所有操作都引用了序列化closure中的值。
在这里插入图片描述
在本地模式下,在某些情况下,foreach函数实际上会在与驱动程序相同的JVM中执行,并引用相同的原始计数器,并可能实际更新它。

为了确保在该类场景下的正确行为,应该要使用累加器(Accumulator)。Spark中的Accumulator专门用于提供一种机制,以便当任务在集群中的工作节点上执行时安全地更新变量。下面有专门的部分更加详细地讲述了Accumulator

一般来说,像循环或局部定义的方法这样的闭包结构(closures - constructs)不应该用来改变全局状态。Spark不定义也不保证闭包外部引用的对象的行为。一些这样做的代码可能在本地模式下工作,但那只是偶然的,而且这样的代码在分布式模式下不会像预期的那样工作。如果需要全局聚合( global aggregation),则使用累加器(Accumulator)。

Printing elements of an RDD(打印RDD中的元素)

另一常见的习惯用法就是使用rdd.foreach(println)或者rdd.map(println)来打印RDD中的元素。在一台机器上,这将生成预期的输出并打印所有的RDD元素。然而,在集群模式下executors调用stdout的输出是写入executors的stdout文件上,而不是driver program的stdout上。所有driver program的stdout上是看不到这些元素的! 我们可以首先使用collect()方法把RDD数据收集到driver program节点上然后再打印即rdd.collect().foreach(println)。这可能会导致driver program节点耗尽内存,因为collect()会把整个RDD数据收集到
driver program节点这一台机器上。如果你仅仅需要打印RDD中的少量元素,更加安全的操作是使用take(),如rdd.take(100).foreach(println)

Working with Key-Value Pairs(使用Key-Value对)

虽然大多数Spark操作是在包含任何类型对象的RDD上工作,但也有少数特殊操作在键值对的RDDs上工作。最常见的就是分布式“shuffke”操作,如groupByreduceByKey
在Scala中,这些操作在包含Tuple2对象(scala语言中内置的元组,通过简单的编写(a, b)创建)的RDDs上自动可用。键值对操作在PairRDDFunctions类中是可用的,它自动包装元组的RDD。

例如,下面的代码使用基于key-value对的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()来把所有的数据作为数组返回给driver program
注意:在键-值对操作中使用自定义对象作为key时,必须确保自定义equals()方法和hashCode()方法。更加详细的内容可以参考Object.hashCode() documentation.
在这里插入图片描述

Transformations And Actions(两种算子)

请参考我写的另一篇博客RDD两种算子

Shuffle operations(shuffle操作)

shuffle是Spark用于重新分布数据的机制,以便在分区之间以不同的方式进行分组。这通常会涉及到跨executor和节点复制数据,使得shuffle成为一个复杂而昂贵的操作。

Background(背景)

为了明白在shuffle期间到底发生了,我们以reduceByKey为例来说明下。reduceByKey操作生成一个新的RDD,其中单个key的所有值都被组合成一个元组即key和对与该键关联的所有值执行reduce函数的结果。难点在于,单个key的所有值不一定都位于相同的分区,甚至也不一定位于同一台机器上,但它们必须位于同一位置才能计算结果。

在Spark中,数据通常不会跨分区分布到特定操作所需的位置。在计算期间,单个task将在单个partition上操作——因此,要组织单个reduceByKey reduce任务执行的所有数据,Spark需要执行all-to-all操作。它必须从所有分区中读取所有键的值,然后将各个分区的值放在一起,以计算每个key的最终结果——这称为shuffle。

尽管shuffle后的数据的每个分区中的元素集是确定的,分区本身的排序也是确定的,但是这些元素的排序是不确定的。如果一个人想要在shuffle之后得到有序数据,那么可以使用:

  • mapPartitions对每个分区进行排序,例如 .sorted对分区内的数据进行排序
  • repartitionAndSortWithinPartitions可以在进行重新分区的同时对分区进行排序
  • 使用sortBy函数来排序,其实底层用的就是sortByKey,参数是一个函数
/**
   * Return this RDD sorted by the given key function.
   */
  def sortBy[K](
      f: (T) => K,
      ascending: Boolean = true,
      numPartitions: Int = this.partitions.length)
      (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]
val numRDD = sc.parallelize(Array(2,3,1,0,5,9,6,7,8,20,11,4,18),3)
scala> numRDD.mapPartitionsWithIndex((index,iter)=>{
     |       iter.toList.sorted.iterator
     |     }).collect()
res4: Array[Int] = Array(0, 1, 2, 3, 5, 6, 7, 9, 4, 8, 11, 18, 20)
scala> numRDD.sortBy(x=>x).collect()
res8: Array[Int] = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 18, 20)

scala> numRDD.sortBy(x=>x,false).collect()
res9: Array[Int] = Array(20, 18, 11, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
Performance Impact(性能影响)

shuffle是一个非常昂贵的操作因为它涉及到了磁盘IO,数据序列化以及网络IO。为了在shuffle期间组织数据,Spark生成了很多任务---- map task用于组织数据,reduce task用于聚合。这个术语来自MapReduce,但是与Spark的map和reduce操作没有直接关系。

在内部,单个map task的结果会保存到内存里直到内存放不下。然后数据基于目标分区进行排序然后写到一个文件里(这个过程可以参考MapReduce里的shuffle过程)。在reduce端,reduce task获取相对应的排序数据块。

某些shuffle操作会消耗大量的堆内存因为它们需要在传输数据之前和之后用到内存里的数据结构去组织数据。特别地,reduceByKeyaggregateByKey在map端创建这些数据结构,然后ByKey操作在reduce端生成这些结构。当内存放不下数据的时候,Spark会溢出一些数据到磁盘,会导致额外的磁盘IO开销及增加GC操作。

shuffle也会在磁盘生成大量的中间文件。从Spark1.3开始,这些文件会一直保存直到对应的RDD不再使用并且已经开始GC回收。这么做的原因是在根据rdd lineage重新计算时就没必要重新创建shuffle文件了。如果application保留对这些RDDs的引用,或者GC不经常启动,那么GC回收操作可能只会在很长一段时间之后才会发生。这意味着长时间运行的Spark作业可能会消耗大量磁盘空间。临时存储目录是根据spark.local.dir来指定的。如果是spark on yarn的话,该属性会被yarn集群的yarn.nodemanager.local-dirs替代。

可以参考shuffle-behavior的配置信息来对shuffle进行调整。

RDD Persistence(RDD持久化)

Spark一个最重要的能力就是可以对RDD进行持久化存储在内存或磁盘。当你持久化一个RDD后,每个节点都会存储该RDD的一些partition这样它就能在内存里计算并且在某些引用该RDD的action算子触发时可以重用这个RDD。这会运行后面的action算子更快(通常会快十倍以上)。缓存对于迭代算法和快速交互使用是一个非常重要的工具。

可以通过persist()或者cache()方法来持久化RDD(这两个方法都是将RDD只存放在内存里)。当第一次在一个action算子里计算的时候,该RDD就会保存在各个节点的内存里。Spark的缓存是有容错机制的----如果一个RDD中的任何partition丢失了,它都会根据lineage自动重算的。

另外。每个持久化的RDD都可以使用不同的存储级别去存储,例如,持久化到磁盘,持久化到内存但是作为序列化的Java对象(为了节省空间),在节点间进行复制。这些级别可以通过传递参数StorageLevelpersist()方法里。cache()方法就是使用默认的存储级别即StorageLevel.MEMORY_ONLY(在内存里存储反序列化对象)。完整的存储级别如下:

Storage Level Meaning
MEMORY_ONLY 将RDD作为反序列化的Java对象存储在JVM中。如果内存不够,那么一些分区将不会被缓存,而是在需要它们时动态地重新计算。这是默认级别。
MEMORY_AND_DISK 将RDD作为反序列化的Java对象存储在JVM中。如果内存不够,那么没有存到内存的分区会存储在磁盘上,并在需要时从磁盘读取它们。
MEMORY_ONLY_SER 将RDD存储为序列化的Java对象(每个分区一个字节数组(byte Array))。这通常比反序列化对象更节省空间,特别是在使用fast serializer时,但读取时需要更多cpu。
MEMORY_AND_DISK_SER 类似于MEMORY_ONLY_SER
DISK_ONLY RDD数据只存放在磁盘上
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc. 和上面的一样,只不过多个一个副本
OFF_HEAP (experimental) 类似于MEMORY_ONLY_SER,但将数据存储在堆外内存中。这需要启用堆外内存。

注意:如果是使用Python的话,存储的对象总是会用Pickle库去序列化对象,所以无论选择哪个存储级别都不重要。Python里只存的存储级别有MEMORY_ONLY, MEMORY_ONLY_2, MEMORY_AND_DISK, MEMORY_AND_DISK_2, DISK_ONLY, and DISK_ONLY_2。

Spark在shuffle操作期间(如reduceByKey)也会自动地去持久化中间数据,及时用于没有显示调用persist这样做是为了在shuffle期间节点失败时避免要重算整个输入数据。我们也建议用户可以持久化结果RDD如果计算后面要重用这个结果RDD的话。

Which Storage Level to Choose?(选取哪个存储级别)

Spark的存储级别就意味着在内存使用和CPU效率之间做trade-off(取舍)。建议通过以下步骤选取一个合适的:

  • 如果你的RDD对于默认存储级别很合适的话,就不用修改了。这是CPU最高效的选项,运行在这些RDD上的操作跑的尽可能快。
  • 如果不合适的话,试着使用MEMORY_ONLY_SER并且选择一个很快的serialization库使对象更加节省空间,但是仍然可以快速访问。
  • 除非在RDD上的计算是非常昂贵的,或者它们过滤了很庞大的数据,否则不要溢出数据到磁盘。否则重新计算一个分区的速度可能与从磁盘读取的速度是一样的。
  • 如果你想快速的故障恢复的话(例如使用Spark为来自web应用程序的请求提供服务),可以选择带有复制的存储级别。所有的存储级别都有完整的容错能力通过重算丢失的数据,但是带有复制的存储级别可以让你在RDD上继续运行任务而不用等着去重算分区数据。
Removing Data(清理数据)

Spark会自动监控每个节点的缓存使用情况然后使用LRU算法清理掉老的分区数据。如果想手动清理的话可以通过RDD.unpersist()

Shared Variables(共享变量)

正常情况下,当传递给Spark操作(如map, reduce)的函数在远程集群节点上运行时,它会基于函数里使用的所有变量的单独副本工作的。这些变量会复制到每台机器上,对远程机器上的变量的更新是不会传播到driver program节点的。在task之间支持通用的、读写共享的变量是效率很低的。但是Spark确实为两种常见的常见提供了两类共享变量:广播变量(broadcast variables)和累加器(accumulators)。

Broadcast Variables(广播变量)

广播变量允许程序员在每台机器上缓存一个只读变量而不是在task之间拷贝变量。例如,它们可以用来以一个有效的方式给每个节点一个大量输入数据的拷贝。Spark也尝试着用有效的广播算法去分发广播变量来减少连接交流成本。

Saprk的action算子的执行是通过一系列的stage,由分布式shuffle操作来分割( 具体stage如何划分请看另一篇博客)。Spark在每个stage内自动广播task所需要的公共数据。用这种方式广播的数据是以序列化的形式缓存然后在执行每个task之前会反序列化回去。这意味着,只有当跨多个stage的task需要相同的数据,或者以反序列化的形式缓存数据很重要时,显式地创建广播变量才有用。

广播变量可以通过SparkContext.broadcast(v)来创建。广播变量是对变量v的一个包装,它的值可以通过value方法来获取。如下所示:

scala> val bcVar = sc.broadcast(Array(1 to 5))
bcVar: org.apache.spark.broadcast.Broadcast[Array[scala.collection.immutable.Range.Inclusive]] = Broadcast(19)

scala> bcVar.value
res14: Array[scala.collection.immutable.Range.Inclusive] = Array(Range(1, 2, 3, 4, 5))

在广播变量被创建后,应该使用广播变量,而不是使用集群上任意函数的v的值,这样v就不会多次发送到节点上。另外,变量v在被广播后是不应该被修改的,以确保所有的节点获取广播变量同样的值(如变量拷贝到一个新的节点上)。

Accumulators(累加器)

累加器是通过符合交换律和结合律操作的用于“added”的变量,因此可以有效地支持并行操作。它们可以用来实现counters(如MapReduce中的程序计数器)或者求和。Spark本身支持数字类型的累加器,程序员可以添加对新类型的支持。

作为一个用户,你可以创建已命名或未命名的累加器。正如下面图片所示。一个命名的累加器(例子中的counter)将会展示在web UI。Spark在“Tasks”表中显示由task修改的每个累加器的值。
在这里插入图片描述
在UI界面追踪累加器对于理解运行的stage的过程是非常有用的(注意:这个暂时不支持python)

scala> val accum = sc.longAccumulator("My Accumulator")
accum: org.apache.spark.util.LongAccumulator = LongAccumulator(id: 475, name: Some(My Accumulator), value: 0)

scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum.add(x))

scala> accum.value
res16: Long = 10

可以通过调用SparkContext.longAccumulator()或者SparkContext.doubleAccumulator()来创建一个数字型的累加器用于累加Long类型或者Double类型的数据。在集群上运行的task可以通过使用add方法来累加它。然而,task是不能读累加器的值。只有driver program才能通过value方法获取到累加器的值

程序员可以通过继承AccumulatorV2来创建自定义类型的累加器。但是AccumulatorV2是抽象类,有几个抽象方法需要覆盖。
在这里插入图片描述
看下面的自定义累加器的例子

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

// Then, create an Accumulator of this type:
val myVectorAcc = new VectorAccumulatorV2
// Then, register it into spark context:
sc.register(myVectorAcc, "MyVectorAcc1")

注意,程序员定义自己的累加器时,结果类型可以和待添加的元素类型不一样。

对于仅仅在action算子操作的累加器的更新,Spark会保证每个task对累加器的更新只会执行一次,例如重启任务不会更新该值。在transformation算子操作里,用户需要意识到每个task对累加器的更新在task或者job stage重执行时可能会执行多次

累加器不会改变Spark的lazy evaluation模式。看下面代码,执行了map操作后累加器的值并不会更新。

scala> accum.value
res19: Long = 20

scala> sc.parallelize(Array(1, 2, 3, 4)).map(x=>{accum.add(x);x})
res20: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[36] at map at <console>:27

scala> accum.value
res21: Long = 20

Deploying to a Cluster(部署spark程序到集群)

如何提交代码给集群。将应用打包成jar(scala、Java)或者打包成zip包(python),然后通过脚本spark-submit提交给集群

Launching Spark jobs from Java / Scala(从Java/Scala启动Spark job)

org.apache.spark.launcher提供class用简单的Java API的方式以子进程的形式启动Spark job

Unit Testing(单元测试)

Spark对任何流行的单元测试框架都很友好。只需在您的测试中创建一个master URL设置为local的SparkContext,运行您的操作,然后调用SparkContext.stop()将其销毁。确保在finally块或测试框架的tearDown方法中停止SparkContext因为Spark不支持在同一个程序中同时运行两个SparkContext

参考网址

Spark官网 - - RDD编程指南

发布了19 篇原创文章 · 获赞 0 · 访问量 692

猜你喜欢

转载自blog.csdn.net/qq_23120963/article/details/104433290