畅聊Spark(二)RDD

                                                 Spark Core RDD

为什么要有RDD?

      Hadoop的MapReduce是一种基于数据集的工作模式,面向数据,这种工作模式一般是从存储上加载数据集,然后操作数据集,最后写入到物理存储设备中,数据更多面临的是一次性处理。

      MR的这种方式对数据领域两种常用的操作不是很高效。

      第一种是迭代式计算的算法,比如机器学习中的ALS,凸优化梯度下降等,这些都需要基于数据集或数据集的衍生数据反复查询反复操作,MR模式不太适合,即时多MR串行处理,性能和时间上也是一个问题,数据的共享依赖于磁盘。

      第二种是交互式数据挖掘,MR显然不擅长。

      MR中的迭代:

      Spark中的迭代:

      因此需要一个效率非常快,而且能够支持迭代计算和有效数据共享的模型,Spark应运而生。

 

什么是RDD?

      RDD是Spark的基石,是实现Spark数据处理的核心抽象,RDD是基于工作集的工作模式,更多的是面向工作流,但无论是MR还是RDD都具有类似的位置感知、容错和负载均衡等特性。

      RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,RDD代表一个不可变、可分区、里面的元素可并行计算的集合

      在Spark中,对数据的所有操作不外乎创建RDD、转化已有的RDD以及调用RDD操作进行求值,每个RDD都被分为多个分区去,这些分区运行在集群中的不同节点上,RDD可以包含Python、Java、Scala中的任意对象,甚至可以包含用户自定义对象。

      RDD具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。

      RDD允许用户在执行多个查询时,显示的将工作集缓存在内存中,后续的查询能够重用工作集,这极大的提升了查询速度。

      RDD支持两种操作,转换操作和行动操作

      RDD的转换操作是返回一个新的RDD的操作,比如map()、fliter……

      RDD的行动操作则是向驱动器程序返回结果或把结果写入到外部系统的操作,比如count()、first()……

      Spark采用惰性计算模式,RDD只有第一次在一个行动操作中用到时,才会真正的计算,Spark可以优化整个计算过程,默认情况下Spark的RDD会在行动操作时重新计算,如果有多个行动操作重用同一个RDD,可以使用RDD.persist让Spark把这个RDD缓存下来。

 

      小结:RDD就是一堆的函数,就是隐藏了细节的MapReduce,开发者不在需要关心map和reduce阶段,开发者只要了解RDD的函数是怎么样的,就可以了。

 

RDD的特性

      1.一组分片(A list of partitions)

           即数据集的基本组成单位,对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度,用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么则会采用默认值,默认值就是程序所分片的CPU Core的数目

      2.计算每个分区的函数(A function for computing each split)

            Spark中的RDD计算是以分片为单位的,每个RDD都会实现compute函数以达到这个目的,compute函数会对迭代器进行复合,不需要保存每次计算结果。

      3.RDD之间的依赖关系(A list of dependencies on other RDDs)

           RDD的每次转换操作都会生成一个新的RDD,所以RDD之间会形成类似流水线一样的前后依赖关系,在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。

      4.一个Partitioner,即RDD的分片函数(Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned))

           当前Spark中实现了两种类型的分片函数,一个是基于HashPartitioner,另一个是基于范围的RanggePartitioner,只有对于K-V的RDD,才会有Partitioner,非K-V的RDD的Partitioner的值是Node,Partitioner函数不但决定了RDD本身分片数量,也决定了父RDD Shuffle输出时的分片数量。

      5.一个列表,存取每个Partitioner的优先位置(Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file))

           对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置,按照“移动数据不如移动计算”的理念,Spark在进行任务调度时,会尽可能的将计算任务分配到其所要处理的数据块存储位置。

      小结:RDD就是一个应用层面的逻辑概念,一个RDD多个分片,RDD就是一个元数据记录集,记录了RDD内存所有的关系数据。

 

RDD的其他特性

