Spark RDD用法

  • RDD简介
    • 并行化集合
    • 外部数据集
      • 读取文本文件
      • 读取其他数据格式
      • 存储RDD
  • RDD 操作
    • 向Spark传递函数
    • 理解闭包
      • 本地模式 vs 集群模式
      • 打印RDD元素
    • 使用键值对
    • 中间操作Transformations
      • map
      • filter
      • flatMap
      • mapPartitions
      • mapPartitionsWithIndex
      • sample
      • union
      • intersection
      • distinct
      • groupByKey
      • reduceByKey
      • aggregateByKey
      • sortByKey
      • join
      • cogroup
      • cartesian
      • pipe
      • coalesce
      • repartition
      • repartitionAndSortWithinPartitions
    • 结束操作Actions
      • reduce
      • collect
      • count
      • first
      • take
      • takeSample
      • takeOrdered
      • saveAsTextFile
      • countByKey
      • foreach
    • Shuffle操作
      • 背景
      • 性能影响
  • RDD持久化
      • 持久化级别
      • 如何选择持久化级别
      • 移除数据

RDD简介

Spark的核心概念是RDD(Resilient Distributed Dataset),是一个可并行操作、提供容错机制的数据集合,有两种方式来创建RDD:

  • 在驱动程序中的并行化一个现有集合
  • 引用外部存储系统的数据集

并行化集合

并行集合可以通过SparkContext.parallelize方法来创建,该方法接收一个集合作为参数,并将集合中的元素转化成可并行操作的分布式数据集。

scala> val data = Array(1,2,3,4,5)
data: Array[Int] = Array(1, 2, 3, 4, 5)

scala> val distData = sc.parallelize(data)
distData: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :26

一旦创建完成,这个分布式数据集就可以被并行操作了,例如,可以进行如下的操作

scala> distData.reduce(_ + _)
res0: Int = 15

对于SparkContext.parallelize方法来说,还有一个重要参数就是分区数(partitions)——表示一个数据集分割的区域数目,Spark会在集群上为每一个分区运行一个任务。通常,你需要为集群中的每个CPU分配2-4个分区。默认情况下,Spark会根据你的集群状况自动地设置分区的数目。不过,你也可以手动设置分区数,通过调用sc.parallelize(data,10)方法来实现。

外部数据集

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

读取文本文件

