4、Spark概要

一、基本特性

1、与MapReduce不同

        不同于MapReduce的是Job中间输出和结果可以“保存在内存”中,从而不再需要读写HDFS,

(1)、基于内存

        mapreduce任务后期再计算的时候,每一个job的输出结果会落地到磁盘,后续有其他的job需要依赖于前面job的输出结果,这个时候就需要进行大量的磁盘io操作。性能就比较低。

        spark任务后期再计算的时候,job的输出结果可以保存在内存中,后续有其他的job需要依赖于前面job的输出结果,这个时候就直接从内存中获取得到,避免了磁盘io操作,性能比较高、


(2)、进程与线程

        mapreduce任务以进程的方式运行在yarn集群中,比如程序中有100个MapTask,一个task就需要一个进程,这些task要运行就需要开启100个进程。

        spark任务以线程的方式运行在进程中,比如程序中有100个MapTask,后期一个task就对应一个线程,这里就不在是进程,

        这些task需要运行,这里可以极端一点:只需要开启1个进程,在这个进程中启动100个线程就可以了。进程中可以启动很多个线程,而开启一个进程与开启一个线程需要的时间和调度代价是不一样。开启一个进程需要的时间远远大于开启一个线程。

2spark集群安装部署

        vim spark-env.sh#配置java的环境变量  #配置zk相关信息

        vim slaves指定spark集群的worker节点

         vim /etc/profile  修改spark环境变量

        环境变量生效  source /etc/profile


1、先启动zk   ${ZK_HOME}/bin/zkServer.sh start

2、启动spark集群 $SPARK_HOME/sbin/start-all.sh

3、zk作用:高可用

        在高可用模式下,整个spark集群就有很多个master,其中只有一个master被zk选举成活着的master,其他的多个master都处于standby,同时把整个spark集群的元数据信息通过zk中节点进行保存。

        如果活着的master挂掉。首先zk会感知到活着的master挂掉,开始在多个处于standby中的master进行选举,再次产生一个活着的master;这个活着的master会读取保存在zk节点中的spark集群元数据信息,恢复到上一次master的状态。


在master的恢复阶段对任务的影响?

        对已经运行的任务是没有任何影响,由于该任务正在运行,说明它已经拿到了计算资源,这个时候就不需要master。

        对即将要提交的任务是有影响,由于该任务需要有计算资源,这个时候会找活着的master去申请计算资源,由于没有一个活着的master,该任务是获取不到计算资源,也就是任务无法运行。


4、web管理界面

http://master主机名:8080:集群的详细信息、总资源信息、已用资源信息、还剩资源信息;正在运行的任务信息、已经完成的任务信息


bin/spark-submit

--class org.apache.spark.examples.SparkPi \

--master spark://node01:7077,node02:7077,node03:7077 \

--executor-memory 1G \

--total-executor-cores 2 \

examples/jars/spark-examples_2.11-2.3.3.jar \

10


        spark集群中有很多个master,并不知道哪一个master是活着的master,即使你知道哪一个master是活着的master,它也有可能下一秒就挂掉,这里就可以把所有master都罗列出来

--master spark://node01:7077,node02:7077,node03:7077

        后期程序会轮训整个master列表,最终找到活着的master,然后向它申请计算资源,最后运行程序。


5、spark-shell使用

spark-shell --master local[2],默认会产生一个SparkSubmit进程,sc

--master local[N]:表示本地采用N个线程计算任务