血统:RDD表示只读的分区的数据集,对RDD进行改动,只能通过RDD的转换操作,由一个RDD得到另一个新的RDD,新的RDD包含了从其他RDD衍生所必需的信息,RDDs之间存在依赖,RDD的执行是按照血统关系延时计算的,如果血统关系比较长,可以通过持久化RDD来切断。

      分区:RDD逻辑上是分区的,每个分区的数据是抽象存在的,计算时会通过一个compute函数得到每个分区的数据,如果RDD是通过已有的数据系统构建,则compute函数是读取指定文件系统中的数据,如果RDD是通过其他RDD转换而来的,compute函数是执行转换逻辑将其他的RDD的数据进行转换。

      只读:RDD是只读的,要想改变RDD中的数据,只能在现有的RDD基础上创建新的RDD

      val words:RDD[String] = lines.flatMap(_.split(" "))

      val wordAndOne:RDD[(String,Int)] = words.map((_,1))

            由一个RDD转换到另一个RDD可以通过丰富的操作算子实现,不在像MapRedudce那样只能是写map和reduce了。(隐藏了细节)

           

 

         RDD的操作算子,包括两类:

                Transformation:是用来将RDD进行转化的,构建RDD的血统关系。(业务逻辑)

                Actions:是用来触发RDD的计算的,得到RDD的相关结果或将RDD保存到文件系统中。(业务逻辑执行结果获取)

 

      依赖:RDDs通过操作算子进行转换,转换得到新的RDD,包含了从其他RDDs衍生所必需的信息(元数据),RDDs之间维护这种血统关系,也叫做依赖。

      依赖分为两种:

           窄依赖,RDDs之间分区是一一对应,

           宽依赖:父RDD的数据给子RDD的多个分区,即子RDD的每个分区都和父RDD的每个分区都有关,是多对多的关系,也涉及到了Shuffle的操作。

          

      缓存:如果在应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算时,会根据血统关系得到分区的数据,在后续的其他地方用到该RDD时,会直接从缓存处获取,而不用在根据血统关系计算,这样就加速了后期的重用,经过一系列的转换后得到RDD-n,并保存到HDFS,RDD-1在这一过程中会有个中间结果,则将其缓存到内存中,那么在随后的RDD-1转换到RDD-m这一过程中,就不会计算其RDD-0了。

      RDD-1只是存储上一个RDD的元数据信息,RDD内部并不存储真正的数据。

 

 

RDD做了什么?

      SparkCore其实就是在操作RDD,从RDD的创建、转换、缓存、行动、输出。

     

      RDD定义为这一系列的方法都是数学的函数计算,每一个函数都是一个算子,这些算子之间有承上启下的作用,这些算子都是有关联的,如果其中一个算子出现问题了,可以通过这种关系重新找回哪里出问题了,再次接着运行,即前面运行过了不用再运行了。

      RDD的另一个重大的作用就是以前我们需要写Hadoop的Map和Reduce,现在不用了,RDD帮助我们做 分发、做了Map和Reduce,帮我们把任务发给每一个Worker里面的Executor去执行,内部自动分片,分区,隐藏了细节,不需要在关心Map阶段做什么,Reduce阶段做什么。

      学习RDD,其实就是学习里面的,这个过程的一些方法。(例如:如何让Spark去运行这个List的计算,Spark的基础就是RDD,那么就是如何把这个计算转换成符合RDD要求,从而进行运算)

 

      例如:1 + 2 + 3 + 4 + 5 + 6

      运算:1 + 2、3 + 3、……

      小结:这样的运算太慢了,如果1 + 2是一个Spark内部的Task得出来的结果,+3又是一个Task,那样太累了,而且太繁琐,如果是 1 + 2给一个Task1去运行这个逻辑,3 + 4给Task2运行,5 + 6给Task3运行,最后在进行一次性汇总,如果在一个Task内,就可以直接在内存完成操作,如果是Hadoop,则是要1 + 2放到HDFS,在去取……

 

RDD的编程模型

      在Spark中,RDD表示为对象,通过对象上的方法调用来对RDD进行转换。

      经过一系列的transformations定义RDD之后,就可以调用Actions来触发RDD的计算,Action可以是向应用程序返回结果(count、collect等),或者是向存储系统保存数据(saveAs……),在Spark中,只有遇到Action才会执行RDD的计算(延时计算),这样在运行时可以通过管道的方式传输多个转换。

      要使用Spark,开发者需要编写一个Driver程序,他被提交到集群以调度Worker。

      如图:Driver中定义了一个或多个RDD,并调用RDD上的Action,Worker则执行RDD分区的计算任务。

 

 

RDD的创建方式

 

import org.apache.spark.rdd.RDD

    import org.apache.spark.{SparkConf, SparkContext}

    

    object CreateRDD {

  def main(args: Array[String]): Unit = {

    //每次上传到服务器调试太麻烦了

    //本地调试

    val conf = new SparkConf()

      .setAppName("ScalaWordCount")

      //local 本地运行 - 一个结果文件

      //local[*] 按照cpu的核数开启对应的线程去执行(可以为*个Executor在运行),多个结果文件

      //local[2] 开启2个线程去跑(可以为2个Executor在运行),2个结果文件

      .setMaster("local[2]")

    

    //创建Spark执行入口 - RDD

    val sc = new SparkContext(conf)

    

    

    /*

      创建RDD:

         编程创建方式:parallelize()和makeRDD()创建

         RDD来源创建方式:

            Scala集合中创建:sc.parallelize(List(("jetty",2),("kitty",2),("tom",2)))

            读取外部文件:sc.textFile("hdfs://hadoop100/wc")

            从另一个RDD中创建:val rdd2: RDD[String] = rdd1.flatMap(_.split(" "))

     */

    //从Scala集合中创建

    val rdd:RDD[(String,Int)] = sc.parallelize(List(("jetty",2),("kitty",2),("tom",2)))

    //读取外部文件

    val rdd1: RDD[String] = sc.textFile("hdfs://hadoop100/wc")

    //从另一个集合中获取

    val rdd2: RDD[String] = rdd1.flatMap(_.split(" "))

    

    /*

      底层:

          def makeRDD[T: ClassTag](

            seq: Seq[T],

            numSlices: Int = defaultParallelism): RDD[T] = withScope {

            parallelize(seq, numSlices)

          }

     */

    sc.makeRDD(List(("jetty",2),("kitty",2),("tom",2)))

  }

}