文本文件的RDDs可以通过SparkContext.textFile方法创建,该方法接收一个文件URI(本地机器上的文件路径、hdfs://、s3n://等),读取的内容为行的集合

scala> val distFile = sc.textFile("README.md")
distFile: org.apache.spark.rdd.RDD[String] = README.md MapPartitionsRDD[2] at textFile at :24

一旦创建完成,distFile就能够执行相关的数据集操作。例如,可以计算整个文件的长度

scala> distFile.map(_.length).reduce(_ + _)
res2: Int = 3706

使用Spark读取文件需要注意:

  1. 如果读取本地文件系统上的文件,那么文件必须能在Worker节点上用相同路径访问到。因此,要么复制文件到所有的workers,要么使用网络的方式共享文件系统。
  2. 所有Spark支持文件的方法,包括textFIle,都能很好的支持文件目录压缩过的文件和通配符。例如,你可以使用textFile("/my/foo/")textFile("/my/foo/*.txt")textFile("/my/foo/*.gz")
  3. textFile方法可以接受第二个可选择参数,用于控制文件的分区数。默认情况下,Spark为每一个文件块创建一个分区(在HDFS中,默认块大小是128MB),也就是说有该文件有几个block,那就有几个分区。你也可以传递一个更大的值来创建一个更大的分区,但是该值不能比块数小

读取其他数据格式

  1. 大量小文本文件

    通过SparkContext.wholeTextFiles能够读取包含多个小文本文件的目录,并且返回(filename,content)Tuple2类型数组格式的数据。

    scala> val wholeTextFiles = sc.wholeTextFiles("./data/")
    wholeTextFiles: org.apache.spark.rdd.RDD[(String, String)] = ./data/ MapPartitionsRDD[5] at wholeTextFiles at :24
    
    scala> wholeTextFiles.foreach{
        | case (name,content) => println(name)
        | }
    file:/opt/spark/data/1.md
    file:/opt/spark/data/5.md
    file:/opt/spark/data/2.md
    file:/opt/spark/data/6.md
    file:/opt/spark/data/3.md
    file:/opt/spark/data/4.md

    分区是由数据的本地性决定,在某些情况下可能会导致分区数太小。为了解决此问题,wholeTextFiles方法提供了一个可选参数用于设置最小分区数。

  2. SequenceFiles

    对于SequenceFiles,可以使用SparkContext.sequenceFile[K,V]来读取,方法中K和V分别代表了文件中键和值的类型。这些类型对应的类必须是Hadoop的Writable接口的子类,就像IntWritable或Text。

  3. Hadoop InputFormat格式文件

    对于其他Hadoop InputFormat格式的文件,你可以使用SparkContext.hadoopRDD方法,该方法接收一个任意的JobConf对象、输入格式类型,键类型和值类型。

存储RDD

RDD.saveAsObjectFileSparkContext.objectFile支持以包含序列化Java对象的简单格式保存RDD。虽然效率没有Avro高,但它提供了一种简单的方式来保存任何RDD。

RDD 操作

RDDs支持两种类型的操作:中间操作和结束操作

  • 中间操作(transformations):基于一个已有的数据集转换成一个新的数据集。

    map就是一个中间操作,把一个函数应用到数据集中的所有元素,并创建一个新的RDD。

  • 结束操作(actions):在一个数据集上进行计算后向驱动程序返回一个值。

    reduce就是一个结束操作,它能够使用一些函数聚合RDD中的所有元素,并将最终的结果返回给驱动程序。

所有的中间操作都是惰性执行的,并不会立即计算结果。它们只是记录了转换需要哪些数据集,以及对数据集进行哪些操作。只有当结束操作触发时,才会真正对数据集执行这些转换操作,并将最终的结果返回给驱动程序。

默认情况下,每次执行结束操作(actions),对RDDS的转换操作都会重复执行。因此,可以对RDD调用persistcache方法,将RDD保存在内存中,这样Spark在下次访问时更加快速。另外,也可以将RDD持久化在磁盘中,或者在多个节点中以副本方式保存。

向Spark传递函数

在集群上,Spark依赖于驱动程序给它传递的函数来运行,对于函数的传递有以下几种方式(推荐前两个方式):

  1. 匿名函数语法,可在比较短的代码中使用;

    myRdd.map(_.length).reduce(_+_)
  2. 全局单例对象中的静态方法,则会将MyFunctions.func1传递过去

    object MyFunctions{
       def func1(s: String): String = {...}
    }
    myRdd.map(MyFunctions.func1)
  3. 普通类实例对象的方法,将会发送整个对象

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

    如果创建了一个MyClass的实例,并且调用了doStuff方法。由于在doStuff方法内部的map引用了MyClass实例的func1方法,因此整个MyClass对象都需要发送给集群(doStuff方法实际上等价于rdd.map(x => this.func1(x)))。

  4. 外部对象的属性,将会发送整个对象

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

    上段代码中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操作的范围之外修改变量可能会频繁的带来错误。可以考虑下面的求RDD元素之和的例子,其返回的结果是否相同,取决于是否在同一个JVM中执行。

var counter = 0
var rdd = sc.parallelize(Array(1,2,3,4,5))

// Wrong: Don't do this
rdd.foreach(x => counter +=x )
println("Counter value: " + counter)

本地模式 vs 集群模式

在集群模式中,为了执行作业,Spark会将RDD操作的处理分解为多个任务,每个任务都交给一个执行器去执行。在执行之前,Spark将会计算任务的闭包(这些闭包就是变量和方法,它们必须对整个RDD上执行计算的executor可见),并将闭包序列化后发送给每个执行器。这些闭包中的变量都是原始数据(counter)的副本。而在驱动程序节点内存中的原始数据(counter)对执行器程序所在的节点而言是不可见的,执行器只会对闭包中的数据副本进行操作。因此,驱动器节点中counter的最终值总是为0。

在本地模式中,某些情况下foreach函数会和驱动器在同一个JVM中运行,这时都会引用相同原始的counter,而counter肯定会被更新。

因此,为了避免以上错误情况发生,则对于累加器应该使用Accumulator。Spark中的累加器专门用于解决在集群中跨工作者节点计算时的变量安全更新问题。累加器的详细使用见下文的累加器。

打印RDD元素

有时候需要使用rdd.foreach(println)方法或者rdd.map(println)打印出RDD的元素。在单个机器中这是没有问题的。然而在集群模式下,打印的stdout会在执行器节点下执行,而不是驱动器节点。因此,驱动器节点压根儿不会显示打印的结果。

为了能够在驱动器节点看到打印的数据,可以使用rdd.collect().foreach(println)方法,将全部的RDD数据返回到单个驱动器节点然后打印,不过这样有可能造成OOM。因此,如果你仅需要打印部分RDD数据,安全的方法是rdd.take(100).foreach(println)

使用键值对

虽然大量的Spark操作适用于包含任何类型对象的RDD,但一些特殊操作只能在键值对RDD上使用。最常见的是分布式Shuffle操作,例如按键分组或聚合元素等操作(见下文中的Shuffle)。

在Scala中,这些操作可以在包含Tuple2对象的RDD中自动使用,这些键值对操作都定义在PairRDDFunctions类中,该类能够自动处理Tuple类型的RDD。

如果在键值对操作中使用用户自定义的对象作为键,那么必须确定该类覆写了equals方法和hashCode方法。

中间操作(Transformations)

map

  1. 用途

    返回每个元素经过传入的函数func处理后形成的新分布式数据集

  2. 使用示例

    map[U](f: (T) ⇒ U)(implicit arg0: ClassTag[U]): RDD[U]

    scala> val distData = sc.parallelize(Array(1,2,3,4,5))
    distData: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[6] at parallelize at :26
    
    scala> val newDistData = distData.map(_*10)
    newDistData: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[7] at map at :28
    
    scala> newDistData.foreach(println)
    10
    20
    30
    40
    50

filter

  1. 用途

    过滤出经传入func函数计算后返回true的数据,返回的结果对原数据没有影响,所以必须赋给一个变量存储。

  2. 使用示例

    filter(f: (T) ⇒ Boolean): RDD[T]

    scala> val distData = sc.parallelize(1 to 10)
    distData: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[9] at parallelize at :24
    
    scala> distData.filter(_ % 2 ==0 ).foreach(println)
    2
    4
    6
    8
    10

flatMap

  1. 用途

    flatMap函数是两个操作的集合——正是“先映射后扁平化”:

    • 同map函数一样:对每一条输入进行指定的操作,然后为每一条输入返回一个对象
    • 最后将所有对象合并为一个对象
  2. 使用示例

    flatMap[U](f: (T) ⇒ TraversableOnce[U])(implicit arg0: ClassTag[U]): RDD[U]

    scala> val list = Array("hello scala","hello spark","haha !")
    list: Array[String] = Array(hello scala, hello spark, haha !)
    
    scala> val distList = sc.parallelize(list)
    distList: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[13] at parallelize at :26
    
    scala> distList.flatMap(_.split(" ")).foreach(println)
    hello
    scala
    hello
    spark
    haha
    !
    // 对同一份数据,使用map进行操作
    scala> distList.map(_.split(" ")).foreach(println) // 返回的是三个对象
    [Ljava.lang.String;@229bb432
    [Ljava.lang.String;@15d247dd
    [Ljava.lang.String;@559c9880
    
    scala> distList.map(_.split(" ")).foreach(_.foreach(println))
    hello
    scala
    hello
    spark
    haha
    !

mapPartitions

  1. 用途

    与map方法类似,map是对rdd中的每一个元素进行操作,而Iterator%5bU%5d,preservesPartitioning:Boolean%29%28implicitevidence%246:scala.reflect.ClassTag%5bU%5d%29:org.apache.spark.rdd.RDD%5bU%5d”>mapPartitions则是对rdd中的每个分区的迭代器进行操作。

  2. 使用示例

    mapPartitions[U](f: (Iterator[T]) ⇒ Iterator[U], preservesPartitioning: Boolean = false)(implicit arg0: ClassTag[U]): RDD[U]

    例如,实现将每个数字变成原来的2倍的功能,map方式与mapPartitions实现方式对比如下:

    • 使用map方式实现

      scala> val list = sc.parallelize(1 to 9 ,3)
      list: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[27] at parallelize at :25
      
      scala> list.map(a => (a,a*2)).foreach(println)
      (1,2)
      (2,4)
      (3,6)
      (4,8)
      (5,10)
      (6,12)
      (7,14)
      (8,16)
      (9,18)
    • 使用mapPartitions方法实现

      scala> val list = sc.parallelize(1 to 9 ,3)
      list: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[22] at parallelize at :25
      
      scala> def doubleFunc(iter:Iterator[Int]):Iterator[(Int,Int)] = {
        | val res = scala.collection.mutable.ListBuffer[(Int,Int)]()
        | while(iter.hasNext){
        | val cur = iter.next
        | res += ((cur,cur*2))
        | }
        | res.iterator
        | }
      doubleFunc: (iter: Iterator[Int])Iterator[(Int, Int)]
      
      scala> list.mapPartitions(doubleFunc).foreach(println)
      (1,2)
      (2,4)
      (3,6)
      
      (7,14) (8,16) (9,18)
      (4,8) (5,10) (6,12)

    通过对比可知,如果在map过程中需要频繁创建额外的对象(例如将rdd中的数据通过jdbc写入数据库,map需要为每个元素创建一个链接,而mapPartition为每个partition创建一个链接),可见mapPartitions效率比map高的多。

mapPartitionsWithIndex

  1. 用途

    和mapPartitions类似,但是提供的函数必须能够接受两个参数,第一个参数代表分区号,第二个参数代表每个分区数据的迭代器。

  2. 使用示例

    mapPartitionsWithIndex[U](f: (Int, Iterator[T]) ⇒ Iterator[U], preservesPartitioning: Boolean = false)(implicit arg0: ClassTag[U]): RDD[U]

    例如,实现将每个数字变成原来的2倍的功能,使用方式如下:

    scala> val list = sc.parallelize(1 to 9 ,3)
    list: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[29] at parallelize at :25
    
    scala> def doubleFunc(index:Int, iter:Iterator[Int]):Iterator[(Int,Int,Int)] = {
        | val res = ListBuffer[(Int,Int,Int)]()
        | while(iter.hasNext){
        | val cur = iter.next
        | res += ((index,cur,cur*2))
        | }
        | res.iterator
        | }
    doubleFunc: (index: Int, iter: Iterator[Int])Iterator[(Int, Int, Int)]
    
    scala> list.mapPartitionsWithIndex(doubleFunc).foreach(println)
    (0,1,2)
    (0,2,4)
    (0,3,6)
    (1,4,8)
    (1,5,10)
    (1,6,12)
    (2,7,14)
    (2,8,16)
    (2,9,18)

sample

  1. 用途

    用于对数据进行采样

  2. 使用示例

    sample(withReplacement: Boolean, fraction: Double, seed: Long = [Utils.random.nextLong]: RDD[T]

    scala> val distData = sc.parallelize(1 to 10)
    distData: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[43] at parallelize at :25
    
    scala> distData.sample(false ,0.5).foreach(print) // 采集50%的数据,不进行替换
    12349
    scala> distData.sample(true ,0.5).foreach(print) // 
    2568
    scala> distData.sample(true ,1).foreach(print) // 采集全部数据,并替换
    1223447910

union

  1. 用途

    两个RDD的并集

  2. 使用示例

    union(other: RDD[T]): RDD[T]

    scala> val data1 = sc.parallelize(1 to 10 by 2)
    data1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[50] at parallelize at :25
    
    scala> val data2 = sc.parallelize(2 to 10 by 2)
    data2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[51] at parallelize at :25
    
    scala> data1.union(data2).foreach(println)
    1
    3
    5
    2
    6
    8
    10
    4
    7
    9

intersection

  1. 用途

    两个RDD的交集

  2. 使用示例

    intersection(other: RDD[T]): RDD[T]

    scala> val data1 = sc.parallelize(1 to 10)
    data1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[53] at parallelize at :25
    
    scala> val data2 = sc.parallelize(5 to 15)
    data2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[54] at parallelize at :25
    
    scala> data1.intersection(data2,3).foreach(println)
    8
    5
    6
    9
    7
    10
    
    scala> data1.intersection(data2).foreach(println)
    6
    10
    9
    5
    7
    8

distinct

  1. 用途

    返回一个原数据集中不包含重复元素的新数据集

  2. 使用示例

    distinct(): RDD[T]

    scala> val data = sc.parallelize(Array(1,3,4,2,1,3,2,2,2))
    data: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[67] at parallelize at :25
    
    scala> data.distinct().foreach(print)
    4123

groupByKey

  1. 用途

    将RDD中每个键的值分组为单个序列。每个组中的元素的排序不能保证,并且每次生成的RDD结果都可能会有所不同。

  2. 方法示例

    • groupByKey(): RDD[(K, Iterable[V])]

      默认使用现有的分区/并行级别对生成的RDD进行哈希分区。

      scala> val data = sc.parallelize(Array(("cwc",98),("cwc",93),("lm",43),("lm",32),("cwc",44),("lm",98)))
      data: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[71] at parallelize at :25
      
      scala> data.groupByKey().foreach(println)
      (cwc,CompactBuffer(98, 93, 44))
      (lm,CompactBuffer(43, 32, 98))
    • groupByKey(numPartitions: Int): RDD[(K, Iterable[V])]

      • numPartitions:输出将使用numPartition进行散列分区。
      scala> data.groupByKey(2).foreach(println)
      (cwc,CompactBuffer(98, 93, 44))
      (lm,CompactBuffer(43, 32, 98))
    • groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])]

      • partitioner:使用给定的分区器对输出结果进行分区
  3. 使用注意

    • 该操作的代价非常大,如果您正在进行分组,以便在每个键上执行聚合(如总和或平均值)操作,那么使用PairRDDFunctions.reduceByKey或者PairRDDFunctions.aggregateByKey将会有更好的性能。
    • 按照当前实现,groupByKey必须有足够空间在内存中保存任何键的所有键值对,如果某个键有太多的值,那么会发生OutOfMemoryError。

reduceByKey

  1. 用途

    使用给定的函数对每个键的值进行聚合操作

  2. 使用示例

    • reduceByKey(func: (V, V) ⇒ V): RDD[(K, V)]

      输出根据现有的分区/并行级别进行哈希分区

      scala> val data = sc.parallelize(Array(("cwc",98),("cwc",93),("lm",43),("lm",32),("cwc",44),("lm",98)))
      data: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[71] at parallelize at :25
      
      scala> data.reduceByKey((sum,score) => sum+score).foreach(println)
      (lm,173)
      (cwc,235)
    • reduceByKey(func: (V, V) ⇒ V, numPartitions: Int): RDD[(K, V)]

      • numPartitions:输出将使用numPartition进行散列分区。
    • reduceByKey(partitioner: Partitioner, func: (V, V) ⇒ V): RDD[(K, V)]

      • partitioner:使用给定的分区器对输出结果进行分区

aggregateByKey

  1. 用途

    使用给定的组合函数和中性“零值”来聚合每个相同键的值。

    该函数返回了一个与RDD中的值类型V不同的类型U。

    为了避免内存分配,这两个功能都允许修改并返回其第一个参数,而不是创建一个新的U

  2. 使用示例

    • aggregateByKey[U](zeroValue: U)(seqOp: (U, V) ⇒ U, combOp: (U, U) ⇒ U)(implicit arg0: ClassTag[U]): RDD[(K, U)]

      • seqOp:在同一分区下,利用seqOp对相同的键K对应的值V进行操作,并返回的类型为U的值(与给定的zeroValue类型相同)。
      • combOp:在不同分区之间,利用combOp对相同K对应的V进行合并。
      scala> val data = List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8))
      data: List[(Int, Int)] = List((1,3), (1,2), (1,4), (2,3), (3,6), (3,8))
      
      scala> sc.parallelize(data).aggregateByKey(0.0)(math.max(_,_), _+_).foreach(println)
      (1,7.0)
      (2,3.0)
      (3,8.0)
      
      scala> sc.parallelize(data,1).aggregateByKey(0)(math.max(_,_), _+_).foreach(println)
      (1,4)
      (3,8)
      (2,3)
      
      // 分区:{1: [(1,3),(1,2),(1,4)], 2: [(2,3),(3,6),(3,8)]
      scala> sc.parallelize(data,2).aggregateByKey(0)(math.max(_,_), _+_).foreach(println)
      (1,4)
      (3,8)
      (2,3)
      
      // 分区:{1: [(1,3),(1,2)], 2: [(1,4),(2,3)] 3: [(3,6),(3,8)]}
      scala> sc.parallelize(data,3).aggregateByKey(0)(math.max(_,_), _+_).foreach(println)
      (1,7) 
      (2,3)
      (3,8)
    • aggregateByKey[U](zeroValue: U, numPartitions: Int)(seqOp: (U, V) ⇒ U, combOp: (U, U) ⇒ U)(implicit arg0: ClassTag[U]): RDD[(K, U)]

      • numPartitions:输出将使用numPartition进行散列分区。