sc.textFile("file:///home/words.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).collect


读取HDFS上文件: vim spark-env.sh export HADOOP_CONF_DIR=hdoop安装位置

//实现读取hdfs上文件之后,需要把计算的结果保存到hdfs上

sc.textFile("/words.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).saveAsTextFile("/out")

import org.apache.spark.rdd.RDD

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

//todo:利用scala语言开发spark程序实现单词统计

object WordCount {

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

    //1、构建sparkConf对象 设置application名称和master地址

val sparkConf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[2]")

//2、构建sparkContext对象,该对象非常重要,它是所有spark程序的执行入口,它内部会构建 DAGScheduler和 TaskScheduler 对象

    val sc = new SparkContext(sparkConf)

    sc.setLogLevel("warn")   //设置日志输出级别

    val data: RDD[String] = sc.textFile("E:\\words.txt")    //3、读取数据文件

  val words: RDD[String] = data.flatMap(x=>x.split(" "))   //4 切分每一行,获取所有单词

    val wordAndOne: RDD[(String, Int)] = words.map(x => (x,1))//5、每个单词计为1

    val result: RDD[(String, Int)] = wordAndOne.reduceByKey((x,y)=>x+y) //6、相同单词出现的1累加

    //按照单词出现的次数降序排列 第二个参数默认是true表示升序,设置为false表示降序

    val sortedRDD: RDD[(String, Int)] = result.sortBy( x=> x._2,false)

    val finalResult: Array[(String, Int)] = sortedRDD.collect()

    finalResult.foreach(println)     //7、收集数据打印

    sc.stop()   //8、关闭sc

  }

}


打成jar包提交到集群中运行

spark-submit \

--master spark://node01:7077,node02:7077 \

--class com.kaikeba.WordCountOnSpark \

--executor-memory 1g \

--total-executor-cores 4 \

original-spark_class01-1.0-SNAPSHOT.jar /words.txt /out   jar包与输入输出


spark-submit --class org.apache.spark.examples.SparkPi \

--master yarn \

--deploy-mode cluster \

--driver-memory 1g \

--executor-memory 1g \

--executor-cores 1 \

/kkb/install/spark/examples/jars/spark-examples_2.11-2.3.3.jar 10 10是main方法里面的参数


            executor-memory小了,会把rdd一部分数据保存在内存中,一部分数据保存在磁盘;用该rdd时从内存和磁盘中获取,一定的磁盘io。需要设置的大一点,如10G/20G/30G等;

            total-executor-cores:表示任务运行需要总的cpu核数,它决定了任务并行运行的粒度,也会设置的大一点,如30个/50个/100个;    

        加大计算资源它是最直接、最有效果的优化手段。  在计算资源有限的情况下,可以考虑其他方面,比如说代码层面,JVM层面等

6、两种Yarn模式,最大的区别就是Driver端的位置不一样。

            yarn-cluster: Driver端运行在yarn集群中,与ApplicationMaster进程在一起。

            yarn-client:  Driver端运行在提交任务的客户端,与ApplicationMaster进程没关系,经常 用于进行测试


二、集群架构

2、spark集群架构

(1)Master:集群的主节点,负责任务资源的分配。

(2)Worker:集群的从节点,负责任务计算的节点。

            ①Executor:是一个进程,它会在worker节点启动该进程(计算资源)

            ②Task:任务是以task线程的方式运行在worker节点对应的executor进程中;

(3)ClusterManager:给程序提供计算资源的外部服务,standAlone模式整个任务的资源分配由spark集群的老大Master负责;把spark程序提交到yarn中运行,整个任务的资源分配由yarn中的老大ResourceManager负责

(4)Driver:是所有spark程序的执行入口,会执行客户端写好的main方法,它会构建一个名叫SparkContext对象

(5)Application:是一个spark的应用程序,它是包含了客户端的代码和任务运行的资源信息

(6)一个application就是一个应用程序,包含了很多个job;

            一个action操作对应一个DAG有向无环图,即一个action操作就是一个job;

            一个job中包含了大量的宽依赖,按照宽依赖进行stage划分,一个job产生了很多个stage;

            一个stage中有很多分区,一个分区就是一个task,即一个stage中有很多个task;


Spark中的调度模式:FIFO(先进先出)、FAIR(公平调度)

任务的分配资源worker策略:尽量打散、尽量集中

            尽量打散:一个Application尽可能多的分配到不同的节点,发挥数据的本地性,提升执行效率

            尽量集中:尽量分配到尽可能少的节点

3、运行流程

        (1) Driver端向资源管理器Master发送注册和申请计算资源的请求

        (2) Master通知对应的worker节点启动executor进程(计算资源)

        (3) executor进程向Driver端发送注册并且申请task请求

        (4) Driver端运行客户端的main方法,构建SparkContext对象,在SparkContext对象内部依次构建DAGScheduler和TaskScheduler

        (5)按照客户端代码rdd的一系列操作顺序,生成DAG有向无环图

         (6) DAGScheduler拿到DAG有向无环图之后,按照宽依赖进行stage的划分。每一个stage内部有很多可以并行运行的task,最后封装在一个一个的taskSet集合中,然后把taskSet发送给TaskScheduler

        (7) TaskScheduler得到taskSet集合之后,依次遍历取出每一个task提交到worker节点上的executor进程中运行

        (8)所有task运行完成,Driver端向Master发送注销请求,Master通知Worker关闭executor进程,Worker上的计算资源得到释放,最后整个任务也就结束了。


三、计算资源

1RDD概念

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

        Resilient: 表示弹性,rdd的数据是可以保存在内存或者是磁盘中.

        Distributed:它内部的元素进行了分布式存储,方便于后期进行分布式计算.

        Dataset:就是一个集合,存储很多数据.


五大属性:

 (1) A list of partitions:一个rdd有很多分区,每一个分区内部是包含了该rdd的部分数据

 (2) A function for computing each split:每个分区都会实现 计算函数

 (3) A list of dependencies on other RDDs:一个rdd会依赖于其他多个rdd

 (4) Optionally, a Partitioner for key-value RDDs :kv数据的分区函数基于哈希,非kv是None

 (5) Optionally, a list of preferred locations to compute each split on:有分区数据的节点会优先开启计算任务,数据的本地性


RDD自定义分区

RDD数据进行分区时,默认使用的是HashPartitioner:对key进行哈希,然后对分区总数取模,

实现自定义partitioner大致分为3个步骤:

            继承==org.apache.spark.Partitioner==

            重写==numPartitions==方法

            重写==getPartition==方法


//对应上面的rdd数据进行自定义分区

val result: RDD[(String, Int)] = wordLengthRDD.partitionBy(new MyPartitioner(3))


2RDD的创建

1、scala集合sc.parallelize

        val rdd1=sc.parallelize(List(1,2,3,4,5))

        val rdd2=sc.parallelize(Array("hadoop","hive","spark"))

        val rdd3=sc.makeRDD(List(1,2,3,4))

2、加载外部的数据源sc.textFile

        val rdd1=sc.textFile("/words.txt")

3、已存在rdd转换成一个新的rdd

        val rdd2=rdd1.flatMap(_.split(" "))

        val rdd3=rdd2.map((_,1))


3RDD算子分类

transformation算子:根据已经存在的rdd转换生成一个新的rdd, 延迟加载,不会立即执行

                                map 、mapPartitions、flatMap、

                                filter、Union(求并)、intersection(求交)、distinct(去重)、

                                Join、reduceByKey 、groupByKey、sortByKey、sortBy

                                repartition有shuffle、coalesce不shuffle


action算子:真正触发任务的运行:

                reduce、

                collect :把RDD的数据进行收集之后,以数组的形式返回给Driver端

                count、first、take(n)

                foreach、foreachPartition

                saveAsTextFile、saveAsSequenceFile


        默认Driver端的内存大小为1G,由参数 spark.driver.memory 设置,果某个rdd的数据量超过了Driver端默认的1G内存,对rdd调用collect操作,这里会出现Driver端的内存溢出,所有这个collect操作存在一定的风险,实际开发代码一般不会使用。new SparkConf().set("spark.driver.memory","5G")

4、RDD依赖

RDD和它依赖的父RDD的关系有两种不同的类型

        窄依赖:每一个父RDD的Partition最多被子RDD的一个Partition使用,不会产生shuffle;Map、flatMap、filter、union等等,

        宽依赖:多个子RDD的Partition会依赖同一个父RDD的Partition,会产生; shufflereduceByKey/sortByKey/groupBy/groupByKey/join等等

        join分为宽依赖和窄依赖,如果RDD有相同的partitioner,那么将不会引起shuffle,这种join是窄依赖,反之就是宽依赖


5、RDD的Lineage血统

            lineage保存了RDD的依赖关系,会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区

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


6RDD缓存机制

        把一个rdd的数据缓存起来,后续有其他的job需要用到该rdd的结果数据,可以直接从缓存中获取得到,避免了重复计算。缓存是加快后续对该数据的访问操作。

(1)persist/cache

     RDD通过persist方法cache方法可以将前面的计算结果缓存,不会立即缓存,触发后面的action,才会被缓存在计算节点的内存中。调用rdd的unpersist方法,一个application应用程序结束之后,对应的缓存数据也就自动清除;

        cache:最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份

        persist:可以把数据缓存在内存或者是磁盘,有丰富的缓存级别,这些缓存级别都被定义在StorageLevel这个object中。

        为了获取得到一个rdd的结果数据,经过了大量的算子操作或者是计算逻辑比较复杂,可以把多次使用到的rdd,是公共rdd进行持久化;

(2)checkpoint

        把数据保存在内存中不安全,服务器挂掉或进程终止,会导致数据的丢失;存在本地磁盘中,操作删除了,或者是磁盘损坏,也有可能导致数据的丢失;

            checkpoint把数据保存在分布式文件系统HDFS上。高可用性,高容错性(多副本)来最大程度保证数据的安全性。

    1、在hdfs上设置一个checkpoint目录 sc.setCheckpointDir("hdfs://node01:8020/checkpoint")

    2、对需要做checkpoint操作的rdd调用checkpoint方法

                val rdd1=sc.textFile("/words.txt")

                rdd1.checkpoint

                val rdd2=rdd1.flatMap(_.split(" "))

3、最后需要有一个action操作去触发任务的运行

                rdd2.collect

7、广播变量

        spark中分布式执行的代码,需要==传递到各个Executor的Task上运行==。对于一些只读、固定的数据(比如从DB中读出的数据),每次都需要Driver广播到各个Task上,这样效率低下

        广播变量允许将变量广播给各个Executor。该Executor上的各个Task从所在节点的BlockManager获取变量,而不是从Driver获取变量,以减少通信的成本,减少内存的占用,从而提升了效率。

        (1)通过对一个类型T的对象调用 SparkContext.broadcast创建出一个Broadcast[T]对象。(任何可序列化的类型都可以这么实现)

        (2)通过 value 属性访问该对象的值

        (3)变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)