RDD编程

常用Transformation

      RDD中的所有转换操作都是延迟加载(懒加载),也就是说,并不会直接计算结果,相反,只记住这些应用到基础数据集上的转换动作(元数据),只有当发生一个要求返回结果给Driver的动作时,这些转换才会真正的运行,这种设计让Spark更加有效率的运行。

 

转换函数

描述

map(func)

返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成

 

filter(func)

返回一个新的RDD,该RDD经过func函数计算后返回值为true的输入元素组成

 

flatMap(func)

类似于map,但是每个输入元素可以被映射为0或多个输出元素

 

mapPartitions

类似map,但是独立在RDD的每一个分区运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U],假设有N个元素,有M个分区,那么map的函数的将被调用N次,而mapPartitions被调用M次,一次处理一个分区的所

有数据。

 

mapPartitionsWithIndex(func)

类似mapPartitions,但func带有一个整数参数,表示分片的索引值,因此在类型为T的RDD上运行时,func函数类型必须为((Int, Interator[T]) => Iterator[U])

 

taskSample

返回最终的结果集合

 

union(otherDataset)

求并集

 

intersection(otherDataset)

求交集

 

distinct([numTasks])

去重,默认只有8个并行任务,但可以传入一个可选的numTask参数改变

 

partitionBy

对RDD进行分区操作,如果原有的partitionRDD和现有的一致,就不分区,否则产生ShuffleRDD

 

reduceByKey(func,[numTasks])

在一个(K,V)的RDD上调用,把Key值聚合到一起,reduce任务通过可选的第二个参数进行设置

 

groupByKey

对每个key进行操作,但只生成一个sequence

 

combineByKey(

createCombiner:V => C,

mergeValue(C,V) => C,

mergeCombiners(C,C)=>C)

对相同K,把V合并成为一个集合

createCombiner:会遍历分区所有元素

mergeValue:如果是一个在处理当前分区之前已经遇到的键,会使用mergeValue()方法,将该键的累加器对应的当前值与这个新的值进行合并。

mergeCombiners:将各个分区的结果进行合并

 

aggregateByKey(zeroValue:U,[partitioner: Partitioner]) (seqOp: (U, V) => U,combOp: (U, U) => U)

按Key将Value分组合并,合并时将每个value初始值作为第一个func的参数进行计算,返回一个新的KV对,然后第二个func在进行全局聚合

foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]

aggregateByKey的简化操作

sortByKey([ascending], [numTasks])

在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD

sortBy(func,[ascending], [numTasks])

与sortByKey类似,但是更灵活,可以用func先对数据进行处理,按照处理后的数据比较结果排序。

join(otherDataset, [numTasks])

 

在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD

cogroup(otherDataset, [numTasks])

 

在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable<V>,Iterable<W>))类型的RDD

 

cartesian(otherDataset)

笛卡尔积

 

pipe(command, [envVars])

 

对于每个分区,都执行一个perl或者shell脚本,返回输出的RDD

 

coalesce(numPartitions)     

 

缩减分区数,用于大数据集过滤后,提高小数据集的执行效率。

repartition(numPartitions)

根据分区数,从新通过网络随机洗牌所有数据。

repartitionAndSortWithinPartitions(partitioner)

repartitionAndSortWithinPartitions函数是repartition函数的变种,与repartition函数不同的是,repartitionAndSortWithinPartitions在给定的partitioner内部进行排序,性能比repartition要高。 

glom

将每一个分区形成一个数组,形成新的RDD类型时RDD[Array[T]]

 

mapValues

针对于(K,V)形式的类型只对V进行操作 

 

subtract

计算差的一种函数去除两个RDD中相同的元素,不同的RDD将保留下来 

 

常用Action

转换函数

描述

reduce(func)

通过func函数聚集RDD中的所有元素,这个功能必须是可交换且可并联的

collect()

在驱动程序中,以数组的形式返回数据集的所有元素

count()

返回RDD的元素个数

first()

返回RDD的第一个元素(类似于take(1))

take(n)

返回一个由数据集的前n个元素组成的数组