sortByKey

  1. 用途

    如果一组(K,V) 键值对中的键值实现了Ordered,那么在这组键值对上调用sortByKey,会返回一组按键值升序排序或降序排序的(K,V) 键值对。升序还是降序由布尔参数ascending决定。

  2. 使用示例

    sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length): RDD[(K, V)]

    • ascending:是否升序
    • numPartitions:分区数
    scala> val data = List((1,4),(2,3),(3,6),(1,5),(0,4),(4,2),(3,8))
    data: List[(Int, Int)] = List((1,4), (2,3), (3,6), (1,5), (0,4), (4,2), (3,8))
    
    scala> sc.parallelize(data).sortByKey(true,1).foreach(println)
    (0,4)
    (1,4)
    (1,5)
    (2,3)
    (3,6)
    (3,8)
    (4,2)
    
    scala> sc.parallelize(data).sortByKey(true,2).foreach(println)
    (0,4)
    (1,4)
    (1,5)
    (2,3)
    (3,6)
    (3,8)
    (4,2)
    
    scala> sc.parallelize(data).sortByKey(true,4).foreach(println)
    (2,3)
    
    (0,4) (1,4)
    (1,5) (4,2)
    (3,6) (3,8) scala> sc.parallelize(data).sortByKey(false,4).foreach(println) (1,4) (1,5) (0,4)
    (2,3)
    (3,6) (3,8)
    (4,2) scala> sc.parallelize(data).sortByKey(false,2).foreach(println) (2,3) (1,4) (1,5) (0,4)
    (4,2) (3,6) (3,8)