val conf = new SparkConf().setMaster("local[2]").setAppName("brocast")

val sc=new SparkContext(conf)

val rdd1=sc.textFile("/words.txt")

val word="spark"

//通过调用sparkContext对象的broadcast方法把数据广播出去

val broadCast = sc.broadcast(word)

//在executor中通过调用广播变量的value属性获取广播变量的值

val rdd2=rdd1.flatMap(_.split(" ")).filter(x=>x.equals(broadCast.value))

rdd2.foreach(x=>println(x))


注意:

        1、不能将一个RDD使用广播变量广播出去

        2、广播变量只能在Driver端定义,不能在Executor端定义

        3、在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的值

        4、如果executor端用到了Driver的变量,如果不使用广播变量在Executor有多少task就有多少Driver端的变量副本

        5、如果Executor端用到了Driver的变量,如果使用广播变量在每个Executor中只有一份Driver端的变量副本


累加器

        累加器(accumulator)是Spark中提供的一种分布式的变量机制,其原理类似于mapreduce,即分布式的改变,然后聚合这些改变。

        累加器的一个常见用途是,在调试时对作业执行过程中的事件进行计数。可以使用累加器来进行全局的计数。


8DAG划分stage

        原始的RDD通过一系列的转换就形成了DAG。(Directed Acyclic Graph,有向无环图)

        根据RDD之间依赖关系的不同将DAG划分成不同的Stage(调度阶段);对于窄依赖,转换处理在一个Stage中完成计算;对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,

        划分完stage之后,在同一个stage中只有窄依赖,没有宽依赖,可以实现流水线计算;stage中的每一个分区对应一个task,在同一个stage中就有很多可以并行运行的task。


        一个Job会被拆分为多组Task,每组任务被称为一个stage。stage表示不同的调度阶段,一个spark job会对应产生很多个stage


划分stage的依据就:宽依赖

        (1)首先根据rdd的算子操作顺序生成DAG有向无环图,接下里从最后一个rdd往前推,创建一个新的stage,把该rdd加入到该stage中,它是最后一个stage。

         (2)在往前推的过程中运行遇到了窄依赖就把该rdd加入到本stage中,如果遇到了宽依赖,就从宽依赖切开,那么最后一个stage也就结束了。

        (3) 重新创建一个新的stage,按照第二个步骤继续往前推,一直到最开始的rdd,整个划分stage也就结束了。

        划分完stage之后,每一个stage中有很多可以并行运行的task,后期把每一个stage中的task封装在一个taskSet集合中,最后把一个一个的taskSet集合提交到worker节点上的executor进程中运行。

        rdd与rdd之间存在依赖关系,stage与stage之前也存在依赖关系,前面stage中的task先运行,运行完成了再运行后面stage中的task,也就是说后面stage中的task输入数据是前面stage中task的输出结果数据。

9序列化

        spark是分布式执行引擎,其核心抽象是弹性分布式数据集RDD,其代表了分布在不同节点的数据。Spark的计算是在executor上分布式执行的,故用户开发的关于RDD的map,flatMap,reduceByKey等transformation 操作(闭包)有如下执行过程:

         1)代码中对象在driver本地序列化,对象序列化后传输到远程executor节点

        2)远程executor节点反序列化对象,最终远程节点执行。

对象在执行中,需要序列化通过网络传输,则必须经过序列化过程。

解决序列化的办法:

        1)如果函数中使用了该类对象,该类要实现序列化,类extends Serializable

        2)如果函数中使用了该类对象的成员变量,该类除了要实现序列化之外,所有的成员变量必须要实现序列化

        3)对于不能序列化的成员变量使用==“@transient”==标注,告诉编译器不需要序列化

        4)也可将依赖的变量,独立放到一个小的class中,让这个class支持序列化,这样做可以减少网络传输量,提高效率。

        5)可以把对象的创建直接在该函数中构建这样避免需要序列化�

10spark的shuffle

        Shuffle就是对数据进行重组,由于分布式计算的特性和要求,在实现细节上更加繁琐和复杂。Stage阶段的划分:是根据是否有宽依赖shuffle过程,job会划分成多个Stage,每一个stage内部有很多task。stage与stage之间的过程就是shuffle阶段。

        在Spark的中,负责shuffle过程的执行、计算和处理的组件,主要就是ShuffleManager。ShuffleManager分为HashShuffleManagerSortShuffleManager,因此spark的Shuffle有Hash Shuffle和Sort Shuffle两种。

        在Spark 1.2以前默认的shuffle是HashShuffleManager。该ShuffleManager-HashShuffleManager有着一个非常严重的弊端,就是会产生大量的中间磁盘文件,进而由大量的磁盘IO操作影响了性能。因此在Spark 1.2以后的版本中,默认的ShuffleManager改成了SortShuffleManager

       SortShuffleManager相较于HashShuffleManager来说,有了一定的改进。主要就在于每个Task在进行shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并(merge)成一个磁盘文件,因此每个Task就只有一个磁盘文件。在下一个stage的shuffle read task拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据即可。

        HashShuffleManager的运行机制主要分成两种:普通运行机制、合并运行机制;合并机制主要是通过复用buffer来优化Shuffle过程中产生的小文件的数量

        普通HashShuffle :

            shuffle write,每个task处理的数据按key进行“hash分区”,每个task都要创建分区个份磁盘文件,每个Executor上总共就要创建task*分区数个磁盘文件, 产生的磁盘文件的数量是极其惊人的;

            shuffle read的过程中,每个task要从上游stage的所有task所在节点上,拉取属于自己的那一个磁盘文件,每个ask都有一个自己的buffer缓冲,每次都只能拉取与buffer缓冲相同大小的数据32k,   

        缺点:在磁盘上会产生海量的小文件,建立通信和拉取数据的次数变多,此时会产生大量耗时低效的 IO 操作;大量耗时低效的 IO 操作 ,导致写磁盘时的对象过多,读磁盘时候的对象也过多,这些对象存储在堆内存中,会导致堆内存不足,相应会导致频繁的GC,GC会导致OOM。

   合并HashShuffle :

        一个Executor只有一种类型的Key的数据,每个Executor上总共就要创建分区数个磁盘文件;

        缺点:如果Reducer 端的并行任务或者是数据分片过多的话则 Core * Reducer Task 依旧过大,也会产生很多小文件。


普通SortShuffle

        普通SortShuffle 相当于预聚合,数据会先写入一个数据结构,聚合算子写入Map,一边通过Map局部聚合,一边写入内存。。在该模式下,数据会先写入一个数据结构,聚合算子写入Map,一边通过Map局部聚合,一边写入内存。Join算子写入ArrayList直接写入内存中。然后需要判断是否达到阈值(5M),如果达到就会将内存数据结构的数据写入到磁盘,清空内存数据结构。

        在溢写磁盘前,先根据key进行排序,排序过后的数据,会分批写入到磁盘文件中。每批一万条写入到一个临时磁盘文件,每次溢写都会产生一个临时磁盘文件,一个task过程会产生多个临时文件。最后在每个task中,将所有的临时文件合并merge,一次写入到最终文件。

        一个task的所有数据都在这一个文件中。同时单独写一份索引文件,标识下游各个task的数据在文件中的索引start offset和end offset。这样算来如果第一个stage 50个task,每个Executor执行一个task,那么无论下游有几个task,就需要50*2=100个磁盘文件。

            1. 小文件明显变少了,一个task只生成一个file文件

            2. file文件整体有序,加上索引文件的辅助,查找变快,虽然排序浪费一些性能,但是查找变快很多


bypass模式SortShuffle 

            优化后sortshuffle的普通机制相比,在shuffleMapTask不多的情况下,首先写的机制是不同,其次不会进行排序。这样就可以节约一部分性能开销。

            在shuffleMapTask数量小于默认值200时,启用bypass模式的sortShuffle(原因是数据量本身比较少,没必要进行sort全排序,因为数据量少本身查询速度就快,正好省了sort的那部分性能开销。)


该机制与普通SortShuffleManager运行机制的不同在于:

        第一: 磁盘写机制不同;

        第二: 不会进行sort排序;

bypass机制运行条件

- shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值

- 不是聚合类的shuffle算子(比如reduceByKey)


四、SparkSQL

是apache Spark用来处理结构化数据的一个模块

1、sparksql的四大特性

(1)易整合,将SQL查询与Spark程序无缝混合,可以使用不同的语言进行代码开发

(2)统一的数据源访问,可以采用一种统一的方式去对接任意的外部数据源

val  dataFrame = sparkSession.read.文件格式的方法名("该文件格式的路径")

(3)兼容hive,sparksql可以支持hivesql语法  sparksql兼容hivesql

(4)支持标准的数据库连接,支持标准的数据库连接JDBC或者ODBC


2、DataFrame

        DataFrame是一种以RDD为基础的分布式数据集,类似于传统数据库的二维表格;DataFrame带有Schema元信息,即所表示的二维表数据集的每一列都带有名称和类型。

        RDD,具有面向对象编程的风格;而且开发时会进行类型的检查,保证编译时类型安全。但是RDD数据量比较大时,由于需要存储在堆内存中,堆内存有限,容易出现频繁的垃圾回收(GC);同时,RDD发送到其他服务器,序列化和反序列性能开销很大

        DataFrame引入了schema元信息和off-heap(堆外内存),大量的对象构建直接使用操作系统层面上的内存,堆内存就比较充足,不容易GC;同时,schema元信息代表数据结构的描述信息,可以省略掉对schema的序列化网络传输,只需对数据内容本身进行序列化,减小序列化和反序列性能开销。但是,编译时不会进行类型的检查,编译时类型不安全;不在具有面向对象编程的风格;

        DataSet是在Spark1.6中添加的新的接口,提供了强类型支持,在RDD的每行数据加了类型约束,可以用强大lambda函数,使用了Spark SQL优化的执行引擎。DataSet包含了DataFrame的功能,Spark2.0中两者统一,DataFrame表示为DataSet[Row],即DataSet的子集。修改了DataSet的缺陷,DataSet可以在编译时检查类型,并且是面向对象的编程接口。