takeSample(withReplacement,num, [seed])

返回一个数组,该数组由从数据集中随机采样的num个元素组成,可以选择是否用随机数替换不足的部分,seed用于指定随机数生成器种子

takeOrdered(n)

返回前几个的排序

aggregate (zeroValue: U)(seqOp: (U, T) ⇒ U, combOp: (U, U) ⇒ U)

 

aggregate函数将每个分区里面的元素通过seqOp和初始值进行聚合,然后用combine函数将每个分区的结果和初始值(zeroValue)进行combine操作。这个函数最终返回的类型不需要和RDD中元素类型一致。

fold(num)(func)

折叠操作,aggregate的简化操作,seqop和combop一样。

saveAsTextFile(path)

将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本

saveAsSequenceFile(path) 

将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。

saveAsObjectFile(path) 

用于将RDD中的元素序列化成对象,存储到文件中。

 

countByKey()

针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。

foreach(func)

在数据集的每一个元素上,运行函数func进行更新。

 

统计操作

 

程序运行过程 (3)

 

RDD的持久化

RDD的缓存

    Spark非常快的原因之一,就是在不同操作中可以在内存中持久化或缓存某个数据集,当持久化某个RDD后,每一个节点都会将计算的分片结果保存到内存中,并对此RDD或衍生出来的RDD进行的其他动作中重用,这使得后续的操作变得更加迅速,RDD相关的持久化和缓存,是Spark最重要的特征之一,可以说缓存是Spark构建迭代式算法和快速交互式查询的关键,如果一个有持久化数据的节点发生故障,Spark会在需要用到缓存的数据时重算丢失的数据分区,如果希望接地单故障的情况下,不会拖累我们的执行速度,也可以把数据备份到多个节点上。

 

RDD可以通过perisst方法或cache方法将前面计算结果进行缓存,默认情况下erpsist()会把数据以序列化的形式保存到JVM的堆空间。

      但是并不是被调用立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点中的内存。

          

          

 

 

      由此可见,Spark并非全内存计算,而是内存放不下了,会缓存到磁盘或指定区域。

 

RDD的检查点机制

检查点机制(checkpoint)虽然RDD的血缘关系天然的实现容错,当RDD的某个分区数据失败或丢失,可以通过血统关系重建,但是对于长时间迭代型应用来说,随着迭代的进行,RDDs之间的血统关系也原来越长,一旦后续的迭代过程中出错了,则需要非常长的血统关系去重建,这样很不友好,为此RDD支持checkpoint,将数据保存到持久化的存储中,这样就可以切断之前的血统关系,因为checkpoint后的RDD不需要知道它父RDDs了,它可以从checkpoint处获取数据。(保存一部分下来,以后去这里拿,前面执行过的就不用再执行了)

      cache和checkpoint有显着的区别,缓存把RDD计算出来,然后放到内存中,但是RDD的依赖链(相当于数据库中的redo日志),也不能丢掉,当某个点某个executor挂了,上面的cache的RDD就会丢掉,需要通过依赖链重放,计算出来,不同的是checkpoint是把RDD保存在HDFS中,是多副本可靠存储,所以依赖链就可以丢掉了,所以是通过复制实现高容错。

 

      小结:

           1、分区数以及分区方式

           2、由父RDDs衍生而来的相关依赖信息

           3、计算每个分区的数据

                      1.如果被缓存,则从缓存分区数据

                      2.在1不成立的情况下,如果被checkpoint了,则从checkpoint处获取恢复数据

                       3.根据血统关系计算分区的数据

 

 

程序运行过程 (4)

 

 

RDD依赖关系

      RDD和它的父RDD(s)的关系有两种不同类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。

 

窄依赖

      窄依赖:是指一个父RDD的Partition最多被一个子RDD的一个Partition使用,相当于生活的独生子女。

 

宽依赖

      宽依赖:指的是多个子RDD的Partition会依赖同一个父RDD的Partition,会引起Shuffle,相当于生活的超生。

 

血统

      RDD只支持粗粒度转换,即在大量记录上执行的单个操作,将创建RDD的一系列血统记录下来,以便恢复丢失的分区。

      RDD的血统会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。

 

 

向RDD操作传递函数的注意

      Spark的大部分转换操作和一部分行动操作,都需要依赖用户传递的函数来计算,在Scala中,我们可以定义为内联函数、方法的引用和静态方法传递给Spark,就像Scala的其他函数式API一样。

      还需要考虑其他的细节,比如传递的函数及引用的数据需要是可序列化(实现Java的Serializable接口)。

      传递一个对象的方法或字段时,会包含对整个对象的引用。(执行端在Executor)

     

 