join

  1. 用途

    当对类型(K,V)和(K,W)的数据集进行调用时,返回每个键的所有元素对数据集(K,(V,W))对。

    左连接与右连接的区别

  2. 使用示例

    join[W](other: [RDD][(K, W)], numPartitions: Int): RDD[(K, (V, W))]

    scala> val rdd1 = sc.parallelize(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
    rdd1: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[170] at parallelize at :25
    
    scala> val rdd2 = sc.parallelize(Array((2,"B"),(3,"C")))
    rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[171] at parallelize at :25
    
    // 内连接:左边和右边的数据只有存在才返回
    scala> rdd1.join(rdd2).foreach(println)
    (2,(b,B))
    (3,(c,C))
    
    // 左连接:以左边的对象为主,只有存在的才会返回;如果左连接对应的数据为空,则返回None。
    scala> rdd1.leftOuterJoin(rdd2).foreach(println)
    (2,(b,Some(B)))
    (3,(c,Some(C)))
    (1,(a,None))
    (4,(d,None))
    
    scala> rdd2.leftOuterJoin(rdd1).foreach(println)
    (2,(B,Some(b)))
    (3,(C,Some(c)))
    
    // 右连接:以右边的对象为主,只有存在的才会返回;如果右连接对应的数据为空,则返回None。
    scala> rdd1.rightOuterJoin(rdd2).foreach(println)
    (2,(Some(b),B))
    (3,(Some(c),C))
    
    scala> rdd2.rightOuterJoin(rdd1).foreach(println)
    (3,(Some(C),c))
    (1,(None,a))
    (4,(None,d))
    (2,(Some(B),b))
    
    // 全外连接:
    scala> rdd1.fullOuterJoin(rdd2).foreach(println)
    (1,(Some(a),None))
    (2,(Some(b),Some(B)))
    (3,(Some(c),Some(C)))
    (4,(Some(d),None))
    
    scala> rdd2.fullOuterJoin(rdd1).foreach(println)
    (4,(None,Some(d)))
    (1,(None,Some(a)))
    (2,(Some(B),Some(b)))
    (3,(Some(C),Some(c)))

cogroup

  1. 用途

    类似于fullOuterJoin,只是返回的数据类型不同

  2. 使用示例

    cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))]

    scala> val rdd1 = sc.parallelize(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
    rdd1: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[193] at parallelize at :25
    
    scala> val rdd2 = sc.parallelize(Array((2,"B"),(3,"C")))
    rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[194] at parallelize at :25
    
    scala> rdd1.cogroup(rdd2).foreach(println)
    (3,(CompactBuffer(c),CompactBuffer(C)))
    (2,(CompactBuffer(b),CompactBuffer(B)))
    (1,(CompactBuffer(a),CompactBuffer()))
    (4,(CompactBuffer(d),CompactBuffer()))
    
    scala> rdd2.cogroup(rdd1).foreach(println)
    (1,(CompactBuffer(),CompactBuffer(a)))
    (2,(CompactBuffer(B),CompactBuffer(b)))
    (3,(CompactBuffer(C),CompactBuffer(c)))
    (4,(CompactBuffer(),CompactBuffer(d)))

cartesian

  1. 用途

    对两个数据集进行笛卡尔积计算

  2. 使用示例

    cartesian[U](other: RDD[U])(implicit arg0: ClassTag[U]): RDD[(T, U)]

    scala> val rdd1 = sc.parallelize(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
    rdd1: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[193] at parallelize at :25
    
    scala> val rdd2 = sc.parallelize(Array((2,"B"),(3,"C")))
    rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[194] at parallelize at :25
    
    scala> rdd1.cartesian(rdd2).foreach(println)
    ((1,a),(2,B))
    ((2,b),(3,C))
    ((3,c),(2,B))
    ((3,c),(3,C))
    ((1,a),(3,C))
    ((4,d),(2,B))
    ((4,d),(3,C))
    ((2,b),(2,B))

pipe

  1. 用途

    通过shell命令管理RDD的每个分区

  2. 使用示例

    pipe(command: String): RDD[String]

    • 创建一个分区

      
      #!/bin/bash
      
      
      # 文件名为echo.sh
      
      echo "Running shell script";
      RESULT="";#变量两端不能直接接空格符
      while read LINE; do
      RESULT=${RESULT}" "${LINE}
      done
      
      echo ${RESULT} > out123.txt
      scala> val data = Array("my","name","is","cwc")
      data: Array[String] = Array(my, name, is, cwc)
      
      // 创建一个分区
      scala> val rdd = sc.parallelize(data,1)
      rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[0] at parallelize at :26
      
      scala> val pipeRdd = rdd.pipe("./echo.sh")
      pipeRdd: org.apache.spark.rdd.RDD[String] = PipedRDD[1] at pipe at :28
      
      scala> pipeRdd.collect.foreach(println)
      Running shell script
      
      scala> pipeRdd.collect.foreach(println)
      Running shell script
      
      scala> val out123Rdd = sc.textFile("./out123.txt")
      out123Rdd: org.apache.spark.rdd.RDD[String] = ./out123.txt MapPartitionsRDD[6] at textFile at :24
      
      scala> out123Rdd.foreach(println) 
      my name is cwc
    • 创建两个分区

      
      #!/bin/bash
      
      echo "Running shell script";
      RESULT="";#变量两端不能直接接空格符
      while read LINE; do
      RESULT=${RESULT}" "${LINE}
      done
      
      
      # 对同一文件内容进行追加,否则后面的stdout会将前面的内容覆盖
      
      echo ${RESULT} >> out123.txt 
      scala> val data = Array("my","name","is","cwc")
      data: Array[String] = Array(my, name, is, cwc)
      
      scala> val dataRdd = sc.parallelize(data,2)
      dataRdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[9] at parallelize at :26
      
      scala> val pipeRdd = dataRdd.pipe("./echo.sh")
      pipeRdd: org.apache.spark.rdd.RDD[String] = PipedRDD[10] at pipe at :28
      
      scala> pipeRdd.collect.foreach(println) // 两个分区运行了该脚本
      Running shell script
      Running shell script
      
      scala> val out123RDD = sc.textFile("./out123.txt")
      out123RDD: org.apache.spark.rdd.RDD[String] = ./out123.txt MapPartitionsRDD[12] at textFile at :24
      
      scala> out123RDD.foreach(println)
      is cwc // 分区1输出
      my name // 分区2输出

coalesce

  1. 用途

    当把一个大型数据集过滤后,可以调用此方法将分区数减小到numPartitions,这样会更加有效的进行操作。

  2. 使用示例

    coalesce(numPartitions: Int, shuffle: Boolean = false, partitionCoalescer: Option[PartitionCoalescer] = Option.empty)(implicit ord: Ordering[T] = null): RDD[T]

    scala> val largeDatasetRdds = sc.parallelize(1 to 10000,10)
    largeDatasetRdds: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[25] at parallelize at :24
    
    scala> val largeDatasetAfterFilterRdds = largeDatasetRdds.filter(_%5==0)
    largeDatasetAfterFilterRdds: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[26] at filter at :26
    
    scala> val largeDatasetAfterFilterWithIndexRdds = largeDatasetAfterFilterRdds.mapPartitionsWithIndex((index,iter) => iter.toList.map((index,_)).iterator)
    largeDatasetAfterFilterWithIndexRdds: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[27] at mapPartitionsWithIndex at :28
    
    scala> largeDatasetAfterFilterWithIndexRdds.aggregateByKey(0)((counter,value) => counter+1, _+_).foreach(println)
    (2,200)
    (3,200)
    (4,200)
    (0,200)
    (1,200)
    (5,200)
    (6,200)
    (7,200)
    (8,200)
    (9,200)
    
    // 数据过滤后降低分区数
    scala> val largeDatasetAfterCoalesceRdds= largeDatasetAfterFilterRdds.coalesce(5)
    largeDatasetAfterCoalesceRdds: org.apache.spark.rdd.RDD[Int] = CoalescedRDD[29] at coalesce at :28
    
    scala> val largeDatasetAfterCoalesceWithIndexRdds = largeDatasetAfterCoalesceRdds.mapPartitionsWithIndex((index,iter) => iter.toList.map((index,_)).iterator)
    largeDatasetAfterCoalesceWithIndexRdds: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[30] at mapPartitionsWithIndex at :30
    
    scala> largeDatasetAfterCoalesceWithIndexRdds.aggregateByKey(0)((counter,value) => counter+1, _+_).foreach(println)
    (0,400)
    (1,400)
    (2,400)
    (3,400)
    (4,400)
    
    // 对于小数据集可以是由countByKey
    scala> largeDatasetAfterCoalesceWithIndexRdds.countByKey()
    res17: scala.collection.Map[Int,Long] = Map(0 -> 400, 1 -> 400, 2 -> 400, 3 -> 400, 4 -> 400)

repartition

  1. 用途

    Shuffle当前RDD中的数据,并重新分配到到numPartition个分区,这期间会通过网络来传输所有数据。

  2. 使用示例

    repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]

  3. 使用注意

    如果尝试减少此RDD中的分区数,请考虑使用coalesce,这可以避免执行shuffle。

repartitionAndSortWithinPartitions

  1. 用途

    根据给定的分区器对RDD进行重新分区,并对每个分区的数据按键值进行排序。

  2. 使用示例

    repartitionAndSortWithinPartitions(partitioner: Partitioner): RDD[(K, V)]

结束操作(Actions)

reduce

  1. 用途

    通过给定的函数来聚合RDD中的元素

  2. 使用示例

    reduce(f: (T, T) ⇒ T): T

    scala> val rdd = sc.parallelize(Array(1,2,3,4,5,6))
    rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[34] at parallelize at :24
    
    scala> rdd.reduce(_+_)
    res18: Int = 21

collect

  1. 用途

    在驱动程序中将数据集的所有元素作为数组返回。该操作适用于经过过滤或其他操作后足够小的数据集。

  2. 使用示例

    • collect(): Array[T]

      scala> val rdd = sc.parallelize(Array(1,2,3,4,5,6))
      rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[35] at parallelize at :24
      
      scala> rdd.filter(_%2==0).collect
      res19: Array[Int] = Array(2, 4, 6)
    • collect[U](f: PartialFunction[T, U])(implicit arg0: ClassTag[U]): RDD[U]

      • f:一个PartialFunction[A, B]类型的偏函数是一元函数,其域并不需要包括类型A*的所有值,isDefinedAt函数允许你动态地测试值是否在函数的有效域中。
      scala> val data = Array(2,43,1,4,2,0,22,76)
      data: Array[Int] = Array(2, 43, 1, 4, 2, 0, 22, 76)
      
      scala> val dataRdd = sc.parallelize(data)
      dataRdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[44] at parallelize at :26
      
      // 定义偏函数
      scala> val convert1To6 = new PartialFunction[Int,String] with Serializable {
        | val nums = Array("one", "two", "three", "four", "five")
        | override def isDefinedAt(i: Int): Boolean = i > 0 && i < 6
        | override def apply(v1: Int): String = nums(v1 - 1)
        | }
      convert1To6: PartialFunction[Int,String] with Serializable{val nums: Array[String]} = 
      
      scala> val convertedRdd = dataRdd.collect(convert1To6)
      17/09/09 01:37:40 WARN ClosureCleaner: Expected a closure; got $line76.$read$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$anon$1
           17/09/09 01:37:40 WARN ClosureCleaner: Expected a closure; got $line76.$read$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$anon$1
      convertedRdd: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[46] at collect at :32
      
      scala> convertedRdd.foreach(println)
      two
      one
      four
      two

count

  1. 用途

    返回数据集中元素的个数

  2. 使用示例

    count(): Long

    scala> val data = Array(2,43,1,4,2,0,22,76)
    data: Array[Int] = Array(2, 43, 1, 4, 2, 0, 22, 76)
    
    scala> sc.parallelize(data).count
    res25: Long = 8

first

  1. 用途

    返回数据集中的第一个元素,类似于take(1)

  2. 使用示例

    first(): T

    scala> val data = Array(2,43,1,4,2,0,22,76)
    data: Array[Int] = Array(2, 43, 1, 4, 2, 0, 22, 76)
    
    scala> sc.parallelize(data).first
    res26: Int = 2

take

  1. 用途

    以数组形式返回数据集中前n个元素

  2. 使用示例

    take(num: Int): Array[T]

    scala> val data = Array(2,43,1,4,2,0,22,76)
    data: Array[Int] = Array(2, 43, 1, 4, 2, 0, 22, 76)
    
    scala> sc.parallelize(data).take(3)
    res27: Array[Int] = Array(2, 43, 1)

takeSample

  1. 用途

    以数组形式返回固定数量的RDD字数据集

  2. 使用示例

    takeSample(withReplacement: Boolean, num: Int, seed: Long = Utils.random.nextLong): Array[T]

    • withReplacement:是否替换数据
    • num:样本数量
    • seed:种子随机数发生器
    scala> val data = Array(2,43,1,4,2,0,22,76)
    data: Array[Int] = Array(2, 43, 1, 4, 2, 0, 22, 76)
    
    scala> val dataRdd = sc.parallelize(data)
    dataRdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[50] at parallelize at :26
    
    scala> dataRdd.takeSample(true,2)
    res28: Array[Int] = Array(4, 2)
    
    scala> dataRdd.takeSample(false,4)
    res29: Array[Int] = Array(43, 1, 2, 76)
    
    scala> dataRdd.takeSample(false,8)
    res30: Array[Int] = Array(1, 4, 2, 43, 76, 2, 22, 0)
    
    scala> dataRdd.takeSample(true,8)
    res31: Array[Int] = Array(76, 43, 4, 22, 4, 22, 4, 1)

takeOrdered

  1. 用途

    以自然顺序或用户定义的比较器计算的顺序后,返回RDD中前n个元素

  2. 使用示例

    takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T]

    scala> sc.parallelize(Array(2,4,3,1,5,3)).takeOrdered(3)
    res32: Array[Int] = Array(1, 2, 3)
    
    scala> sc.parallelize(Array(1,2,3,4,5,6)).takeOrdered(3)
    res33: Array[Int] = Array(1, 2, 3)