RDD,存储在堆内存、序列化开销很大、编译时类型安全、面向对象编程。

DataFrame,引入了schema元信息和堆外内存,只需对数据内容本身进行序列化,编译时类型不安全、不具有面向对象编程风格。

DataSet,Spark1.6中提供了强类型支持,包含了DataFrame的功能,DataFrame表示为DataSet[Row]、编译时类型安全、面向对象编程。


3、 RDD DataFrameDataSet 

1RDD

优点:

        编译时类型安全、编译时就能检查出类型错误、面向对象的编程风格、直接通过类名点的方式来操作数据

缺点:

        序列化和反序列化的性能开销、无论是集群间的通信,还是IO操作都需要对对象的结构和数据进行序列化和反序列化。GC的性能开销,频繁的创建和销毁对象, 势必会增加GC

2DataFrame

        DataFrame引入了schema和off-heap

        schema : RDD每一行的数据, 结构都是一样的,这个结构就存储在schema中。 Spark通过schema就能够读懂数据, 因此在通信和IO时就只需要序列化和反序列化数据, 而结构的部分就可以省略了。

3DataSet

        DataSet结合了RDD和DataFrame的优点,并带来的一个新的概念Encoder。

        当序列化数据时,Encoder产生字节码与off-heap进行交互,能够达到按需访问数据的效果,而不用反序列化整个对象。Spark还没有提供自定义Encoder的API,但是未来会加入。


三者之间的转换:

(1)DataFrame与DataSet互转

        ·DataFrame转换成DataSet:val dataSet=dataFrame.as[强类型]

        ·DataSet转换成DataFrame:val dataFrame=dataSet.toDF

(1)DataFrame、DataSet与RDD互转:

        ·从dataFrame和dataSet获取得到rdd:

            val rdd1=dataFrame.rdd;val rdd2=dataSet.rdd

        ·RDD转换为DataFrame:

        方法一:反射机制,定义一个样例类,后期直接映射成DataFrame的schema信息;

        方法二:通过StructType直接指定Schema


3、DataFrame常用操作

DSL风格语法:sparksql中的DataFrame自身提供了一套自己的Api,可以去使用这套api来做相应的处理

(1) SQL风格语法

可以把DataFrame注册成一张表,然后通过sparkSession.sql(sql语句)操作

import org.apache.spark.SparkContext

import org.apache.spark.rdd.RDD

import org.apache.spark.sql.{Column, DataFrame, Row, SparkSession}


//todo:利用反射机制实现把rdd转成dataFrame

case class Person(id:String,name:String,age:Int)

object CaseClassSchema {

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

    //1、构建SparkSession对象

    val spark: SparkSession = SparkSession.builder().appName("CaseClassSchema").master("local[2]").getOrCreate()

    //2、获取sparkContext对象

    val sc: SparkContext = spark.sparkContext

    sc.setLogLevel("warn")

    //3、读取文件数据

    val data: RDD[Array[String]] = sc.textFile("E:\\person.txt").map(x=>x.split(" "))


方法一:反射机制,定义一个样例类,后期直接映射成DataFrame的schema信息

 //4、定义一个样例类

case class Person(id:String,name:String,age:Int)

    //5、将rdd与样例类进行关联

    val personRDD: RDD[Person] = data.map(x=>Person(x(0),x(1),x(2).toInt))

    //6、将rdd转换成dataFrame

    //需要手动导入隐式转换

    import spark.implicits._

    val personDF: DataFrame = personRDD.toDF


方法二:通过StructType直接指定Schema

//4、将rdd与Row对象进行关联

 val rowRDD: RDD[Row] = data.map(x=>Row(x(0),x(1),x(2).toInt))

//5、指定dataFrame的schema信息,这里指定的字段个数和类型必须要跟Row对象保持一致

  val schema=StructType(

        StructField("id",StringType)::

        StructField("name",StringType)::

        StructField("age",IntegerType)::Nil

    )

    val dataFrame: DataFrame = spark.createDataFrame(rowRDD,schema)



    //7、对dataFrame进行相应的语法操作

    //todo:----------------- DSL风格语法-----------------start

    //打印schema

    personDF.printSchema()

    //展示数据

    personDF.show()

    //获取第一行数据

    val first: Row = personDF.first()

    println("first:"+first)

    //取出前3位数据

    val top3: Array[Row] = personDF.head(3)

    top3.foreach(println)

    //获取name字段

    personDF.select("name").show()

    personDF.select($"name").show()

    personDF.select(new Column("name")).show()

    personDF.select("name","age").show()

    //实现age +1

    personDF.select($"name",$"age",$"age"+1).show()

    //按照age过滤

    personDF.filter($"age" >30).show()

    val count: Long = personDF.filter($"age" >30).count()

    println("count:"+count)

    //分组

    personDF.groupBy("age").count().show()

    personDF.show()

    personDF.foreach(row => println(row))

    //使用foreach获取每一个row对象中的name字段

    personDF.foreach(row =>println(row.getAs[String]("name")))

    personDF.foreach(row =>println(row.get(1)))

    personDF.foreach(row =>println(row.getString(1)))

    personDF.foreach(row =>println(row.getAs[String](1)))

    //todo:----------------- DSL风格语法--------------------end

    //todo:----------------- SQL风格语法-----------------start

    personDF.createTempView("person")

    //使用SparkSession调用sql方法统计查询

    spark.sql("select * from person").show

    spark.sql("select name from person").show

    spark.sql("select name,age from person").show

    spark.sql("select * from person where age >30").show

    spark.sql("select count(*) from person where age >30").show

    spark.sql("select age,count(*) from person group by age").show

    spark.sql("select age,count(*) as count from person group by age").show

    spark.sql("select * from person order by age desc").show

    //todo:----------------- SQL风格语法----------------------end

    //关闭sparkSession对象

    spark.stop()

  }

}

16、sparksql操作hivesql

添加依赖

        

            org.apache.spark

            spark-hive_2.11

            2.3.3

        

代码开发

import org.apache.spark.sql.SparkSession

//todo:利用sparksql操作hivesql

object HiveSupport {

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