DAG生成

      DAG(Directed Acyclic Graph)名为有向无环图,原始的RDD通过一系列的转换就形成了DAG,根据RDD之间的依赖关系的不同,将DAG划分成不同的Stage,对于窄依赖,partition的转换处理在Stage中完成计算。
      对于宽依赖,由于有Shuffle的存在,只能在父RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据。

 

RDD相关概念关系

      输入可能是多个文件的形式存储在HDFS上,每个File都包含了很多块,称为Block。

      当Spark读取这些文件作为输入时,会根据具体的数据格式对应的InputFormat进行解析,一般是将若干个Block合并成为一个输入分片,称为InputSplit。(InputSplit是不能跨越文件的)

      在为这些输入分片生成具体的Task,InputSplit和Task是一一对应的关系。

      随后这些具体的Task每个都会被分配到集群上的某个节点的某个Executor去执行。

            1.每个节点可以起一个或多个Executor。

      2.每个Executor由若干个core组成,每个Executor的每个core一次只能执行一个Task。

           3.每个Task执行的结果就是生成了目标RDD的一个Partition。

          

           备:此core是虚拟的core,而不是真正的机器物理CPU核,可以理解为就是Executor进程中的一个工作线程。

                  

Task被执行的并发度 = Executor数目 * 每个Executor核数。

                  至于partition的数目:

1.对于数据读取阶段,例如sc.textFile,输入文件被划分为多少个InputSplit就会需要多少初始化Task。

                      2.在Map阶段partition数目保持不变。

3.在Reduce阶段,RDD的聚合会出发shuffle操作,聚合后的RDD的partition数目和具体的操作无关,例如repartition操作会聚合成指定分区数,还有一些算子是可配置的。

                

           RDD在计算时,每个分区都会起一个Task,所以RDD的分区数量决定了总的Task数量。

      申请的计算节点(Executor)数量和每个计算节点的核数,决定了在同一时刻可以并行执行的Task。

      比如有100分区,那么计算时就生成100个Task,资源配置是10个计算节点,每个2核,同一时刻的Task数量为20,计算RDD就需要5轮了。

      如果有101个就需要6轮,最后一轮只有一个Task运行,其他核空转,产生也是空结果。

      如果资源不变,RDD只有2个分区,那么同一时刻只有2个Task,其余18个核空转,造成资源浪费,这就是Spark调优中,增大RDD分区数目,增加任务并行度的做法。

 

 

程序运行过程 (5)

 

 

键值对RDD

      Spark为包含键值对类型的RDD,提供了一些转有的操作,在PairRDDFunctions专门进行了定义,这些RDD被称为pairRDD。

 

 

键值对RDD的部分Transformation操作

 

 

键值对RDD的部分Action操作

 

 

分区

HashPartitioner

      HashPartitioner分区的原理,对于给定的Key,计算其hashCode,并除于分区的个数取余,如果余数小于0,则用余数+分区的个数,最后返回的值就是这个Key所属的分区ID。

 

RangePartitioner

      HashPartitioner分区弊端:可能导致每个分区中数据量的均匀,极端情况下会导致某些分区拥有RDD的全部数据。

      RangePartitioner分区优势:尽量保证每个分区中的数据量的均匀,而且分区与分区直接是有序的,一个分区中的元素,肯定都是比另一个分区内的元素小或大。

      但是分区内的元素是不能保证顺序的,简单的说,就是将一定范围内的数据映射到某一个分区内。

      RangePartitioner作用:将一定范围内的数据映射到某一个分区内,在实现中,分界的算法尤为重要,用到了水塘抽样法。

 

自定义分区

import org.apache.spark.rdd.RDD

    import org.apache.spark.{Partitioner, SparkConf, SparkContext}

    

    object MyPartition {

  def main(args: Array[String]): Unit = {

    //每次上传到服务器调试太麻烦了

    //本地调试

    val conf = new SparkConf()

      .setAppName("ScalaWordCount")

      //local 本地运行 - 一个结果文件

      //local[*] 按照cpu的核数开启对应的线程去执行(可以为*个Executor在运行),多个结果文件

      //local[2] 开启2个线程去跑(可以为2个Executor在运行),2个结果文件

      .setMaster("local[2]")

    

    //创建Spark执行入口 - RDD

    val sc = new SparkContext(conf)

    

    val rdd:RDD[(String,Int)] = sc.parallelize(List(("jetty",2),("kitty",2),("tom",2)))

    

    val rdd2:RDD[(String,Int)] = rdd.reduceByKey(_+_)

    

    rdd2.partitionBy(new MyPartitioner(3,rdd))

    

    rdd2.cache()

    

    println("==================================")

    println(rdd2.collect().toBuffer)

    println(rdd2.partitions.length)

    //释放资源

    sc.stop()

  }

}

    

    //自定义分区器,默认是HashPartitioner

    class MyPartitioner(num: Int,rDD: RDD[(String,Int)]) extends Partitioner {

    

  //分区的数量,下一个RDD有多少个分区

  override def numPartitions: Int = num

    

  //根据传入的key计算分区标号

  //用key去标识分区,可传数组等(Any)

  override def getPartition(key: Any): Int = {

    val sub = key.asInstanceOf[(String,Int)]

    //分区编号

    sub._2

  }

}