saveAsTextFile

  1. 用途

    该方法可将RDD中的元素写到指定目录的文本文件中,该目录可以使本地文件系统、HDFS或任何其他Hadoop支持的文件系统给定的目录。对于每个元素,Spark会调用它的toString方法,转化为每个文件的行。

  2. 使用示例

    saveAsTextFile(path: String): Unit

    scala> val hello = Array("hello","spark")
    hello: Array[String] = Array(hello, spark)
    // 将helloRdd存储到saveAsTextFileTest目录下的文件中,并指定3个分区
    scala> sc.parallelize(hello,3).saveAsTextFile("./saveAsTextFileTest")
    [root@master saveAsTextFileTest]# ls
    part-00000  part-00001  part-00002  _SUCCESS
    [root@master saveAsTextFileTest]# cat part-00000 // 有一个分区数据为空
    [root@master saveAsTextFileTest]# cat part-00001
    hello
    [root@master saveAsTextFileTest]# cat part-00002
    spark

countByKey

  1. 用途

    统计每个键的元素数量,将结果以Map类型返回。

  2. 使用示例

    countByKey(): Map[K, Long]

    scala> val scoreRdd = sc.parallelize(Array(("cwc",98),("cwc",34),("david",56)))
    scoreRdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[65] at parallelize at :24
    
    scala> scoreRdd.countByKey()
    res37: scala.collection.Map[String,Long] = Map(david -> 1, cwc -> 2)
  3. 使用注意

    只有在预计生成的Map结果比较情况下才使用该方法,因此整个RDD数据都需要加载到内存中。

    如果需要处理较大的数据量,考虑使用rdd.aggregateByKey(0)((counter,value) => counter+1,_+_)方法,并返回RDD[T, Long] 类型的数据。