    //1、构建SparkSession对象

    val spark: SparkSession = SparkSession.builder().appName("HiveSupport") .master("local[2]")

      .enableHiveSupport()//开启对hive的支持

      .getOrCreate()


    //2、直接使用sparkSession去操作hivesql语句

      //2.1创建一张hive表

       spark.sql("create table people(id string,name string,age int) row format delimited fields terminated by ','")

      //2.2加载数据到hive表中

       spark.sql("load data local inpath './data/kaikeba.txt' into table people ")

      //2.3查询

      spark.sql("select * from people").show()

    spark.stop()

  }

}


添加mysql连接驱动jar包

    mysql

    mysql-connector-java

    5.1.38

代码开发

import java.util.Properties

import org.apache.spark.sql.{DataFrame, SparkSession}

//todo:通过sparksql把结果数据写入到mysql表中

object Data2Mysql {

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

    //1、创建SparkSession

    val spark: SparkSession = SparkSession .builder().appName("Data2Mysql")  .getOrCreate()


    //2、读取mysql表中数据

        //2.1定义url连接

        val url="jdbc:mysql://node03:3306/spark"

        //2.2定义表名

        val table="user"

        //2.3定义属性

        val properties=new Properties()

        properties.setProperty("user","root")

        properties.setProperty("password","123456")

    val mysqlDF: DataFrame = spark.read.jdbc(url,table,properties)

    //把dataFrame注册成一张表

      mysqlDF.createTempView("user")

    //通过sparkSession调用sql方法

       //需要统计经度和维度出现的人口总数大于1000的记录 保存到mysql表中

    val result: DataFrame = spark.sql("select * from user where age >30")

    //保存结果数据到mysql表中

    //mode:指定数据的插入模式

        //overwrite:表示覆盖,如果表不存在,事先帮我们创建

        //append   :表示追加, 如果表不存在,事先帮我们创建

        //ignore   :表示忽略,如果表事先存在,就不进行任何操作

        //error    :如果表事先存在就报错(默认选项)


     result.write.mode(args(0)).jdbc(url,args(1),properties)

    //关闭

     spark.stop()

  }

}

提交任务脚本

spark-submit \

--master spark://node01:7077 \

--class com.kaikeba.sql.Data2Mysql \

--executor-memory 1g \

--total-executor-cores 4 \

--driver-class-path /home/hadoop/mysql-connector-java-5.1.38.jar \

--jars /home/hadoop/mysql-connector-java-5.1.38.jar \

spark_class02-1.0-SNAPSHOT.jar \

append  kaikeba


4、sparksql中自定义函数(★★★★★)


import org.apache.spark.sql.api.java.UDF1

import org.apache.spark.sql.types.StringType

import org.apache.spark.sql.{DataFrame, SparkSession}

//TODO:自定义sparksql的UDF函数    一对一的关系

object SparkSQLFunction {

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

    //1、创建SparkSession

    val sparkSession: SparkSession = SparkSession.builder().appName("SparkSQLFunction").master("local[2]").getOrCreate()

 //2、构建数据源生成DataFrame

    val dataFrame: DataFrame = sparkSession.read.text("E:\\data\\test_udf_data.txt")

 //3、注册成表

    dataFrame.createTempView("t_udf")

    //4、实现自定义的UDF函数

    //小写转大写

    sparkSession.udf.register("low2Up",new UDF1[String,String]() {

      override def call(t1: String): String = {

        t1.toUpperCase

      }

    },StringType)

    //大写转小写

    sparkSession.udf.register("up2low",(x:String)=>x.toLowerCase)

    //4、把数据文件中的单词统一转换成大小写

    sparkSession.sql("select  value from t_udf").show()

    sparkSession.sql("select  low2Up(value) from t_udf").show()

    sparkSession.sql("select  up2low(value) from t_udf").show()

    sparkSession.stop()

  }

}

5、sparksql整合hive

1、把hive目录下的hive-site.xml,拷贝到每一个spark的conf文件夹中

2把连接mysql驱动的jar包,拷贝到spark的jars文件夹中

可以使用spark-sql脚本 后期执行sql相关的任务

启动脚本

spark-sql \

--master spark://node01:7077 \

--executor-memory 1g \

--total-executor-cores 4 \

--conf spark.sql.warehouse.dir=hdfs://node01:8020/user/hive/warehouse

应用场景

#!/bin/sh

#定义sparksql提交脚本的头信息

SUBMITINFO="spark-sql --master spark://node01:7077 --executor-memory 1g --total-executor-cores 4 --conf spark.sql.warehouse.dir=hdfs://node01:8020/user/hive/warehouse" 

#定义一个sql语句

SQL="select * from default.hive_source;" 

#执行sql语句   类似于 hive -e sql语句

echo "$SUBMITINFO" 

echo "$SQL"

$SUBMITINFO -e "$SQL"

6、sparkSql输出jdbc

val properties=new Properties()

properties.setProperty("user","root")

properties.setProperty("password","123456")

val mysqlDF: DataFrame = spark.read.jdbc(url,table,properties) 

//dataFrame注册成一张表

mysqlDF.createTempView("user")


//保存结果数据到mysql表中

result.write.mode("append").jdbc(url,"kaikeba",properties)

//mode:指定数据的插入模式

//overwrite: 表示覆盖,如果表不存在,事先帮我们创建

//append :表示追加, 如果表不存在,事先帮我们创建

//ignore :表示忽略,如果表事先存在,就不进行任何操作

//error :如果表事先存在就报错(默认选项) //关闭



 五、 saprk Sreaming


1、DStream

离散数据流

        一个DStream以一系列连续的RDDs所展现,其中的每个RDD都包含来自一定间隔的数据,在DStream上使用的任何操作都会转换为针对底层RDD的操作。

scala版本

object WordCount {

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

    //步骤一:初始化程序入口

    val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")

    val ssc = new StreamingContext(conf, Seconds(1))//间隔1s的数据,形成rdd

    //步骤二:获取数据流

    val lines = ssc.socketTextStream("localhost", 9999)

    //步骤三:数据处理

    val words = lines.flatMap(_.split(" "))

    val pairs = words.map(word => (word, 1))