文件输入输出

import org.apache.hadoop.io.{LongWritable, Text}

    import org.apache.spark.{SparkConf, SparkContext}

    import org.apache.spark.rdd.RDD

    

    object SaveFile {

  def main(args: Array[String]): Unit = {

    //每次上传到服务器调试太麻烦了

    //本地调试

    val conf = new SparkConf()

      .setAppName("ScalaWordCount")

      //local 本地运行 - 一个结果文件

      //local[*] 按照cpu的核数开启对应的线程去执行(可以为*个Executor在运行),多个结果文件

      //local[2] 开启2个线程去跑(可以为2个Executor在运行),2个结果文件

      .setMaster("local[2]")

    

    //创建Spark执行入口 - RDD

    val sc = new SparkContext(conf)

    

    

    //从Scala集合中创建

    val rdd:RDD[(String,Int)] = sc.parallelize(List(("jetty",2),("kitty",2),("tom",2)))

    //读取外部文件

    val rdd1: RDD[String] = sc.textFile("hdfs://hadoop100/wc")

    sc.

    /*

      底层:

          def makeRDD[T: ClassTag](

            seq: Seq[T],

            numSlices: Int = defaultParallelism): RDD[T] = withScope {

            parallelize(seq, numSlices)

          }

     */



    val hdfs: String = "hdfs://hadoop100/save"

    

    //保存对象文件,需要实现序列化接口

    rdd.saveAsObjectFile(hdfs + "Obj")

    rdd.saveAsTextFile(hdfs + ".txt")

    rdd.saveAsTextFile(hdfs + ".json")

    rdd.saveAsHadoopFile(hdfs + "Hadoop")

    rdd.saveAsSequenceFile(hdfs + "Seq")

    

    //这是Hadoop新Api

    //rdd.saveAsNewAPIHadoopFile(hdfs + "NewHadoopApi",classOf[LongWritable],classOf[Text],classOf[org.apache.hadoop.mapreduce.lib.output.TextOutputFormat[LongWritable,Text]])

    //其他的 rdd.saveAs....

    

    //读取

    val rddSeqFile: RDD[(Nothing, Nothing)] = sc.sequenceFile(hdfs + "Seq")

    

  }

}

数据库的输入输出

import java.sql.DriverManager

    

    import org.apache.spark.rdd.{JdbcRDD, RDD}

    import org.apache.spark.{SparkConf, SparkContext}

    

    /**

  * JDBC 连接数据库

  */

    object JdbcRDDTest {

    

  val getConn = () => {

    DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/bigdata?characterEncoding=utf-8","root","root")

  }

    

  def main(args: Array[String]): Unit = {

    

    //配置

    val conf = new SparkConf()

                      .setAppName("JdbcRDD")

                      .setMaster("local[*]")

    

    //程序入口

    val sc = new SparkContext(conf)

    

    //里面没有真正要计算的数据,而是告诉RDD,以后触发Action,在哪里读取

    val jdbcRDD:RDD[(Int,String)] = new JdbcRDD(sc,

                getConn,

                "select * from access_log where id = ? and id = ?",

                1, //第一个?

                3, //upperBound

                2, //分区数量

                rs => { //结果

                  val id = rs.getInt(1)

                  val name = rs.getString(2)

                  (id,name)

                })

    

    //触发action

    val r = jdbcRDD.collect()

    println(r.toBuffer)

    

    //释放资源

    sc.stop()

  }

}

广播变量

import java.sql.{Connection, DriverManager, PreparedStatement}

    

    import org.apache.spark.broadcast.Broadcast

    import org.apache.spark.rdd.RDD

    import org.apache.spark.{SparkConf, SparkContext}

    

    

    