foreach

  1. 用途

    在数据集的每个元素上执行func函数,该方法通常用于副作用的操作,就像更新累加器或与外部存储系统交互

  2. 使用注意

    修改foreach之外的变量而不是累加器,有可能导致未定义的行为结果。具体可见上文中的闭包。

Shuffle操作

Spark内部的某些操作会触发称为Shuffle的事件,Shuffle是Spark**重新分配数据的机制**。这通常涉及到跨执行器之间与机器之间的数据复制操作,从而使得Shuffle变得复杂并且花费昂贵。

背景

为了了解Shuffle期间发生了什么,我们可以考虑一下reduceByKey操作(reduceByKey使用给定的函数对每个相同键的值进行聚合操作,并返回一个新的RDD)。该操作的挑战在于,并不是每个键对应的所有值都会在同一个分区内,或者在同一个机器上,但是他们必须位于同一位置才能计算结果。

在Spark中,数据通常情况下不会跨分区分布,而是针对特定操作分布在必要的位置。在计算期间,单个任务会在单个分区上执行。因此,为了单个reduceByKey的reduce任务能够执行,Spark需要执行一个all-to-all的操作来组织所有的数据。它必须读取所有的分区去查找所有键的所有值,然后跨分区将某个键的所有值汇集在一起,以便计算最终结果——这就称之为Shuffle