    val wordCounts = pairs.reduceByKey(_ + _)

   //步骤四: 数据输出

    wordCounts.print()

    //步骤五:启动任务

    ssc.start()

    ssc.awaitTermination()

    ssc.stop()

  }

}


2、Transformation 高级算子

updateStateByKeymapWithStateTransformWindow


 (1)updateStateByKey

/       *数据的处理

      * Option:

      *   Some:有值;None:没有值

      *   values:Seq[Int]   List{1,1}

      *   state:Option[Int]上一次这个单词出现了多少次  None  Some 2

      */


    val wordCountDStream = dstream.flatMap(_.split(","))

      .map((_, 1))

      .updateStateByKey((values: Seq[Int], state: Option[Int]) => {

        val currentCount = values.sum

        val lastCount = state.getOrElse(0)

        Some(currentCount + lastCount)

      })

(2)mapWithState

/**

  *性能更好

  */

    // currentBatchTime :表示当前的Batch的时间

    // key:表示需要更新状态的key

    // value:表示当前batch的对应的key的对应的值

    // currentState:对应key的当前的状态

    val stateSpec = StateSpec.function((currentBatchTime: Time, key: String, value: Option[Int], currentState: State[Long]) => {

      val sum = value.getOrElse(0).toLong + currentState.getOption.getOrElse(0L)

      val output = (key, sum)

      if (!currentState.isTimingOut()) {

        currentState.update(sum)

      }

      Some(output)

    }).initialState(initialRDD).numPartitions(2).timeout(Seconds(30))

 //timeout:当一个key超过这个时间没有接收到数据的时候,这个key以及对应的状态会被移除掉

    val result = wordsDStream.mapWithState(stateSpec)


(3)Transform


    //transform需要有返回值,必须类型是RDD

    val wordCountDStream = wordOneDStream.transform(rdd => {

      val filterRDD: RDD[(String, Boolean)] = rdd.sparkContext.parallelize(blackListBroadcast.value)

      val resultRDD: RDD[(String, (Int, Option[Boolean]))] = rdd.leftOuterJoin(filterRDD)

           resultRDD.filter(tuple => {

        tuple._2._2.isEmpty

      }).map(_._1)

    }).map((_, 1)).reduceByKey(_ + _)


(4) Window操作

  *实现一个 每隔4秒,统计最近6秒的单词计数的情况。

      *数据的处理

      *我们一直讲的是数据处理的算子

      *这个地方算子 就是生产时候用的算子。

      *

      *  reduceFunc: (V, V) => V,

         windowDuration: Duration,6窗口的大小

         slideDuration: Duration,4滑动的大小

         numPartitions: Int指定分区数

      */

    val resultWordCountDStream = dstream.flatMap(_.split(","))

      .map((_, 1))

      .reduceByKeyAndWindow((x: Int, y: Int) => x + y, Seconds(6), Seconds(4))

  }

}


(5)foreachRDD

核心算子讲解


    //将结果保存到Mysql(二)

    wordCounts.foreachRDD { (rdd, time) =>

      rdd.foreach { record =>

        Class.forName("com.mysql.jdbc.Driver")

        val conn = DriverManager.getConnection("jdbc:mysql://master:3306/test", "root", "root")

        val statement = conn.prepareStatement(s"insert into wordcount(ts, word, count) values (?, ?, ?)")

        statement.setLong(1, time.milliseconds)

        statement.setString(2, record._1)

        statement.setInt(3, record._2)

        statement.execute()

        statement.close()

        conn.close()

      }

    }


    //将结果保存到Mysql(七)

    wordCounts.foreachRDD { (rdd, time) =>

      rdd.foreachPartition { partitionRecords =>

        val conn = ConnectionPool.getConnection

        conn.setAutoCommit(false)

        val statement = conn.prepareStatement(s"insert into wordcount(ts, word, count) values (?, ?, ?)")

        partitionRecords.zipWithIndex.foreach { case ((word, count), index) =>

          statement.setLong(1, time.milliseconds)

          statement.setString(2, word)

          statement.setInt(3, count)

          statement.addBatch()

          if (index != 0 && index % 500 == 0) {

            statement.executeBatch()

            conn.commit()

          }

        }

        statement.executeBatch()

        statement.close()

        conn.commit()

        conn.setAutoCommit(true)

        ConnectionPool.returnConnection(conn)

      }

    }


(6) Checkpoint

/**

  * Dirver HA

  */

object DriverHAWordCount {

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

    val checkpointDirectory:String="hdfs://hadoop1:9000/streamingcheckpoint2";

    def functionToCreateContext(): StreamingContext = {

      val conf = new SparkConf().setMaster("local[2]").setAppName("NetWordCount")

      val sc = new SparkContext(conf)

      val ssc = new StreamingContext(sc,Seconds(2))

      ssc.checkpoint(checkpointDirectory)

      val dstream: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop1",9999)

      val wordCountDStream = dstream.flatMap(_.split(","))

        .map((_, 1))

        .updateStateByKey((values: Seq[Int], state: Option[Int]) => {

          val currentCount = values.sum

          val lastCount = state.getOrElse(0)

          Some(currentCount + lastCount)

        })

      wordCountDStream.print()

      ssc.start()

      ssc.awaitTermination()

      ssc.stop()

      ssc

    }

    val ssc = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _)

    ssc.start()

    ssc.awaitTermination()

    ssc.stop()

  }

}

3、SparkStreaming和SparkSQL整合

pom.xml里面添加

      

            org.apache.spark

            spark-sql_2.11

            2.2.1

      


  WordCount程序,Spark Streaming消费TCP Server发过来的实时数据的例子:

  1、在master服务器上启动一个Netcat server

`$ nc -lk 9998` (如果nc命令无效的话,我们可以用yum install -y nc来安装nc)

object NetworkWordCountForeachRDDDataFrame {