/**

  * 广播变量:是用来高效的分发一些较大的对象,

  * 向所有工作节点发送一个只读值,以供一个或多个Spark操作使用。

  * 比如:需要向所有节点发送一个较大的只读查询表,

  * 甚至是机器学习算法中的一个很大特征向量,广播变量用起来会很顺手。

  */
    object Broadcase {

  def main(args: Array[String]): Unit = {

    

    //Spark配置

    val conf = new SparkConf()

                      .setAppName("Broadcase")

                      .setMaster("local[*]")

    

    //Spark应用入口

    val sc = new SparkContext(conf)

    

    val rdd:RDD[(String,Int)] = sc.parallelize(List(("jetty",2),("kitty",2),("tom",2)))

    

    //一个大的变量,不用在每一个都有一份,直接公用一个大的变量

    val broadcase: Broadcast[Array[(Int,String)]] = sc.broadcast(Array((1,"jerry"),(2,"tom"),(3,"mike")))

    

    

    //对于性能的优化,可以使用这个,每次提取的是一整个分区数据做批量处理,适合多数据使用

    //视场景使用

    //rdd.foreachPartition()

    

    //-----------------------------------------

    // 没有实现Serializable接口,传送到Executor执行时就执行不了。

    //这也说明了,到Executor执行的对象,必须实现Seriablizable接口

    

    //写入到数据库中

    //val connection: Connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/bigdata?characterEncoding=utf-8","root","root")

    //保存数据

    //val prepareStatement: PreparedStatement = connection.prepareStatement("INSERT INTO access_log VALUES(?,?)")

    //-----------------------------------------

    

    

    rdd.foreach(tp2 => {

      //只有是jetty的话,才需要把数据保存到数据库

      if(tp2._1.equals("jetty")){

        //取出广播变量值

        //广播变量,广播出去的内容就不能改变了,如果需要实时改变可以把规则放到Redis中

        val value: Array[(Int,String)] = broadcase.value

    

        //写入到数据库中

        val connection: Connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/bigdata?characterEncoding=utf-8","root","root")

    

        //保存数据

        val prepareStatement: PreparedStatement = connection.prepareStatement("INSERT INTO access_log VALUES(?,?)")

    

        value.foreach(tp2 => {

          prepareStatement.setInt(1,tp2._1) //第一个 ?

          prepareStatement.setString(2,tp2._2) //第二个 ?

          prepareStatement.executeUpdate()

        })

    

        //释放资源

        prepareStatement.close()

        connection.close()

      }

    })

    

    //释放资源

    sc.stop()

  }

}

自定义排序

 

import org.apache.spark.rdd.RDD

    import org.apache.spark.{SparkConf, SparkContext}

    

    object MySort {

  def main(args: Array[String]): Unit = {

    

    //配置

    val conf = new SparkConf()

                      .setAppName("MySort")

                      .setMaster("local[*]")

    

    //程序入口

    val sc = new SparkContext(conf)

    

    //排序规则:首先按照颜值从高到低进行排序,如果颜值相等,在按照年龄升序

    val users = Array("A 30 92","B 28 98","C 32 99")

    

    //将Driver端的数据变成RDD

    val lines:RDD[String] = sc.parallelize(users)

    

    //切分整理数据

    /*val tpRDD:RDD[(String,Int,Int)] = lines.map(line => {

      val fields = line.split(" ")

      val name = fields(0)

      val age = fields(1).toInt

      val fv = fields(2).toInt

      (name,age,fv)

    })*/

    

    //不满足要求

    //tpRDD.sortBy(tpRDD => tpRDD._3,false)

    

    val tpRDD:RDD[User] = lines.map(line => {

      val fields = line.split(" ")

      val name = fields(0)

      val age = fields(1).toInt

      val fv = fields(2).toInt

      new User(name,age,fv)

    })

    

    //将RDD内的User类型数据进行排序

    val sorted: RDD[User] = tpRDD.sortBy(u => u)

    

    val r = sorted.collect()

    println("=========================")

    println(r.toBuffer)

    

    //释放资源

    sc.stop()

  }

}

    

    class User(val name: String,val age: Int,val fv: Int) extends Ordered[User] with Serializable {

    

  override def compare(that: User): Int = {

    //正数是升序,负数,你懂得

    if(this.fv == that.fv) {

      this.age - that.age

    } else {

      -(this.fv - that.fv)

    }

  }

    

  override def toString: String = {

    name + " - " + age + " - " + fv

  }

}

 

回顾WordCount的RDD过程

 

 

Spark RDD的十五问

1.SparkContext是在哪一端创建?

Driver

 

2.DAG是在哪一端被构建的?

Driver

 

3.RDD是在哪一端生成?

Driver

 

4.广播变量在哪一端调用的方法进行广播的?

Driver

 

5.要广播的数据应该在哪一端先创建好再广播?

Driver

 

6.调用RDD的算子(Transformation和Action)是在哪一端调用的?

Driver

 

7.RDD在调用Transformation和Action时需要传入函数,函数是在哪一端声明和调用的?

Driver

 

8.RDD在调用Transformation和Action时需要传入函数,请问传入的函数是在哪一端执行了函数的业务逻辑?

Executor的Task

 

9.自定义的分区器这个类是在哪一端被实例化的?

Driver

 

10.分区器中的getPartition方法是在哪一端调用的?

Executor的Task

 

11.Task是在哪一端生成的?

Driver

 

12.DAG是在哪一端构建好的并切分成一到多个Stage的

Driver

 

13.DAG是在哪个类完成的切分Stage的功能?

DAGScheduler

 