虽然新混洗后数据的每个分区中的元素集是确定的,分区本身的顺序也是如此,但是分区内的数据集的元素却没有顺序。因此,如果需要Shuffle后得到一个有序的数据集,则可以执行以下操作:

  • 利用mapPartitions,对分区内的元素使用sorted排序;
  • 利用repartitionAndSortWithinPartitions对分区内元素进行排序并重新分区;
  • 对RDD使用sortBy方法进行全局排序;

可能会导致Shuffle发生的操作包括:

  • repartition操作:repartition,coalesce;
  • ByKey操作:groupByKey,reduceByKey,除了countByKey;
  • join操作:cogroup、join;

性能影响

Shuffle过程非常耗费资源,因为它涉及到磁盘I/O、数据序列化和网络I/O。为了为Shuffle的过程组织数据,Spark会生成一组任务——包括一组用于组织数据的map任务和一组用于聚合数据的reduce任务(其中map任务和reduce任务的命名法来自于MapReduce,并不直接与Spark的mapreduce操作有关)。

在内部,单独的map任务结果都保存在内存中,直到内存无法容纳为止。接着,这些结果会根据目标分区进行排序并写入到单个文件中。在reduce端,reduce任务会从map端读取相关的排序块。

某些Shuffle操作会消耗大量的堆内存,因为records在传输前后,都会使用内存中的数据结构来存储。特别的,reduceByKeyaggregateByKey会在map端创建这些结构,类似*ByKey的操作会在reduce端生成。当内存无法装下这些数据后,将会从tables中溢出到磁盘,这会导致额外的磁盘IO开销,导致垃圾收集的次数增加。