  def main(args: Array[String]) {

    val sparkConf = new SparkConf().setAppName("NetworkWordCountForeachRDD")

    val sc = new SparkContext(sparkConf)

    // Create the context with a 1 second batch size

    val ssc = new StreamingContext(sc, Seconds(1))

    //创建一个接收器(ReceiverInputDStream),这个接收器接收一台机器上的某个端口通过socket发送过来的数据并处理

    val lines = ssc.socketTextStream("master", 9998, StorageLevel.MEMORY_AND_DISK_SER)

    //处理的逻辑,就是简单的进行word count

    val words = lines.flatMap(_.split(" "))

    //将RDD转化为Dataset

    words.foreachRDD { rdd =>

      // Get the singleton instance of SparkSession

      val spark = SparkSession.builder.config(rdd.sparkContext.getConf).getOrCreate()

      import spark.implicits._

      // Convert RDD[String] to DataFrame

      val wordsDataFrame = rdd.toDF("word")

      // Create a temporary view

      wordsDataFrame.createOrReplaceTempView("words")

      // Do word count on DataFrame using SQL and print it

      val wordCountsDataFrame =

        spark.sql("select word, count(*) as total from words group by word")

      wordCountsDataFrame.show()

    }

    //启动Streaming处理流

    ssc.start()

    ssc.stop(false)

    //将RDD转化为Dataset

    words.foreachRDD { (rdd, time) =>

      // Get the singleton instance of SparkSession

      val spark = SparkSession.builder.config(rdd.sparkContext.getConf).getOrCreate()

      import spark.implicits._

      // Convert RDD[String] to DataFrame

      val wordsDataFrame = rdd.toDF("word")

      // Do word count on DataFrame using SQL and print it

      val wordCountsDataFrame = wordsDataFrame.groupBy("word").count()

      val resultDFWithTs = wordCountsDataFrame.rdd.map(row => (row(0), row(1), time.milliseconds)).toDF("word", "count", "ts")

      resultDFWithTs.write.mode(SaveMode.Append).parquet("hdfs://master:9999/user/spark-course/streaming/parquet")

    }

    //等待Streaming程序终止

    ssc.awaitTermination()

  }

}

6、SparkStreaming消费kafka

一、基于Receiver的方式

        这种方式使用Receiver来获取数据。Receiver是使用Kafka的高层次Consumer API来实现的。receiver从Kafka中获取的数据都是存储在Spark Executor的内存中的(如果突然数据暴增,大量batch堆积,很容易出现内存溢出的问题),然后Spark Streaming启动的job会去处理那些数据。

        然而,在默认的配置下,这种方式可能会因为底层的失败而丢失数据。如果要启用高可靠机制,让数据零丢失,就必须启用Spark Streaming的预写日志机制(Write Ahead Log,WAL)。该机制会同步地将接收到的Kafka数据写入分布式文件系统(比如HDFS)上的预写日志中。所以,即使底层节点出现了失败,也可以使用预写日志中的数据进行恢复。

二、基于Direct的方式

        这种新的不基于Receiver的直接方式,是在Spark 1.3中引入的,从而能够确保更加健壮的机制。替代掉使用Receiver来接收数据后,这种方式会周期性地查询Kafka,来获得每个topic+partition的最新的offset,从而定义每个batch的offset的范围。当处理数据的job启动时,就会使用Kafka的简单consumer api来获取Kafka指定offset范围的数据。

优点如下

    简化并行读取:如果要读取多个partition,不需要创建多个输入DStream然后对它们进行union操作。Spark会创建跟Kafka partition一样多的RDD partition,并且会并行从Kafka中读取数据。所以在Kafka partition和RDD partition之间,有一个一对一的映射关系。

    高性能:如果要保证零数据丢失,在基于receiver的方式中,需要开启WAL机制。这种方式其实效率低下,因为数据实际上被复制了两份,Kafka自己本身就有高可靠的机制,会对数据复制一份,而这里又会复制一份到WAL中。而基于direct的方式,不依赖Receiver,不需要开启WAL机制,只要Kafka中作了数据的复制,那么就可以通过Kafka的副本进行恢复。

一次且仅一次的事务机制

、对比:

        基于receiver的方式,是使用Kafka的高阶API来在ZooKeeper中保存消费过的offset的。这是消费Kafka数据的传统方式。这种方式配合着WAL机制可以保证数据零丢失的高可靠性,但是却无法保证数据被处理一次且仅一次,可能会处理两次。因为Spark和ZooKeeper之间可能是不同步的。

        基于direct的方式,使用kafka的简单api,Spark Streaming自己就负责追踪消费的offset,并保存在checkpoint中。Spark自己一定是同步的,因此可以保证数据是消费一次且仅消费一次。

        在实际生产环境中大都用Direct方式

7、简述SparkStreaming窗口函数的原理(重点)

    窗口函数就是在原来定义的SparkStreaming计算批次大小的基础上,再次进行封装,每次计算多个批次的数据,同时还需要传递一个滑动步长的参数,用来设置当次计算任务完成之后下一次从什么地方开始计算。

        图中time1就是SparkStreaming计算批次大小,虚线框以及实线大框就是窗口的大小,必须为批次的整数倍。虚线框到大实线框的距离(相隔多少批次),就是滑动步长。

8、手写出wordcount代码实现(Scala)

 val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("WordCount")

 val sc = new SparkContext(conf)

 sc.textFile("/input").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).saveAsTextFile("/output")

 sc.stop()

9、Spark实现topN的获取(描述思路或使用伪代码)(重点)

方法1:

(1)按照key对数据进行聚合(groupByKey)

(2)将value转换为数组,利用scala的sortBy或者sortWith进行排序(mapValues)数据量太大,会OOM。

方法2:

(1)取出所有的key

(2)对key进行迭代,每次取出一个key利用spark的排序算子进行排序

方法3:

(1)自定义分区器,按照key进行分区,使不同的key进到不同的分区

(2)对每个分区运用spark的排序算子进行排序

10、京东:调优之前与调优之后性能的详细对比(例如调整map个数,map个数之前多少、之后多少,有什么提升)

这里举个例子。比如我们有几百个文件,会有几百个map出现,读取之后进行join操作,会非常的慢。这个时候我们可以进行coalesce操作,比如240个map,我们合成60个map,也就是窄依赖。这样再shuffle,过程产生的文件数会大大减少。提高join的时间性能。

你可能感兴趣的:(4、Spark概要)