14.DAGScheduler将切分好的Stage以什么样的形式给TaskScheduler?

TaskSet

 

15.Executor从哪里获取Task?

Driver的TaskSet

 

RDD弹性的表现

      存储的弹性:内存和硬盘自动切换。

      容错的弹性:数据丢失了可以通过血统找回来,进而恢复。

      计算的弹性:计算出错的重试机制,Job层面、Task层面

      分片的弹性:根据需要进行重新分片,如果在加一台机器会重新分片。

 

两个问题解答

      问题的答案永远不是唯一的,每个人理解不同。

 

    Spark真的是用来取代Hadoop吗?

           1.对Spark做了一个完整性的学习后,在现在会感觉Spark其实就是隐藏了细节,以前如果要做reduceByKey,需要自己去写一个完整的Map –> Reduce的程序,现在不用了,我们直接调用Spark的RDD提供的算子就可以了,RDD内部会帮助程序做任务的分片、Map到Reduce聚合的整个过程控制。

           2.我们也看到Spark对Hadoop的支持提供了两种Api,煞费苦心。

 

/**
 * Smarter version of hadoopFile() that uses class tags to figure out the classes of keys,
 * values and the InputFormat so that users don't need to pass them directly. Instead, callers
 * can just write, for example,
 *
{{{
 * val file = sparkContext.hadoopFile[LongWritable, Text, TextInputFormat](path)
 *
}}}
 *
 *
@note Because Hadoop's RecordReader class re-uses the same Writable object for each
 * record, directly caching the returned RDD or directly passing it to an aggregation or shuffle
 * operation will create many references to the same object.
 * If you plan to directly cache, sort, or aggregate Hadoop writable objects, you should first
 * copy them using a
`map` function.
 *
@param path directory to the input data files, the path can be comma separated paths as
 * a list of inputs
 *
@return RDD of tuples of key and corresponding value
 */

def hadoopFile[K, V, F <: InputFormat[K, V]](path: String)
    (
implicit km: ClassTag[K], vm: ClassTag[V], fm: ClassTag[F]): RDD[(K, V)] = withScope {
  hadoopFile[
K, V, F](path, defaultMinPartitions)
}


/**
 * Smarter version of
`newApiHadoopFile` that uses class tags to figure out the classes of keys,
 * values and the
`org.apache.hadoop.mapreduce.InputFormat` (new MapReduce API) so that user
 * don't need to pass them directly. Instead, callers can just write, for example:
 *
```
 * val file = sparkContext.hadoopFile[LongWritable, Text, TextInputFormat](path)
 *
```
 *
 *
@note Because Hadoop's RecordReader class re-uses the same Writable object for each
 * record, directly caching the returned RDD or directly passing it to an aggregation or shuffle
 * operation will create many references to the same object.
 * If you plan to directly cache, sort, or aggregate Hadoop writable objects, you should first
 * copy them using a
`map` function.
 *
@param path directory to the input data files, the path can be comma separated paths
 * as a list of inputs
 *
@return RDD of tuples of key and corresponding value
 */

def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]]
    (path:
String)
    (
implicit km: ClassTag[K], vm: ClassTag[V], fm: ClassTag[F]): RDD[(K, V)] = withScope {
  newAPIHadoopFile(
    path,
    fm.runtimeClass.asInstanceOf[
Class[F]],
    km.runtimeClass.asInstanceOf[
Class[K]],
    vm.runtimeClass.asInstanceOf[
Class[V]])
}

               

            3.在存储上,Spark基本还是以HDFS、S3等为主

           4.Spark推荐SparkStreaming在YARN上运行,因为YARN的隔离机制做的更好。

 

    Spark和Hadoop的关系是什么?

           1.Hadoop刚出来时,只要提起Hadoop就代表着大数据,而Hadoop自身也是以一个大数据处理基础架构而存在,Hadoop提供了在大数据处理领域的整个架构思想(不谈谷歌),数据运算核心思想(MapReduce)。

 

           2.随着互联网越走越快,Hadoop的使用者越来越多,以此发展起来的一个生态圈越来越大,此时Hadoop的MapReduce模型,无法应付到来的机器学习时代(迭代式运算),此时的Apache难道要对Hadoop的MapReduce编程模型进行改变么?

毕竟是不现实的,因为太多企业使用了Hadoop,而Hadoop的存储和YARN这个资源调度框架是没有问题的,既然如此还不如找一个符合时代需求的新的计算模型,这个新的计算模型实现的框架可以对接上去Hadoop的HDFS和YARN上去就行了,那么就都可以满足了。

           但这个新技术怎么让这些公司都用起来呢?

           Spark取代Hadoop似乎是一个不错的Spark宣传广告的标语。

          

 

猜你喜欢

转载自blog.csdn.net/Su_Levi_Wei/article/details/86697896
今日推荐