Shuffle过程也会在磁盘中生成大量临时的文件。在Spark 1.3中,这些文件会被保存到相应的RDD不再被使用并且都已被垃圾回收。临时文件的存在保证了相关操作被重新执行后不需要再次重新生成文件,并且垃圾回收只可能在很长一段时间内才会发生。这同时也意味着长时间运行的Spark作业也会消耗大量的磁盘空间(临时存储目录由参数spark.local.dir指定)。

另外,可以通过各种配置参数来调整Shuffle的行为,具体可见Spark的配置

RDD持久化

Spark最重要的能力就是在操作中将数据集持久化到内存中。当持久化一个RDD时,包含该RDD分区的节点都会进行持久化,在未来将其重用于该数据集上的其他操作上,并且更加快速(通常超过10倍)。缓存也是迭代算法和快速交互使用的关键工具。

通常可以调用的persis()或cache()方法进行持久化操作,该操作会在第一次结束操作(action)执行时被计算,RDD则会被保存到节点的内存中。Spark的缓存是支持容错的——如果任何RDD的分区丢失,它将使用最初创建它的转换操作(transformations)自动重新计算。

持久化级别

此外,可以使用不同的存储级别来持久化RDD。例如,持久化到磁盘中、以序列化Java对象的方式持久化到内存中(节省空间)或者以多副本跨界点存储。这些需求都可以向persist()传递StorageLevel对象的方式来实现。另外,cache()方法默认存储级别(StorageLevel.MEMORY_ONLY)的简写。

存储级别 描述
MEMORY_ONLY 默认存储级别。RDD将会以Java对象的方式存储在JVM中, 如果RDD不适合内存,某些分区将不会被缓存,每次需要时都会重新计算。
MEMORY_AND_DISK RDD将会以Java对象的方式存储在JVM中, 如果RDD不适合内存,请存储不适合磁盘的分区,并在需要时从那里读取。
MEMORY_ONLY_SER 以序列化的方式存储RDD。这种方式更加的节省空间,尤其使用fast serializer时,但是读的时候会消耗更多的CPU。
MEMORY_AND_DISK_SER 与MEMORY_ONLY_SER类似,但将不适合内存的分区溢出到磁盘,而不是每次需要重新计算它们。
DISK_ONLY 将RDD分区全部存储到磁盘中
MEMORY_ONLY_2, MEMORY_AND_DISK_2 跟上面的一样,但是会在集群节点上放置存储两个副本
OFF_HEAP (experimental) 类似于MEMORY_ONLY_SER,但是是将数据存储在堆外内存,这需要启用堆外内存。

Spark也会自动持久化(用户没有主动调用persist)一些Shuffle过程中的中间数据,这样做是为了避免在Shuffle期间节点失败后重新计算整个输入。所以建议用户调用persist,如果需要重用RDD的结果。

如何选择持久化级别

Spark的存储级别旨在提供内存使用和CPU效率之间的不同权衡,因此建议通过以下过程来选择一个:

  1. 如果你的RDDs适合默认的存储级别,则不用管。这是CPU效率最高的选项,允许RDD上的操作尽可能快地运行;
  2. 如果不适合默认的存储级别,那么尝试使用MEMORY_ONLY_SER并选择一个快速的序列化库来使对象更加节省空间,但仍然能够快速访问。
  3. 尽量不要溢出数据到磁盘,除非对数据集计算的消耗非常大,或者对数据集进行了大规模的过滤。否则,重新计算分区就可能与从磁盘读取分区一样快了。
  4. 如果想要快速故障恢复,则使用副本存储级别。所有的存储级别都通过重新计算丢失的数据来提供完整的容错能力,但是副本存储级别让你可以继续在RDD上运行任务,而不用等待重新计算丢失的分区。

移除数据

Spark会自动监控每个节点的缓存使用,并使用LRU(least-recently-used)策略删除旧的分区数据。也可以使用RDD.unpersist()来手动移除数据。

你可能感兴趣的:(Spark)