目录
在spark-shell中使用RDD配合样例类,转换成为DataFrame
通过JavaAPI编写SparkSQL程序,读取文本文件创建DataFrame
sparkSQL支持spark on hive,使用sparkSession直接操作hql语句
Spark是什么
Spark是一个开源的类似于Hadoop MapReduce的通用的并行计算框架,Spark基于map reduce算法实现的分布式计算,拥有Hadoop MapReduce所具有的优点;但不同于MapReduce的是Spark是基于内存计算的大数据并行计算框架,它的Job中间输出和结果可以保存在内存中,从而不再需要读写HDFS。因为Spark基于内存计算,所以比Hadoop中MapReduce计算框架具有更高的实时性,同时保证了高效容错性和可伸缩性,能更好地适用于数据挖掘与机器学习等需要迭代的map reduce的算法。
Spark是MapReduce的替代方案,而且兼容HDFS、Hive,可融入Hadoop的生态系统,以弥补MapReduce的不足。
spark core当中的RDD
spark core实现了spark的基本功能,包括任务调度、内存管理、错误恢复与存储系统交互等模块。spark core中还包含了对RDD的定义
RDD(Resilient Distributed Dataset)弹性分布式数据集,是Spark的基石,是实现Spark数据处理的核心抽象。
spark core的操作对象是SparkContext 。
以spark当中单词计数统计为例解释RDD的五大属性
sc.textFile("file:///export/servers/sparkdatas/wordcount.txt").flatMap(x => x.split(" ").map(x => (x,1)). reduceByKey( _ +_ ).saveAsTextFile("file:///export/servers/sparkdatas/out_wordcount")
1. 分区列表
一个RDD就是一个分布式对象集合,本质上是一个只读的分区记录集合,每个RDD可以分成多个分区,每个分区就是一个数据集片段(HDFS上的数据块),并且一个RDD的不同分区可以被保存到集群中不同的节点上,从而可以在集群中的不同节点上进行并行计算。
图中三个block数据块的数据分别属于RDD的三个分区。
2. 作用函数
Spark中RDD的计算是以分区为单位的,每个RDD都会实现算子以达到这个目的。算子会对迭代器进行复合,不需要保存每次计算的结果。
图中textFile、flatMap、map、reduceByKey以及saveAsTextFile都是RDD的算子,也叫作用函数。
3. 依赖关系
不同RDD通过转换操作形成依赖关系,可以实现管道化,中间结果持久化到内存而非落地存储,避免了不必要的读写磁盘开销。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算,实现了Spark的容错性。
图中的RDD依赖关系,也称为血统关系,由父到子依次为 rdd1 ==> rdd2 ==> rdd3 ==> rdd4 ,假设rdd3数据丢失,可以利用rdd2.map来恢复rdd3的数据,体现容错性。
4. 分区函数
当前Spark中实现了两种类型的分区函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于于key-value的RDD,才会有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函数不但决定了RDD本身的分区数量,也决定了parent RDD Shuffle输出时的分区数量。
图中rdd3有键值对,在进行saveAsTextFile作用函数的时候进行了shuffle,把所有分区的数据进行汇总并重新输出分区。比如rdd4的0号分区的数据实际上是rdd3的所有三个分区的数据汇总后切分而来。
5. 位置优先性
按照“移动数据不如移动计算”的理念,Spark在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理的数据块的存储位置。文件在哪一台服务器上面,就在哪一台服务器上面启动task进行运算,尽量避免数据的拷贝。
图中:数据块block1,2都存储在node01服务器上,就在node01服务器上启动任务task对block1,2数据块进行处理;block3在node02服务器上,就在node02服务器上启动任务task对block3数据块进行处理。
rdd的创建三种方式
1. 由外部存储文件创建,包括本地的文件系统,还有所有Hadoop支持的数据集,比如HDFS、Cassandra、HBase等。
2. 由集合创建
3. 由已经存在的rdd转化而来
rdd的算子的介绍以及算子使用
算子分两大类
transfermation:转换算子,所有的转换算子都是懒执行的,不会马上去执行,只是记录了血统关系 。如果返回值是RDD就一定是转换算子
action:动作算子,触发整个任务真正的去执行
常用算子
转换算子
map:返回一个新的RDD,该RDD由每一个输入元素经过参数函数转换后组成。
mapPartitions:类似于map,区别是以分区为单位处理数据,效率更高 。
filter:返回一个新的RDD,该RDD由经过参数函数计算后返回值为true的输入元素组成。
flatMap:类似于map,但是每一个输入元素可以被映射为0或多个输出元素,所以每个元素经参数函数计算后应该返回一个序列,而不是单一元素。
union:对源RDD和参数RDD求并集后返回一个新的RDD。
intersection:对源RDD和参数RDD求交集后返回一个新的RDD。
distinct:对源RDD进行去重后返回一个新的RDD。默认情况下是8个并行任务来操作,可以传入一个可选的numTasks参数设置并行任务数量。
partitionBy:对RDD进行分区操作,手动指定分区数,如果原有的partionRDD和现有的partionRDD是一致的话就不进行分区,否则会生成ShuffleRDD。
reduceByKey:在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用第一个参数函数将相同key的值聚合到一起,使用第二个参数设置reduce任务的个数。效率更高,先部分聚合,再shuffle。
groupByKey:类似于reduceByKey,区别是直接shuffle后再聚合。效率低下,尽量不要用。
sortByKey:在一个(K,V)的RDD上调用,返回一个按照key进行排序的(K,V)的RDD。
sortBy:类似于sortByKey,但是更灵活,可以用参数函数先对数据进行处理,按照处理后的数据比较结果排序。
join:在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD。
coalesce:缩减分区数,用于大数据集过滤后,提高小数据集的执行效率,不会产生shuffle过程。
mapValues:针对于(K,V)形式的类型只对V进行操作。
动作算子
saveAsTextFile:将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本。
saveAsSequenceFile:将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,该目录可以是HDFS或者其他Hadoop支持的文件系统。
DAG的划分以及shuffle过程
DAG(Directed Acyclic Graph)有向无环图,其实就是根据rdd之间的依赖关系来生成的。DAG的生成主要是为了划分stage(调度阶段)。
rdd1 ==> rdd2 ==> rdd3 ==> rdd4
窄依赖是父RDD的分区只被一个子RDD依赖,宽依赖是父RDD的分区被多个子RDD依赖。宽依赖存在shuffle过程。
Spark查询为懒执行,当执行到action算子时开始反向推算。stage根据宽窄依赖来划分,从后往前回溯,遇到窄依赖,加入当前stage,遇到宽依赖,重新开始一个stage。stage的划分最终依据就是shuffle过程,与MapReduce的shuffle类似,都是做数据的分发。
spark当中shuffle过程
负责shuffle过程的执行、计算和处理的组件主要就是ShuffleManager,即shuffle管理器。
spark1.2版本之前,使用的shuffle类叫做HashShuffleManager 。会产生大量的中间结果小文件,进而由大量的磁盘IO操作影响了性能。
spark1.2版本之后,使用SortShuffleManager替代HashShuffleManager。SortShuffleManager将大量临时磁盘文件合并(merge)成一个磁盘文件,因此每个分区Task就只有一个磁盘文件。同时,SortShuffleManager创建一个单独的索引文件,在下一个stage的shuffle read task拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据即可。
SortShuffleManager又分为两种机制:普通机制和bypass机制。
bypass运行机制的条件:
1)分区数量小于spark.shuffle.sort.bypassMergeThreshold参数的值,默认是200。
2)非聚合类的shuffle算子。
分区的个数小于等于200,启用bypass机制,默认不会对数据进行排序的操作,直接将数据写入到内存缓冲中。写机制和HashShuffle一样,上游task会为每个下游task都创建一个临时磁盘文件,并将数据按key进行hash然后根据key的hash值,将key写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后,以batch的形式(每批一万条数据)溢写到磁盘文件的。不同的是,最后会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件,等待下游task拉取数据。
其他情况则启用普通机制,与bypass机制的区别在于,输出到内存缓冲之前多了两个步骤:
1)在写入默认5M大小的内存结构,如果是reduceByKey这种聚合类的shuffle算子,那么会选用Map数据结构,一边通过Map进行聚合,一边写入内存;如果是join这种普通的shuffle算子,那么会选用Array数据结构,直接写入内存。
2)内存数据溢写到磁盘之前要先排序。
bypass不需要进行数据的排序操作,也就节省掉了这部分的性能开销,所以性能比普通机制好。
spark的任务调度
在使用spark-shell等交互式运行或者使用官方提供的run-example实例时,Driver运行在Master节点中;若是客户端使用spark-submit工具进行任务的提交,或者使用IDEA等工具开发运行任务时,Driver是运行在本地客户端的。Driver负责运行Application的main函数并创建SparkContext,SparkContext的目的是为了准备Spark应用程序的运行环境。SparkContext负责资源的申请、任务分配和监控等。当Executor运行结束后,Driver负责关闭SparkContext。
第一步:客户端提交Application
第二步:启动Driver,运行Application 并创建SparkContext
第三步:SparkContext负责计算RDD之间的依赖关系,构建DAG
第四步:当遇到一个Action操作后就会触发一个Job(作业)的计算,SparkContext调用DAGScheduler把DAG划分多个stage,每个stage对应一个taskSet
第五步:DAGScheduler将Task信息(分区信息以及方法等)序列化并打包成taskSet发给taskScheduler(任务调度器),
第六步:taskScheduler将接收到的taskset分解成为一个个的task,将task分配给各个工作节点(Worker Node)上的executor去执行
第七步:Executor运行结束后,Driver关闭SparkContext
SparkSQL
sparkSQL是spark的一个处理结构化数据的模块,与hive功能类似,通过sql语句的方式可以实现结构化数据分析 。它是将Spark SQL转换成RDD,然后提交到集群中去运行,执行效率非常快!
SparkStreaming的数据抽象是DataFrame和DataSet。与RDD类似,DataFrame也是一个分布式数据容器,然而DataFrame更像传统数据库的二维表格,除了数据以外,还记录数据的结构信息,即schema。DataSet是Dataframe API的一个扩展,是Spark最新的数据抽象。DataSet是强类型的,知道每个字段的类型。而Dataframe只知道字段不知道类型,就相当于DataSet[Row]。
SparkStreaming的操作对象是SparkSession。
在spark-shell中使用RDD配合样例类,转换成为DataFrame
读取文本文件成为一个RDD
将RDD给切开,然后将数据转换成为RDD[CASE Class]
将RDD[case class] 转换成为DataFrame
// 读取文本文件成为一个RDD,并将RDD切开
val lineRDD = sc.textFile("file:///export/servers/person.txt").map(x => x.split(" "))
// 定义 case class样例类
case class Person(id:Int,name:String,age:Int)
// 关联RDD 与 case class,将数据转换成为RDD[CASE Class]
val personRDD = lineRDD.map(x => Person(x(0).toInt,x(1),x(2).toInt))
// 将RDD[CASE Class]转换成为DataFrame
val personDF = personRDD.toDF
dataFrame的常用的操作
DSL风格语法
// 1 查看DataFrame当中的数据
personDF.show
// 2 查看DataFram当中部分字段的数据
personDF.select(personDF.col("name")).show
personDF.select("name").show
personDF.select(col("name"), col("age")).show
personDF.select($"name", $"age").show
// 3 打印DataFrame的Schema信息
personDF.printSchema
//4 查询所有的name和age,并将age+1
personDF.select(col("id"), col("name"), col("age") + 1).show
personDF.select(personDF("id"), personDF("name"), personDF("age") + 1).show
personDF.select($"name",$"age",$"age" + 1).show
// 5 过滤age大于等于25
personDF.filter(col("age") >= 25).show
personDF.filter($"age" > 25).show
// 6 统计年龄大于30岁的人数
personDF.filter(col("age")>30).count()
// 7 按年龄进行分组并统计相同年龄的人数
personDF.groupBy("age").count().show
SQL风格语法
// 1 将DataFrame注册成表
personDF.registerTempTable("person")
// 2 使用sql语句实现数据的查询
// 2.1 查询年龄最大的前两名
spark.sql("select * from person order by age desc limit 2").show
// 2.2 显示表的Schema信息
spark.sql("desc person").show
// 2.3 查询年龄大于30的人的信息
spark.sql("select * from person where age > 30").show
通过JavaAPI编写SparkSQL程序,读取文本文件创建DataFrame
第一种方式:使用RDD配合样例类
利用反射机制,推导包含某种类型的RDD,通过反射将其转换为指定类型的DataFrame,适用于提前知道RDD的schema。
import org.apache.spark.SparkContext
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, SparkSession}
case class Person(id:Int,name:String,age:Int)
object SparkDF {
/**
* 需求:读取文件,生成RDD,配合样例类,将RDD转换成为DF
*/
def main(args: Array[String]): Unit = {
//读取文件成为RDD,对RDD进行切割
//定义样例类,将RDD转换成为 RDD[case class]
//将rdd 转换成为 df
//获取sparkSession对象
val sparkSession: SparkSession = SparkSession.builder().appName("sparkSQL").master("local[2]").getOrCreate()
//通过sparkContext来读取文件
val sparkContext: SparkContext = sparkSession.sparkContext
sparkContext.setLogLevel("WARN")
//读取文件
val fileRDD: RDD[String] = sparkContext.textFile("file:///F:\\person.txt")
//对rdd进行切割
val map: RDD[Array[String]] = fileRDD.map(x => x.split(" "))
val personRDD: RDD[Person] = map.map(x => Person(x(0).toInt,x(1),x(2).toInt))
//需要导入隐式转换的包
import sparkSession.implicits._
//将RDD转换成为了DF 得到personDF
val personDF: DataFrame = personRDD.toDF()
sparkContext.stop()
sparkSession.close()
}
}
第二种方式:使用Row对象配合StructType
通过编程接口StructType与RDD进行交互获取schema,并动态创建DataFrame,在运行时决定列及其类型。
// 获取sparkSesssion对象,通过sparkContext读取文件的过程略去
// 对rdd进行切割
val map: RDD[Array[String]] = fileRDD.map(x => x.split(" "))
val rowRDD: RDD[Row] = map.map(x => Row(x(0).toInt,x(1),x(2).toInt))
val structType: StructType = new StructType().add("id",IntegerType).add("name",StringType).add("age",IntegerType)
// 给两个参数 RDD[Row] StructType 将RDD转换成DF
val dataFrame: DataFrame = sparkSession.createDataFrame(rowRDD,structType)
sparkSQL支持spark on hive,使用sparkSession直接操作hql语句
HiveContext对应spark-hive项目,与hive有部分耦合,支持hql,是SqlContext的子类。在Spark2.0之后,HiveContext和SqlContext在SparkSession进行了统一,可以通过操作SparkSession来操作HiveContext和SqlContext。
举例:创建hive表,加载student.csv,进行数据分析。
import org.apache.spark.SparkContext
import org.apache.spark.sql.SparkSession
object SparkHive {
def main(args: Array[String]): Unit = {
//通过spark使用hive的语法,直接创建hive表,然后加载数据即可
//获取sparkSession
val sparkSession: SparkSession = SparkSession.builder().master("local[2]").appName("sparkHive").enableHiveSupport() //开启对hive语法的支持
.getOrCreate()
val context: SparkContext = sparkSession.sparkContext
context.setLogLevel("WARN")
//创建表
sparkSession.sql("create table if not exists student (id int,name string,age int) row format delimited fields terminated by ',' ")
//加载数据
sparkSession.sql("load data local inpath './datas/student.csv' overwrite into table student")
//查看表
sparkSession.sql("select * from student").show()
context.stop()
sparkSession.close()
}
}
Spark Streaming
sparkStreaming是spark当中用于实时处理的一个模块,是流式批处理引擎,主要是用于做一些实时处理的功能,类似于storm,其基本原理是把输入数据以某一时间间隔批量的处理,当批处理间隔缩短到秒级时,便可以用于处理实时数据流。sparkStreaming支持各种各样的数据源,例如kakfa,flume,tcp套接字。
SparkStreaming的数据抽象是DStream,代表持续性的数据流和经过各种Spark算子操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。
SparkStreaming的操作对象是StreamingContext 。
kafka数据源
0.8版本整合,有两种数据接收方式
ReceiverDStream:使用使用kafka的高阶API进行消费,offset保存在zk当中,使用at least once消费模式,会造成数据的重复消费。数据重复消费的原因是 消费者每隔一段时间自动提交一次offset到zk当中去保存,如果一批数据刚消费完,消费者就宕机了,offset没来得及提交到zk中,那么等到消费者恢复消费的时候,会重复消费zk中没来得及记录的offset对应的数据。
DirectDStream:使用kafka的低阶API进行消费,offset自动保存在kafka的topic里面,使用at most once消费模式,会造成数据丢失。数据丢失的原因是 消费者消费到了这个消息,先自动提交offset给kafka的topic,再处理这个消息,如果这时候消费者没来得及处理这个消息就宕机了,那么等到消费者恢复消费的时候按照kafka中topic的最新offset进行消费,这条消息就会丢失。0.8版本也可以设置手动提交offset,但是很麻烦。
0.10版本整合,有一种数据接收方式
DirectDStream:使用lowLevelAPI进行消费,offset保存在topic里面,配合手动提交offset,使用exactly once消费模式,实现数据消费且仅消费一次。可以关闭自动提交,在数据消费完后手动提交offset给Kafka的topic,可以避免数据丢失。如果数据刚消费完,没来得及提交offset,消费者就宕机了,那么等到消费者恢复消费的时候也会重复这条消息,但是重复消费的概率和数据量都比0.8版本的高阶API小。