[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WatTekGu-1614914949331)(C:\Users\Thinkpad\AppData\Roaming\Typora\typora-user-images\1614155080812.png)]
sparkcore前面
spark的rdd是核心,用于sparksql(dataframe、dataset),sparkstreaming和机器学习;
spark:
- 基于内存的快速、通用、可扩展的大数据分析计算引擎
- 迭代计算不落盘,但是遇到shuffle操作仍然会落盘
特点:(mr切片大小默认和块大小一致,128M==决定maptask;分区数决定reducetask数目,maptask和reducetask都是进程)
- 快、spark基于内存的运算,快100倍,基于硬盘的也快10倍;实现了DAG执行引擎,通过基于内存来高效处理数据流。计算的中间结果是存在于内存中
- 易用:支持java、scala、Python的api;
- 通用:spark可以用于交互式查询sparksql、实时流处理sparkstreaming、机器学习和图计算;
- 兼容性:spark可以使用yarn和mesos作为资源管理器和调度器
spark运行模式:
本地模式local:
bin/spark-submit \
–class org.apache.spark.examples.SparkPi
–master local[*] \(线程数)
./examples/jars/spark-examples_2.11-2.4.0.jar
10本质:bin/spark-submit \ + 主类+jar包路径+本地模式+参数
====
- 获取sparkcontext上下文对象!
- sc.textFile(dir)将数据封存到rdd里面,即此时数据类型即为rdd,rdd里面也有常见的数据类型
- 只是封装了思维逻辑而不是真正的数据,需要触发行动算子
standalone模式:
bin/spark-submit \
–class org.apache.spark.examples.SparkPi
–master spark://Centos2:7077
./examples/jars/spark-examples_2.11-2.4.0.jar
10总结:bin/spark-submit + jar路径 + 主类 + 模式(–master) + 参数
其他一些参数:
bin/spark-submit \
–class org.apache.spark.examples.SparkPi
–master spark://Centos2:7077
–executor-memory 2G
–total-executor-cores 2
./examples/jars/spark-examples_2.11-2.4.0.jar
10–executor-memory 2G 表示一个executor分配2G内存,默认1G
–total-executor-cores 2 表示总共分配2个CPU
类型:standalone-client和standalone-cluster,主要区别是driver程序的运行节点!!!
客户端模式:
bin/spark-submit \
–class org.apache.spark.examples.SparkPi
–master spark://Centos2:7077
–deploy-mode client \ #指明client模式,另外一个是cluster
./examples/jars/spark-examples_2.11-2.4.0.jar
10上面代码总结:
用–deploy-mode client来指明运行standalone的client模式
在本地机器提交jar,即spark-submit命令,然后再本地开启了一个driver,然后driver把jar提交到master。让master来控制执行应用
流程:spark-submit命令在本地机器开启driver,driver把jar相关流程交给master进行调度,master在worker节点开启executor运行相关的任务。相对应的executor也需要向driver反向注册,汇报相关进度。executor最终会把结果发送到driver(客户端能够看到结果)
集群模式:
bin/spark-submit \
–class org.apache.spark.examples.SparkPi
–master spark://Centos2:7077
–deploy-mode cluster\ #指明cluster模式,另外一个是client
./examples/jars/spark-examples_2.11-2.4.0.jar
10上面代码总结:
- 用–deploy-mode cluster来指明运行standalone的cluster模式
- 本地机器提交任务,即spark-submit命令,但是此时客户端不会创建driver,把任务发送给master,master就会把请求封装为一个applicationmaster,即driver。会在worker相关 节点开启一个容器中创建applicationmaster。即在集群的某一个节点上创建driver
- 流程:本地机器提交任务,将请求发送给master,master会把请求转换为一个applicationmaster即driver,会在集群中任一集群的某个节点上开启driver,然后master根据driver的需求,在相关的worker开启executor中运行任务,然后这些executor也需要向driver汇报。
- 此时的driver不是在本地机器了
- 查看结果,知道:端口号已经改变,机器也已经改变了。即不是spark:8020了!!!
yarn模式(重点):
需要先停掉standalone的spark模式、停掉zk,如果有高可用的话,也需要停掉。!!!
bin/spark-submit \
–class org.apache.spark.examples.SparkPi
–master yarn
./examples/jars/spark-examples_2.11-2.4.0.jar
10区别在于–master yarn
两种模式:cluster和client,区别在于driver运行的节点(和standalone的类似)
客户端模式:
bin/spark-submit \
–class org.apache.spark.examples.SparkPi
–master yarn \–deploy-mode client \ #指明client模式,另外一个是cluster
./examples/jars/spark-examples_2.11-2.4.0.jar
10流程:
在本地机器用spark-submit提交任务,并在本地机器上开启了driver,driver再提交jar到yarn。
resourcemanager进行资源调度,在nodeManager上执行任务,在nodeManager上开启executor,executor向driver反向注册。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Irdn3mm6-1614914949335)(C:\Users\Thinkpad\AppData\Roaming\Typora\typora-user-images\1614169382451.png)]
图理解:本地机器提交spark-submit,,执行spark-submit里面的main方法,通过反射获取类里面的main方法并执行,然后创建driver。driver创建yarnschdulerbackend,该yarnschdulerbackend会向resourcemanager发送指令,要求开启一个applicationmaster。resourcemanager会在一个nodeManager上开启一个容器,并注册applicationmaster。applicationmaster向resourcemanager申请资源。applicationmaster开启发送指令在有资源的nodeManager上开启相对应的executor,并运行任务。executor也会向driver回报
集群模式:
bin/spark-submit \
–class org.apache.spark.examples.SparkPi
–master yarn \–deploy-mode cluster\ #指明cluster模式,另外一个是client
./examples/jars/spark-examples_2.11-2.4.0.jar
10[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-46M6vf9p-1614914949338)(C:\Users\Thinkpad\AppData\Roaming\Typora\typora-user-images\1614170042297.png)]
图理解:
- 本地机器提交任务,执行spark-submit里面的main方法,并反射获取client类的main方法并执行(但是不创建driver),然后发送开启applicationmaster的命令给resourcemanager。resourcemanager在有资源的nodeManager上开启容器后开启一个applicationmaster,并且在applicationmaster内部开启driver,然后向resourcemanager申请资源,然后resourcemanager在nodeManager上开启executor,并执行任务。这些executor也会向driver汇报!
集群角色
集群角色:
master和worker(宏观)
master:spark特有的资源调度系统的leader,掌管着集群的资源信息,类似于yarn的resourcemanager
- 监听worker,看worker是否正常工作
- 对worker、application等的管理(1.接收worker的注册并管理所有的worker,2.接收客户端提交的application,3.调度等待的application并向worker提交)
worker:spark特有资源调度系统的slave,掌管着所在节点的资源信息,类似于yarn的nodeManager
- 通过registerworker反向注册到master
- 定时发送心跳到master
- 根据master发送的application配置进程环境,并启动executorbackend(执行task所需的临时进行)
Driver和executor(微观)
driver:
- 把用户程序转为作业(job)
- 跟踪executor的运行状况
- 为执行器节点调度任务
- ui展示应用运行状况
端口:8080监听spark的master节点的情况,提交一个任务完成后可以到该页面进行查看。
开启了sparkshell,可以到4040端口查看历史记录,但是sparkshell关闭后,4040就无法访问了,而且以前的历史记录也不见了!!!
本集群配置了spark的历史记录配置,记录在hdfs上的spark-logs目录上
yarn历史配置 端口:8088
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HM2bDyzI-1614914949345)(C:\Users\Thinkpad\AppData\Roaming\Typora\typora-user-images\1614170448612.png)]
依赖问题:如果因为网络问题导致依赖下载不完整,到maven的目录下搜索.lastupdate即可,然后删除这些 文件。然后再到idea下载。
代码中获取sparkcontext:
val conf = new sparkconf().setmaster(“local[*]”).setappname(“suibiande mingzi”);
new sparkcontext(conf)即可获取到sc
后面需要关闭资源。sc.stop
RDD
rdd:弹性分布式数据集,是spark的最基本数据抽象,是一个弹性的、可分区的、里面的元素可并行计算的集合
rdd应用了装饰者模式。(一个个走和一起走的区别)
弹性:
- 存储的弹性:内存与磁盘的自动切换,shuffle
- 容错的弹性:数据丢失可以自动恢复
- 计算的弹性:计算出错重试机制
- 分片的弹性:可根据需要重新分片
调用一个算子会产生一个新的rdd!!!
分布式:
- 数据存储在大数据集群不同节点上
数据集:
- rdd封装了计算逻辑,并不保存数据
数据抽象:
- rdd是抽象类
不可变:
- rdd封装了计算逻辑,是不可以改变的,想要改变只能产生新的rdd。
可分区、并行计算:
- 分区并行处理
算子:rdd的方法才是算子。
特性:
- 一组分区,即是数据集的基本组成单位
- 一个计算每个分区的函数,在分区里面(即算子链)
- rdd之间的依赖关系:getdependence(宽依赖,窄依赖)
- 分区器,即rdd的分片函数:控制分区的数据流向(只针对键值对数据,默认是hash分区)
- 一个列表,存储每个partition的优先位置。
移动数据不如移动计算,除非资源不够!
- rdd算子相关的代码在executor执行;rdd算子外的代码在driver 执行,做的是初始化操作;
- rdd处理的是思维逻辑而不是真实的数据!!!
- 只有触发到行动算子才能真正触发计算。
分区的概念引出==大数据的数据很大,不可能只给一个节点运行,需要多个节点运行。
- 不同分区并行进行工作,互不干扰。即一个分区内的数据就跑自己的算子操作。但是遇到shuffle操作算子的话,就会落盘。(如果不落盘的话,就会可能有多个数据在等待!!!stage阶段分界线)。然后再从磁盘读数据。
编程模式:
在spark中,rdd被表示为对象,通过对象上的方法来对rdd进行转换。rdd经过一系列的transfromation转换定义之后,就可以调用action行动算子触发rdd的计算。
action可以是向应用程序返回结果,或者是向存储系统保存数据。在spark中,只有遇到action,才会执行rdd的计算(即延时计算)
rdd的创建
-
从集合中创建rdd
-
//1.集合创建rdd //创建集合 val list: List[Int] = List(1, 2, 3, 4, 5) //创建rdd val collectionRdd: RDD[Int] = sc.parallelize(list) collectionRdd.collect().foreach(println)
-
-
从外部存储创建rdd
-
//2.从存储系统创建rdd val fileRdd: RDD[String] = sc.textFile("/danci") fileRdd.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).collect() .foreach(println)
-
-
从其他的rdd创建(转换)
分区:
默认分区:
集合创建rdd的分区数为CPU核数或者local[num];
文件创建rdd的分区数为:找到最小分区数local[num]和2比较得到最小的为最小分区数
指定分区:即在创建rdd的时候添加分区数设置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xjgoTN1t-1614914949349)(C:\Users\Thinkpad\AppData\Roaming\Typora\typora-user-images\1614185524023.png)]
文件的:确定分区数(传进去的不是最终的分区数),然后用总字节/分区数获取范围(/r/n也算)
- 设置分区:在创建rdd的时候直接设置
- 获取分区数:sc.getNumPartitions即可获取到
转换算子
单value算子
-
map算子
-
能够转换格式包含数据类型,不改变分区数。
-
一个个数据为单位进行map运算
-
弄懂rdd的数据类型
-
//map算子 val list: List[Int] = List(1, 2, 3, 4) val listRdd: RDD[Int] = sc.parallelize(list) println(listRdd.getNumPartitions) println("=====") val newRdd: RDD[Int] = listRdd.map(_ * 2) println("分区后的分区数") println(newRdd.getNumPartitions)
-
-
mapPartitions算子
-
以一个分区数据为单位进行mapPartitions算子操作,里面需要再调用map算子
-
分区数不改变
-
当内存空间较大的时候用mapPartitions好,可以提高效率
-
//map算子 val list: List[Int] = List(1, 2, 3, 4) val listRdd: RDD[Int] = sc.parallelize(list) println(listRdd.getNumPartitions) println("=====") val newRdd: RDD[Int] = listRdd.map(_ * 2) println("分区后的分区数") println(newRdd.getNumPartitions)
-
-
mapPartitionsWithIndex算子
-
以一个分区数据为单位进行mapPartitionwindex操作,然后对数据进行map算子操作
-
传入的参数为(分区号,对应分区的数据)
-
下面的例子要用1 to n,如果用list的话,会出错。
-
//mapPartitionsWithIndex算子 val rdd: RDD[Int] = sc.parallelize(1 to 4, 2) val mapPartitionsWithIndexRdd: RDD[(Int, Int)] = rdd.m (index, datas) => datas.map((index, _)) ) mapPartitionsWithIndexRdd.collect().foreach(println)
-
-
filter算子
-
里面的函数时判断语句,对集合里面的元素进行筛选
-
contains方法、取模等操作
-
//flatmap算子 val list: List[Int] = List(1, 2, 3, 4, 5, 6, 8) val listRdd: RDD[Int] = sc.parallelize(list) val largerthantwo: RDD[Int] = listRdd.filter(_ > 2) val oushu: RDD[Int] = listRdd.filter(_ % 2 == 0) oushu.collect().foreach(println) largerthantwo.collect().foreach(println)
-
-
flatmap算子
-
对数据进行扁平化处理,里面一般带分隔函数,也可以为自身数据
-
//flatMap算子,对数据进行扁平化处理,后面的函数可以为自身也可以为分隔函数 val rdd: RDD [List[Int]] = sc.parallelize(List(List(1, 2, 3), List(8, 9), List(4, 5, 6))) rdd.flatMap(datas=>datas).collect().foreach(println)
-
-
glom算子(和flatMap的功能相反)
-
将分区的数据转换为一组数组,并放置在rdd中
-
//glom算子 val rdd1: RDD[Int] = sc.parallelize(1 to 4, 2) rdd1.mapPartitionsWithIndex( (index,datas)=>datas.map((index,_)) ).collect().foreach(println) println("====") val maxRdd: RDD[Int] = rdd1.glom().map(_.max) println(maxRdd.sum())
-
-
groupby算子
-
相同规则的进入到同一个迭代器里面
-
规则:被2整除、以首字母为确认值。。。
-
//groupby算子 val rdd1: RDD[Int] = sc.parallelize(1 to 4) val groupbyRdd: RDD[(Int, Iterable[Int])] = rdd1.groupBy(_ % 2) groupbyRdd.collect().foreach(println)
-
groupby注意点
- groupby存在shuffle操作,即将不同的分区数据进行打乱重组
- 因为存在shuffle过程,所以一定会落盘
-
-
sample算子
-
两种情况:
- 当第一个参数为false,表示无放回的抽样,则第二个参数表示每个元素的概率,范围为0-1,第三个为随机数生成器种子
- 当第一个参数为true,表示有放回抽样,则第二个参数表示每个元素的期望次数,范围必须大于等于0,第三个为随机数生成器种子
-
功能:从大量的数据中采样
-
//sample算子 val rdd1: RDD[Int] = sc.parallelize(1 to 10) val sampleRdd1: RDD[Int] = rdd1.sample(true, 0.2, 4) //上面一行代码中,4好像不是规定产生多少个数!!! sampleRdd1.collect().foreach(println) val sampleRdd2: RDD[Int] = rdd1.sample(false, 0.2, 2) sampleRdd2.collect().foreach(println)
-
-
distinct算子
-
去重的作用
-
存在shuffle操作
-
//distinct算子 val list: List[Int] = List(1, 2, 3, 4, 3, 2, 4) val rdd1: RDD[Int] = sc.parallelize(list) rdd1.mapPartitionsWithIndex( (index,datas)=>(datas.map((index,_))) ).collect().foreach(println) println("===") val rdd2: RDD[Int] = rdd1.distinct() rdd2.mapPartitionsWithIndex( (index,datas)=>(datas.map((index,_))) ).collect().foreach(println) rdd2.collect().foreach(println)
-
-
coalesce算子
-
重新分区,但是不涉及shuffle操作,一般用于减少分区
-
但是coalesce算子也可以设置存在shuffle操作
-
//coalesce算子 val list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) val rdd1: RDD[Int] = sc.parallelize(list, 4) rdd1.mapPartitionsWithIndex( (index,datas)=>(datas.map((index,_))) ).collect().foreach(println) println("========") val coalesceRdd: RDD[Int] = rdd1.coalesce(2) coalesceRdd.mapPartitionsWithIndex( (index,datas)=>(datas.map((index,_))) ).collect().foreach(println)
-
-
reparation算子
-
存在shuffle过程,底层是coalesce
-
一般用于增大分区
-
//reparation算子重新分区,存在shuffle过程,底层是coalesce算子,一般用于增大分区 val list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) val rdd1: RDD[Int] = sc.parallelize(list, 2) rdd1.mapPartitionsWithIndex( (index,datas)=>(datas.map((index,_))) ).collect().foreach(println) println("========") val coalesceRdd: RDD[Int] = rdd1.repartition(4) coalesceRdd.mapPartitionsWithIndex( (index,datas)=>(datas.map((index,_))) ).collect().foreach(println)
-
-
sortBy算子
-
按照规则排序
-
存在shuffle过程
-
//sortBy算子 val list: List[Int] = List(3, 4, 2, 1, 6, 8, 7, 5) val rdd1: RDD[Int] = sc.parallelize(list) val rdd2: RDD[Int] = rdd1.sortBy(x => (-x)) rdd2.collect().foreach(println) rdd1.mapPartitionsWithIndex( (index,datas)=>(datas.map((index,_))) ).collect().foreach(println) println("===") rdd2.mapPartitionsWithIndex( (index,datas)=>(datas.map((index,_))) ).collect().foreach(println)
-
-
pipe算子
- 调用脚本的算子,作用于每一个分区
双value类型
-
union并集
-
substract差集
- rdd的位置不同,结果不同
-
intersection交集
-
//union并集、subtract差集、intersect交集 val list1: List[Int] = List(1, 2, 3) val list2: List[Int] = List(3, 4, 5) val rdd1: RDD[Int] = sc.parallelize(list1) val rdd2: RDD[Int] = sc.parallelize(list2) rdd1.union(rdd2).collect().foreach(println) println("===") rdd1.intersection(rdd2).collect().foreach(println) println("===") rdd1.subtract(rdd2).collect().foreach(println)
-
-
zip拉链
-
注意点
- 需要两个rdd的分区数相等
- 两个rdd的每个对应分区数据元素相等
-
生成的是键值对数据,元组数据
-
//zip拉链 val list1: List[Int] = List(1, 2, 3, 4) val list2: List[Int] = List(2, 3, 4, 5) val rdd1: RDD[Int] = sc.parallelize(list1, 2) val rdd2: RDD[Int] = sc.parallelize(list1, 4) val rdd3: RDD[Int] = sc.parallelize(list2, 2) rdd1.zip(rdd3).collect().foreach(println) println("===") //会报错,需要分区数一致 rdd2.zip(rdd3).collect().foreach(println)
-
key-value类型
-
partitionBy算子:按照key重新分区
-
存在shuffle操作
-
可以自定义分区器,默认的是hash分区器
-
//partitionBy,按照key重新分区 val rdd1: RDD[(Int, String)] = sc.parallelize(Array((1, "aaa"), (2, "bbb"), (3, "ccc")),3) rdd1.mapPartitionsWithIndex( (index,datas)=>(datas.map((index,_))) ).collect().foreach(println) val rdd2: RDD[(Int, String)] = rdd1.partitionBy(new HashPartitioner(2)) rdd2.mapPartitionsWithIndex( (index,datas)=>(datas.map((index,_))) ).collect().foreach(println)
-
-
自定义分区器
- 设置分区数
- 重写getpartition方法,具体的分区逻辑
-
reducebykey算子
-
将rdd按照key来对value进行聚合操作
-
先落盘,然后采集相同key的数据到一块,然后逐项累加(下面例子)
-
//reducebykey算子 val rdd: RDD[(String, Int)] = sc.parallelize(Array(("aaa", 2), ("bbb", 2), ("bbb", 3), ("aaa", 4), ("bbb", 5), ("aaa", 6), ("ccc", 7))) val reducebykeyRdd: RDD[(String, Int)] = rdd.reduceByKey(_ + _) reducebykeyRdd.collect().foreach(println)
-
-
groupByKey算子
-
对key进行操作,但是只生成一个seq,不进行聚合操作
-
存在shuffle操作
-
得到的结果是一个元组
-
//groupByKey算子 val rdd: RDD[(String, Int)] = sc.parallelize(Array(("aaa", 2), ("bbb", 2), ("bbb", 3), ("aaa", 4), ("bbb", 5), ("aaa", 6), ("ccc", 7))) rdd.mapPartitionsWithIndex( (index,datas)=>(datas.map((index,_))) ).collect().foreach(println) println("===") val groupbykeyRdd: RDD[(String, Iterable[Int])] = rdd.groupByKey() groupbykeyRdd.mapPartitionsWithIndex( (index,datas)=>(datas.map((index,_))) ).collect().foreach(println) groupbykeyRdd.map {//得到的是一个元组,然后让第二个元素进行累加操作 tuple =>(tuple._1,tuple._2.sum) }.collect().foreach(println)
-
reducebykey和groupByKey的区别:
-
reducebykey有聚合操作,而groupByKey没有聚合操作
-
reducebykey相当于hadoop中的预聚合操作,如果不影响业务的话,用reducebykey算子会好于groupByKey
-
aggregatebykey算子
-
该算子的分区内和分区间的逻辑不同,第一个参数括号为赋予初始值,第二个参数括号为分区内和分区间的逻辑体现
-
//aggregateBykey算子,要括号赋予初始值,然后另一个括号进行封装分区内和分区间的不同思维 val list: List[(String, Int)] = List(("a", 2), ("b", 3), ("c", 4), ("a", 5), ("b", 6), ("c", 7), ("a", 8)) val newRdd: RDD[(String, Int)] = sc.parallelize(list) val aggregateBykey: RDD[(String, Int)] = newRdd.aggregateByKey(0)(math.max(_, _), _ + _)
-
流程:所有的分区都初始化一个key的value为0,然后将相同的key的value不断比较,取出最大值;最后不同分区间的最大值求和
-
-
foldBykey算子
-
是aggregatebykey的特例,即分区内和分区间的思维一致
-
第一个参数括号为赋予初始值,第二个参数括号为分区内和分区间的逻辑体现
-
//foldbykey算子:和aggregatebykey类似,是其特殊例子,即分区内和分区间的逻辑思维一致 val list: List[(String, Int)] = List(("a", 2), ("b", 3), ("c", 4), ("a", 5), ("b", 6), ("c", 7), ("a", 8)) val rdd: RDD[(String, Int)] = sc.parallelize(list) val result: RDD[(String, Int)] = rdd.foldByKey(0)(_ + _) result.collect().foreach(println)
-
-
combineByKey算子
-
共有三个参数
- 第一个,针对key的value进行转换,这里转换为(value,1)的形式
- 第二个,分区内的逻辑体现
- 先由一个经历了转换,然后和相同key的value进行操作(但是一个是(value,1)形式,一个是value形式)==>需要**指明泛型,两个都要指明泛型,**然后函数内部为你自身的思维体现
- 第三个,分区间的逻辑体现
- 两个参数都为(value,count)的形式,此时仍然需要指明泛型,然后函数内部为分区间的逻辑思维体现
-
//combineByKey算子: val list: List[(String, Int)] = List(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98)) val rdd1: RDD[(String, Int)] = sc.parallelize(list,2) val rdd2: RDD[(String, (Int, Int))] = rdd1.combineByKey( (_, 1), (acc: (Int, Int), v: Int) => (acc._1 + v, acc._2 + 1), (acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2) ) rdd2.collect().foreach(println) println("===") val res: RDD[(String, Double)] = rdd2.map { case (key, value) => { (key, value._1 / value._2.toDouble) } } res.collect().foreach(println)
-
-
sortbykey算子
-
按照key来进行排序,也可以用sortBy算子来实现
-
//sortbykey算子 val list: List[(String, Int)] = List(("a", 88), ("d", 95), ("e", 91), ("c", 93), ("f", 95), ("b", 98)) val rdd1: RDD[(String, Int)] = sc.parallelize(list) rdd1.sortByKey().collect().foreach(println)
-
-
mapValues算子
-
只对value操作,返回结果还是原来的数据类型,key不变,value改变了
-
//mapValues算子:只对v进行操作 val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (1, "d"), (2, "b"), (3, "c"))) rdd.mapValues(_+"++++").collect().foreach(println)
-
-
join算子
-
将相同的key对应的多个value关联在一起(不同rdd)
-
返回的rdd类型为(key,(value1,value2))
-
//join算子,对相同key的多个value关联在一起 val rdd1: RDD[(Int, String)] = sc.parallelize(Array((1, "a"), (2, "b"), (3, "c"))) val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (4, 6))) val res: RDD[(Int, (String, Int))] = rdd1.join(rdd2) val rdd3: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (4, 6))) val res: RDD[(Int, ((String, Int), Int))] = rdd1.join(rdd2).join(rdd3) res.collect().foreach(println)
-
-
cogroup算子:类似于全连接,但是在同一个rdd中对key聚合
-
每个rdd中相同key中的元素分别聚合成一个集合
-
先rdd内部聚合,然后rdd键聚合(即两次聚合)
-
//cogroup算子:先rdd内对相同key进行聚合,然后rdd间对相同key聚合 val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c"),(3,"d"))) val rdd1: RDD[(Int, Int)] = sc.makeRDD(Array((1,4),(2,5),(3,6))) val result: RDD[(Int, (Iterable[String], Iterable[Int]))] = rdd.cogroup(rdd1) result.collect().foreach(println)
-
行动算子
-
reduce算子聚合
-
先聚集rdd中的所有元素,先聚合分区内数据,再聚合分区间数据
-
//reduce算子 val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 4, 4, 7, 9, 11, 23, 45, 11)) val res: Int = rdd1.reduce(_ + _) println(res)
-
-
collect算子:以数组的形式返回数据集
- 在驱动程序中,以数组的形式返回数据集的所有元素
- 所有的数据都会从executor拉取到driver端
-
count算子:返回rdd中的元素个数
-
//count算子 val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 4, 4, 7, 9, 11, 23, 45, 11)) println(rdd1.count())
-
-
first算子:返回rdd中的第一个元素
-
//first算子 val rdd1: RDD[Int] = sc.parallelize(List( 11, 23, 45, 11)) println(rdd1.first())
-
-
take算子:返回有rdd前n个元素组成的数组
-
数组可以用mkString方法来拆分数组元素,并展示出来
-
//take算子 val rdd1: RDD[Int] = sc.parallelize(List( 11, 23, 45, 11)) println(rdd1.take(10).mkString("|"))
-
-
takeordered算子
-
返回排序后,rdd的前n个元素的数组
-
先排序,后取元素
-
//takeordered算子:返回排序后的前n个元素组成的数组 val list: List[Int] = List(1, 3, 5, 4, 2, 8, 7, 6) val rdd1: RDD[Int] = sc.parallelize(list) println(rdd1.takeOrdered(5).mkString("|"))
-
-
aggregate算子
-
对rdd分区内的元素通过分区内逻辑和初始值聚合,然后用分区间逻辑和初始值进行操作
-
注意点
- 分区内逻辑再次使用初始值和aggregatebykey是有区别的
- 分区内逻辑和初始值聚合的时候,每个分区都会和初始值聚合,但是到了分区间和初始值聚合的时候,整个rdd作为整体和初始值聚合
-
//aggregate算子 val list: List[Int] = List(1, 2, 3, 4) val rdd1: RDD[Int] = sc.parallelize(list, 4) println(rdd1.reduce(_ + _)) println("===") val res: Int = rdd1.aggregate(10)(_ + _, _ + _) println(res)
-
-
fold算子
-
是aggregate算子的特例,即分区内和分区间的逻辑一致,其余都一致!
-
//fold算子 val list: List[Int] = List(1, 2, 3, 4) val rdd1: RDD[Int] = sc.parallelize(list,4) println(rdd1.fold(10)(_ + _)) println("===") println(rdd1.reduce(_+_))
-
-
countbykey算子
-
统计每种key的个数
-
返回的结果是一个hashmap格式
-
//countByKey算子 val array: Array[(Int, String)] = Array((1, "hello"), (2, "meiyou"), (2, "jiayou"), (3, "success"), (1, "goodjob")) val rdd1: RDD[(Int, String)] = sc.parallelize(array) val res1: collection.Map[Int, Long] = rdd1.countByKey() val res2: collection.Map[(Int, String), Long] = rdd1.countByValue() println(res1) println(res2)
-
-
save的相关算子
-
saveAstextfile算子(还有saveasObjectFile算子)
-
将数据集的元素以textfile的形式保存到hfs文件系统或者其他文件系统。对于每个元素,spark将会调用toString方法,将它转换为文件中的文本
-
//saveAstextfile算子 val list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9) val rdd1: RDD[Int] = sc.parallelize(list, 2) rdd1.saveAsTextFile("/saveAsTextfile")
-
-
-
foreach算子:遍历rdd中每一个元素
-
收集起来在一一打印和分布式一一打印的区别(下面例子)
-
分布式打印的时候,整个分区数据的顺序不同于收集起来再打印
-
//foreach算子 val list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9) val rdd1: RDD[Int] = sc.parallelize(list, 2) val rdd2: RDD[Int] = rdd1.map(x => x) //先收集起来,再打印 rdd2.collect().foreach(println) println("===") //分布式打印 rdd1.foreach(println)
-
rdd序列化
- 初始化工作是在driver端进行的,而实际运行程序是在executor端进行的。这就涉及到了跨进程通信,是需要序列化的
- driver:算子以外的代码都是在driver端进行的
- executor:算子里面的代码都是在executor端进行的
- Kryo序列化框架
- java能够序列化任何的类,但是比较笨重,序列化对象的提交也比较大
- kryo是由spark2.0开始支持的序列化机制
- kryo特点:
- 速度快,是java序列化的10倍
- 当rdd在shuffle的时候,简单数据类型、数组和字符串类型已经在spark内部使用kryo序列化了
- 即使使用kryo序列化,也需要继承Serializable接口,然后再主方法内替换默认的序列化机制
rdd依赖关系
rdd血缘关系
- rdd只支持粗粒度转换,即在大量记录上执行的单个操作。将创建rdd的一系列Linerage(血缘)记录下来,以便恢复丢失的分区
- 血缘会记录rdd的元数据信息和转换行为。当rdd的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区
- 查看血缘关系:通过toDebugString方法,查看Rdd血缘关系(返回前面所有的新的rdd)
- 血缘关系返回的结果中,圆括号的数字表示rdd的并行度,即分区数
依赖关系
- 查看依赖关系:通过dependencies方法,查看rdd的依赖关系
- 只会返回上级的rdd
依赖关系类型:
- 窄依赖
- 表示每一个父rdd的partition最多被子rdd的一个partition使用。窄依赖类似于独生子女
- 宽依赖
- 表示同一个父rdd的partition被多个子rdd的partition依赖,会引起shuffle操作。宽依赖比喻为超生
- 具有宽依赖的转换算子有:sortBy,reducebykey、groupByKey、join和repartion函数
- 宽依赖对spark去评估一个转换有更加重要的影响。比如对性能的影响
spark中的job调度
- 一个spark应用包含一个驱动进程(driver进程,在这个进程写spark的逻辑代码)和多个执行器进程(executor进程,跨越集群中多个节点。)spark程序自己是运行在驱动节点,然后发送指令到执行器节点
- spark集群中可以同时运行多个spark应用,应用由集群管理器来调度
spark应用(sparkcontext对应着application应用)
- 一个spark应用可以包含多个spark job,spark job是在驱动程序中由sparkcontext来定义的(一个sparkcontext对应着一个spark应用)
- 启动sparkcontext时,就开启了一个spark应用。一个驱动程序被启动了,多个执行器在集群中的多个工作节点也被启动了。一个执行器就是一个jvm,一个执行器不能跨越多个节点,但是一个节点可以包含多个执行器
- 一个rdd会跨多个执行器被并行计算,每个执行器可以有这个rdd的过个分区,但是一个分区不能跨越多个执行器
spark job的划分(行动算子对应job)总结
- spark是懒执行的,在驱动程序调用一个action之前,spark应用是不会做任何事情的,针对每个action,spark调度器就创建一个执行图(execution graph)和启动一个spark job
- 每个job由多个stages组成,这些stages就是实现最终rdd所需的数据转换的步骤。一个宽依赖划分一个stage,每个stage由多个tasks来组成。这些tasks就表示每个并行计算(分区的含义),并且会在多个执行器上执行。
- 上面的总结:
- 一个sparkcontext对应着一个spark应用
- 行动算子对应着一个job
- 宽依赖对应stage
- 宽依赖操作最后一个rdd的分区数即为task数。分区数对应着tasks
- 上面的总结:
Stage任务划分(面试重点)
- DAG有向无环图
- DAG有向无环图是由点和线组成的拓扑图形,具有方向,但不会闭环。
- 原始的rdd通过一系列转换就形成了DAG,根据rdd之间的依赖关系的不同将DAG划分成不同的stage。对于窄依赖,partition的转换处理在stage中完成任务。对于宽依赖,由于存在shuffle,只能在parent rdd(父rdd)处理完后,才能开始接下来的计算,因此宽依赖是划分stage的依据。
- rdd任务切分中间分为:application、job、stage和task
- application:初始化一个sparkcontext即生成一个application
- job:一个action算子就会生成一个job
- stage:stage等于宽依赖的个数加1
- task:一个stage阶段中,最后一个rdd的分区个数就是task的个数
RDD持久化(cache、persist、shuffle操作的算子、检查点)
rdd cache、persist缓存
-
rdd通过cache或者persist方法将前面的计算结果缓存,默认情况下会把数据以序列化的形式缓存在jvm的堆内存中。
-
注意点
- 这两个方法cache和persist并不是被调用时就立即缓存。而是触发后面的action时,该rdd才会被缓存在计算节点的内存中。并供后面重用。
- cache操作会增加血缘关系,不改变原有的血缘关系
- cache底层是persist,persist可以设置缓存级别和份数(内存,磁盘)
-
缓存有可能丢失,或者存储与内存的数据由于内存不足而被删除,rdd的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于rdd的一系列转换,丢失数据会被重算,由于rdd的各个partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部的partition
-
自带缓存算子
- spark会自动对一些shuffle操作的中间数据做持久化操作(reducebykey,groupByKey)。这样做的目的是为了当一个节点shuffle失败了,可以避免重新计算整个输入。但是在实际情况下,想重用数据,仍然建议调用cache或persist
rdd checkpoint检查点:检查点是对 缓存点的进一步保障
-
检查点:是通过将rdd中间结果写入hdfs
-
为什么要做检查点:
- 由于血缘依赖过长,会导致容错成本过高,这样就还不如在中间阶段做检查点容错,如果检查点之后有节点出问题,可以从检查点开始重做血缘,减少了开销
-
检查点存储路径:checkpoint的数据通常是存储在hdfs等容错、高可用的文件系统
-
检查点数据存储格式为:二进制文件
-
检查点会切断血缘:在checkpoint过程中,该rdd的所有依赖于父rdd中的信息将全部被移除
-
检查点触发时间:对rdd进行checkpoint操作并不会马上被执行,必须执行action操作才会触发执行。但是,检查点为了数据安全,会从血缘关系的最开始执行一遍
-
设置检查点步骤:
- 设置检查点的数据存储路径:sc.setCheckPointDir(“hdfs路径”)
- 调用检查点方法:rdd.checkpoint()
-
检查点和cache经常会一起使用!!!
-
例子
-
//需要访问集群的用户名,否则无权限访问 System.setProperty("HADOOP_USER_NAME","root") val conf: SparkConf = new SparkConf().setAppName("checkpoint").setMaster("local[*]") val sc: SparkContext = new SparkContext(conf) //设置检查点数据的存储路径 sc.setCheckpointDir("/checkPoint") val rdd: RDD[String] = sc.textFile("/saveAsTextfile") val rdd1: RDD[String] = rdd.flatMap(_.split(" ")) val word_timeRdd: RDD[(String, Long)] = rdd1.map { word => { (word, System.currentTimeMillis()) } } //设置缓存持久化 word_timeRdd.cache() //设置检查点持久化 word_timeRdd.checkpoint() //触发上面两个持久化操作 word_timeRdd.collect().foreach(println) //再次触发执行逻辑 word_timeRdd.collect().foreach(println) //每触发一次action算子,都会执行一次 word_timeRdd.collect().foreach(println) //为了能够在web UI看效果,故意睡眠程序 Thread.sleep(100000) sc.stop()
-
缓存和检查点的区别
- cache缓存只是将数据保存起来,不切断血缘关系。而checkpoint检查点切断血缘关系
- cache缓存的数据通常存储在磁盘、内存等地方,可靠性较低;checkpoint的数据通常存储在hdfs等容错性高、高可用的文件系统,可靠性高。
- 建议对checkpoint的rdd使用缓存,这样即使checkpoint的job只需要从cache缓存中读取数据即可,否则需要再从头计算一次rdd(先cache后做检查点)
- 如果使用完了缓存,可以通过unpersist方法释放缓存
- persist与checkpoint的区别:
rdd.persist(StorageLevel.DISK_ONLY) 与 checkpoint 也有区别。前者虽然可以将 RDD 的 partition 持久化到磁盘,但该 partition 由 blockManager 管理。一旦 driver program 执行结束,也就是 executor 所在进程 CoarseGrainedExecutorBackend stop,blockManager 也会 stop,被 cache 到磁盘上的 RDD 也会被清空(整个 blockManager 使用的 local 文件夹被删除)。而 checkpoint 将 RDD 持久化到 HDFS 或本地文件夹,如果不被手动 remove 掉,是一直存在的,也就是说可以被下一个 driver program 使用,而 cached RDD 不能被其他 dirver program 使用。
键值对rdd数据分区
- spark支持hash分区和range分区和自定义分区,hash分区为当前默认分区
- 分区器直接决定了rdd中分区器的个数、rdd中每条数据经过shuffle后进入哪个分区和 reduce的个数
- 注意点:
- 只有键值对类型的rdd才有分区器,其他的类型rdd的分区器为none
- 每个rdd的分区id范围为0-(numpartitions-1),决定这个值是属于哪个分区的
- 获取rdd的分区器:rdd.partitioner
- 使用hashPartitioner对rdd重新分区:rdd.partitionBy(new HashPartitioner(2))
- range范围分区:
- 对数据的key进行排序
- 抽样得到边界,划分为一定数量的区间
- 将数据放入合适的分区
- 自定义分区
- 设置分区数量
- 重写partitioner方法,封装分区的逻辑思维
数据读取与保存
-
text文件
- 数据读取:sc.textfile(dir)
- 数据保存:rdd.saveAsTextFile(dir)
-
json文件(伪json文件)
-
先通过将json文件当做文本文件text来读取,然后利用相关的json库对每条数据进行json解析
-
处理json文件一般是用sparksql操作来读取的!!!
-
//读取Json输入文件 val jsonRDD: RDD[String] = sc.textFile("input/user.json") //导入解析Json所需的包并解析Json import scala.util.parsing.json.JSON val resultRDD: RDD[Option[Any]] = jsonRDD.map(JSON.parseFull)
-
-
sequence文件
-
sequencefile文件是hadoop采用存储二进制形式的键值对而设计的一种平面文件。
-
sc.sequencefile[keyClass,valueClass](dir)来读取sequencefile文件(中括号的class即为数据类型)
-
只针对键值对类型
-
// 保存数据为SequenceFile dataRDD.saveAsSequenceFile("output") // 读取SequenceFile文件 sc.sequenceFile[Int,Int]("output").collect().foreach(println)
-
-
object对象文件
- 对象文件是将对象序列化后保存的文件,采用java的序列化机制。
- 读取object对象文件:
- 通过用sc.objectFile[k,v](path)函数接收一个路径,读取对象文件,返回相对应的rdd
- 保存object对象文件:
- 通过调用saveasobjectfile()实现对对象文件的输出。因为序列化,所以要指定类型
spark对mysql的操作
-
对mysql读操作
-
//3.定义连接mysql的参数 val driver = "com.mysql.jdbc.Driver" val url = "jdbc:mysql://hadoop102:3306/rdd" val userName = "root" val passWd = "000000" //创建JdbcRDD val rdd = new JdbcRDD(sc, () => { //反射类 Class.forName(driver) //拿到权柄 DriverManager.getConnection(url, userName, passWd) }, "select * from `rddtable` where `id`>=? and `id`<=?;", 1, 10, 1, r => (r.getInt(1), r.getString(2)) )
-
-
对mysql写操作(不用foreach,用foreachPartition操作,减少连接次数)
-
//数据库连接4要素 val driver = "com.mysql.jdbc.Driver" val url = "jdbc:mysql://hadoop202:3306/test" val userName = "root" val passWd = "123456" val rdd: RDD[(String, Int)] = sc.makeRDD(List(("qiaofeng",18),("duanyu",20),("xuzhu",21))) rdd.foreachPartition{ datas=>{ //注册驱动 Class.forName(driver) //获取连接 val conn: Connection = DriverManager.getConnection(url,userName,passWd) //执行的sql var sql:String = "insert into user(name,age) values(?,?)" //获取数据库操作对象 val ps: PreparedStatement = conn.prepareStatement(sql) datas.foreach{ case (name,age)=>{ //给参数赋值 ps.setString(1,name) ps.setInt(2,age) //执行sql语句 ps.executeUpdate() } }
-
累加器
-
累加器:分布式共享只写变量。(task和task之间不能读数据)
-
累加器用来对信息进行聚合,通常在向spark传递参数时,比如使用map函数或则filter传条件时,可以使用驱动器程序中定义的变量。但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值是不会影响到驱动器的对应变量的。而累加器能够实现我们想要的效果
-
如果是副本变量的操作,仅仅是对区间内部进行的,分区间是无法共享的!!!而且driver端的变量时固定不变的(如果不用累加器)
-
累加器代替变量sum的作用
-
累加器也是懒加载,如果不在action算子中操作,其值也为0(初始值)
-
val rdd1: RDD[(String, Int)] = sc.parallelize(List(("a", 1), ("a", 2), ("a", 3), ("a", 4))) val reducebykeyRdd: RDD[(String, Int)] = rdd1.reduceByKey(_ + _) reducebykeyRdd.collect().foreach(println) println("===") //演示副本变量的改变与driver端的变量无关 //说明executor获得的副本变量是无法改变driver的对应变量 var sum =0 rdd1.foreach{ case (string,count)=>{ sum = sum + count println("sum="+sum)//返回结果是四个结果,即分区内逐一累加,分区间不累加 } } println("===") println("a",sum) println("===") //声明累加器 val accumulator: LongAccumulator = sc.longAccumulator("sum") rdd1.foreach{ case (string,count) =>{ //使用累加器进行加,即累加器相当于sum accumulator.add(count) } } //获取累加器的值 println(accumulator.value)
-
驱动器调用sc.accumulator(initialvalue)方法可给累加器设置初始值
-
注意点
- 工作节点上的任务不能相互访问累加器的值。累加器只是一个只写变量
- 对于要在行动操作中使用的累加器,spark只会把每个任务对各累加器的修改应用一次。因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,必须要把累加器放在类似于foreach这样的行动算子中。
- 如果累加器在非行动算子中,则累加器可能会发生不止一次更新
-
自定义累加器
- 继承AccumulatorV2,设定输入和输出的泛型
- 重写方法(6个方法)
- 定义输出数据的集合(对应着输出的泛型)
- 定义初始化状态(初始值)
- 复制累加器
- 重置累加器
- 增加数据(累加器的作用)
- 合并累加器
- 获取累加器的值
-
使用自定义累加器需要注册一下自定义的累加器
广播变量(只发送一次到一个节点)
-
广播变量:分布式共享只读变量
-
在多个并行操作中executor使用同一个变量,spark默认会为每个任务task分别发送;这样如果共享比较大的对象,就会占用很大工作节点的内存
-
广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个spark操作使用。(例如机器学习算法中的一个很大的特征向量,广播变量很适合使用)
-
使用广播变量
- 通过对一个类型T的对象调用Sparkcontext.broadcast创建一个Broadcast[T]对象,任何可序列化的类型都可以这么实现
- 通过value属性访问该对象的值
- 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)
-
其实广播变量就是一个对象,这里例子的广播变量即为list,即将list封装为广播变量而已。
-
val rdd1: RDD[(String, Int)] = sc.parallelize(List(("a", 1), ("b", 2), ("c", 3))) val rdd2: RDD[(String, Int)] = sc.parallelize(List(("a", 4), ("b", 6), ("c", 6))) //采用rdd的方式实现:rdd1.join(rdd2),用到shuffle,性能较低 rdd1.join(rdd2).collect().foreach(println) println("===") //采用集合的方式,实现rdd和list的join val rdd3: RDD[(String, Int)] = sc.parallelize(List(("a", 1), ("b", 2), ("c", 3))) val list: List[(String, Int)] = List(("a", 4), ("b", 6), ("c", 6)) //声明广播变量 val broadcast: Broadcast[List[(String, Int)]] = sc.broadcast(list) res=rdd3.map{ case(k1,v1)=>{ var v2 = 0 //使用广播变量,和累加器类似,代替list的功能,把list封装为广播变量 for ((k3,v3) <- broadcast.value){ if (k1==k3){ v2 = v3 } } (k1,(v1,v2)) } } res.collect().foreach(println)
sparksql
- sparksql是spark用于结构化数据处理的spark模块
- sparksql的数据抽象为spark提供的关于数据结构和正在执行的计算的更多信息
- sparksql是将sparksql转换为rdd,然后提交到集群执行,执行效率非常快
- sparksql数据抽象
- dataframe
- dataset
- sparksql特点:
- 易整合
- 无缝的整合了sql查询和spark编程
- 统一的数据访问方式
- 使用相同的方式连接不通的数据源
- 兼容hive(数仓上直接运行sql)
- 标准的数据连接
- 易整合
spark的dataframe
- dataframe是一种以rdd为基础的分布式数据集,类似于传统的数据库中的二维表格
- dataframe和rdd的区别:
- dataframe带schema元信息,即dataframe表示的二维表数据的每一列都有相对应的名称和类型。
- 支持嵌套数据类型(struct、array和map)
- dataframe也是懒执行的,但是性能上比rdd要高。主要的原因:优化的执行计划,即查询计划通过spark catalystoptimiser进行优化(类似hive的先筛选后join)
spark的dataset(强类型)
- dataset是dataframe一个扩展
- 和类的概念很类似,即person类中有属性dataframe(id,name,age)
- 用样例类来定义dataset中数据的结构信息。样例类中每个属性的名称直接映射到dataset 的字段名称
sparksql编程
- sparkSession是spark最新的sql查询起始点,实质上是sqlcontext和hivecontext的组合。sparkSession还在内部封装了sparkContext,所以计算实际上是由sparkcontext完成的
dataframe的创建方式
-
创建dataframe的方式:
-
通过spark的数据源进行创建
- spark.read.数据源
- df=spark.read.json(“dir”)===>读取json文件创建dataframe
- df.show==>展示df的数据
- spark.read.数据源
-
从一个存在的rdd进行转换
-
从hive table进行查询返回
-
-
注意:如果从内存中获取数据,spark可以知道具体的数据类型,如果是数字,默认作为int处理;但是从文件中读取的数字,不能确定什么数据,所以用bigint接收,bigint和long类型可以进行转换,但是无法和int进行转换!!!
sql风格语法
- sql风格语法:sql语法风格是指我们查询数据的时候使用sql语句来查询,前提是必须要有临时视图或者全局视图来辅助
- 创建dataframe
- val df = spark.read.json(“dir”)
- 对dataframe创建一个临时视图
- df.createOrReplaceTempView(“视图名”)#局部
- df.createGlobalTemView(“全局视图名”)#全局,使用的时候需要在全局视图名加一个global_temp.字段
- 通过对sql语句实现查询全表
- val sqlDF = spark.sql(“sql语句:select * from 视图名”)
- 结果展示
- sqlDF.show
- 创建dataframe
DSL风格语法
-
步骤:
- 创建一个dataframe
- 查询
- df.select(“属性或者*”)
- 如果查询的是属性涉及到函数计算的话,属性前面必须要加一个 符 号 , 否 则 会 报 错 。 如 果 多 个 属 性 的 话 , 其 中 一 个 涉 及 到 函 数 计 算 , 所 有 的 属 性 都 必 须 加 符号,否则会报错。如果多个属性的话,其中一个涉及到函数计算,所有的属性都必须加 符号,否则会报错。如果多个属性的话,其中一个涉及到函数计算,所有的属性都必须加符号
- df.filter($“age”>19) df.groupby(“属性”).count
- 查询完之后调用show方法可以展示结果
-
查看dataframe元数据信息:df.printschema
rdd转换dataframe
- 如果rdd、df和ds之间相互操作,那么需要引入import spark.implicits._ 。其中(spark不是包名,而是sparkSession对象的名称,因此必须先创建sparkSession对象再导入;implicits是一个内部object)
- rdd转dataframe:()
- 创建rdd
- 实现属性和元组一一对应的映射关系。调用toDF(“属性对应列表”)
- rdd手动确认转换并一一对应(两步:转换为元组、元组和属性一一对应)
- 把rdd转换为和dataframe结构化相对应的元组(可以利用toInt或其他方法转换数据类型)#但是dataframe的属性和rdd的元组是无法一一对应的。因此还需要操作
- 上一步调用toDF(“name”,“age”)方法即可实现属性和元组一一对应起来。
- 通过样例类转换
- 创建一个样例类
- case class 类名(属性:数据类型,…,…)#里面的属性列表和dataframe的一一对应
- 根据样例类将rdd转换为dataframe
- 把上面获得的元组有类名用小括号括起来(表示的是该类的类型)
- 创建一个样例类
- 通过编程的方法
dataframe转换为rdd
- 直接用dataframe调用rdd方法即可完成转换了!!!
- 这时候的rdd的数据类型为ROW类型
rdd和dataset之间的转换
- 创建dataset
- 使用样例类序列创建dataset
- case class person(“name:string”,“age:int”)
- 创建样例类序列(集合)
- Seq(person(“name值”,“age值”))
- 样例类序列调用toDS即可创建dataset #调用toDF创建dataframe
- 使用样例类序列创建dataset
- 实际上,一般都是通过rdd来得到dataset的
rdd转换为dataset
sparksql能够自动将包含有样例类的rdd转换为dataset,样例类定义了table的结构,样例类属性通过反射编程了表的列名/属性名。样例类可以包含诸如seq或array等发杂结构
- 步骤:(和rdd转换为dataframe一样)
- 创建rdd
- 创建一个样例类
- 把元组和样例类属性一一对应起来,并调用toDS方法即可获取dataset
dataset转换为rdd
- dataset调用rdd方法即可完成转换
- 返回的rdd的数据类型为dataset的样例类名
dataframe和dataset的转换
dataframe转换为dataset
- 步骤:
- 创建一个dataframe:spark.read.json
- 定义一个样例类:case class 类名(属性:数据类型)
- dataframe调用as[类名]即可完成转换
dataset转换为dataframe
- 步骤:
- 创建一个dataset
- dataset调用toDF方法即可完成转换
-
rdd、dataframe和dataset三者共性
- 三者都是spark里面的分布式数据集
- 三者都是惰性机制,需要碰到action才会真正触发计算
- 三者有共同的函数,如filter,map函数等
- 需要引入spark.implicits._
- 三者都会根据spark的内存情况自动缓存运算,从而不用担心内存溢出
- 三者都有partition的概念
- dataframe和dataset均可使用模式匹配获取各个字段的值和类型
-
三者的区别
- rdd不支持sql操作
- dataframe每一行的类型固定为row,每一列的值没法直接访问,只有通过解析才能获取各个字段的值
- dataframe和dataset均支持sparksql操作,类似于select、groupby和注册临时表
- dataframe和dataset支持溢写特别方便的保存方式。(CSV可以设置带不带表头)
-
常见操作代码:
-
// import spark.implicits._ // val df: DataFrame = spark.read.json("/sparkfile_text/employees.json") // //DSL风格 // df.show() // df.select("name").show() // df.select("*").show() // df.filter("salary > 4000").show() // df.printSchema() // df.select($"salary" +1000).show() // //sql风格 // df.createOrReplaceTempView("employee") // spark.sql("select name from employee").show() // //创建rdd // val rdd: RDD[Int] = spark.sparkContext.parallelize(List(1, 2, 3, 4)) // val rdd1: RDD[Int] = rdd.map(_ * 8) // rdd1.collect().foreach(println) // println("====") // val df1: DataFrame = rdd.toDF("newvalue") // val ds1: Dataset[Int] = rdd.toDS() // df1.show() // ds1.show() // //样例类匹配 rdd->dataframe->dataset // val caseRdd: RDD[(String, Int)] = spark.sparkContext.parallelize(List(("spark", 5), ("flink", 4), ("hive", 4))) // val df1: DataFrame = caseRdd.toDF("name", "money") // val ds1: Dataset[emp] = df1.as[emp] // //dataset->dataframe->rdd // val df2: DataFrame = ds1.toDF() // val rdd3: RDD[Row] = df2.rdd // //dataset->rdd dataframe->rdd // val rdd4: RDD[emp] = ds1.rdd // val rdd5: RDD[Row] = df1.rdd case class emp(name:String,money:Int)
-
用户自定义函数
udf函数
- udf:输入一行,返回一个结果。
- spark.udf功能用户可以自定义函数:spark.udf.register(“函数名”,(参数)=>{对参数进行处理,即方法体})
- 使用:直接用函数传入相关参数即可
udaf函数
- udaf函数:输入多行,返回一行。类似于count、avg、max、min函数
- 可以通过继承UserDefineAggregateFunction来实现用户自定义聚合函数
- 自定义udaf函数(弱类型):主要是确定参数的数据类型、函数缓冲区的数据类型、函数缓冲区的初始化、更新缓冲区的数据、合并缓冲区、最后的结果
- 使用弱类型的自定义udaf函数:创建函数对象,用spark.udf.register(“函数名”,函数对象)。最后直接应用即可
udtf函数
-
输入一行,返回多行(hive)
-
sparksql中没有udtf函数,spark用flatMap实现该功能
sparksql数据的加载与保存
通用的加载和保存方式
- spark.read.load是加载数据的通用方法
- spark.write.save是保存数据的通用方法
加载数据
- read直接加载数据:spark.read.json(“dir”)/textfile/csv等文件和jdbc等#read文件后面括号路径
- format指定加载数据类型:spark.read.format(“加载的数据类型”)[.option("…")].load(“dir”)
- format 指定加载的数据类型,有text,CSV,jdbc、orc等
- load 传入加载数据的路径
- option:在jdbc格式需要传入jdbc响应参数
保存数据
- write直接保存数据:df.write.json/csv/orc/parquet等
- df.write.json(“dir”)
- df.write.save(“dir”)#此时默认的存储格式为parquet
- format指定保存数据类型:df.write.format("…")[.option(…)].save("…")
- format:指定保存的数据类型
- save:保存数据的路径
- option:jdbc需要传入的相对应参数,URL、user、password和datable
- 文件保存选项:
- 保存操作可以使用savemode,用来指明如何处理数据,使用mode方法来设置。
- savemode是一个枚举类,常量包括有:append追加、overwrite覆盖、default抛异常、ignore忽略本次操作
- df.write.mode(“append”).json(“dir”)
从jdbc读取数据
-
spark.read.format("jdbc") .option("url", "jdbc:mysql://hadoop202:3306/test") .option("driver", "com.mysql.jdbc.Driver") .option("user", "root") .option("password", "123456") .option("dbtable", "user") .load().show
从jdbc写数据
-
ds.write .format("jdbc") .option("url", "jdbc:mysql://hadoop202:3306/test") .option("user", "root") .option("password", "123456") .option("dbtable", "user") .mode(SaveMode.Append) .save()
-
读取和保存数据的相关代码例子
-
System.setProperty("HADOOP_USER_NAME","root") val conf: SparkConf = new SparkConf().setAppName("读取与保存").setMaster("local[*]") val spark: SparkSession = SparkSession.builder().config(conf).enableHiveSupport().getOrCreate() import spark.implicits._ //读取json文件,用format和load搭配 val df: DataFrame = spark.read.format("json").load("/sparkfile_text/people.json") df.show() println("===") df.select("age").show() println("===") //保存文件为json文件,并参数类型为追加类型,使用write和save搭配 df.write.format("json").mode("append").save("/df_save_json") println("去看看路径和内容!") //直接看hive表 spark.sql("show tables").show() println("===") //读取mysql数据库的表 spark.read.format("jdbc") .option("url","jdbc:mysql://Centos2:3306/taidi") .option("user","root") .option("password","root") .option("dbtable","score") .load().show()
-
-
读取hive表的时候,直接在设置追加一个设置值即可
-
读取数据尽量使用format和load搭配,保存数据尽量用write和save搭配,可以用mode来选择参数类型(覆盖,追加等情况)
sparkstreaming
-
sparkstreaming是用于流式数据的处理
-
支持多种数据源,如kafka、flume和tcp套接字等
-
数据输入后可以用spark的高度抽象算子:map、reduce 、join、window等运算。
-
结果保存可以保存在多种地方,如hdfs,数据库等
-
sparkstreaming处理数据的单位是一批,而不是一条。数据采集却是一条条采集的
-
sparkstreaming需要设置间隔使得数据汇总到一定量后再一起合并处理。这个间隔就叫批处理间隔
-
批处理间隔决定了sparkstreaming提交作业的频率和数据处理的延迟,也影响着数据处理的吞吐量和性能
-
sparkstreaming使用了高级抽象离散化流,也叫DStreams,它是随着时间推移而收到的数据序列。在内部,收到的数据都作为rdd存在,而DStreams是由这些rdd所组成的序列,因此叫离散化流
-
缺点:是一种批处理,高延迟(对于一条数据来就处理而言)
-
需要有一个executor来专门采集数据!
-
背压机制:根据jobscheduler反馈作业的执行信息来动态调整receiver数据接收率
- spark1.5前,想限制receiver的 数据接收速率,可以通过静态配置参数spark.streaming.receiver.maxRate来实现。防止内存溢出,但是可能会导致资源利用效率下降(大量数据无法进入);
- spark1.5后,可以动态控制数据接收速率来适配集群数据处理能力。通过属性spark.streaming.backpressure.enabled来控制是否启用背压机制,默认值为false,即不启用
-
Dstream入门
-
val conf: SparkConf = new SparkConf().setAppName("DstreamContext").setMaster("local[*]") //new Duration(300)以毫秒为单位,可以用Seconds类来用秒为单位做间隔,Minutes以分钟为单位等 val ssc: StreamingContext = new StreamingContext(conf, Seconds(2)) val socketDS: ReceiverInputDStream[String] = ssc.socketTextStream("Centso2", 9999) val wordDs: DStream[String] = socketDS.flatMap(_.split(" ")) val res: DStream[(String, Int)] = wordDs.map((_, 1)).reduceByKey(_ + _) //打印输出 res.print() //开启采集 ssc.start() //需要收集数据,不能关闭 ssc.awaitTermination()
-
sparkstreaming的上下文对象不同了,是StreamingContext了
-
需要设置批处理间隔
-
数据抽象是DStream,离散化流
-
需要开启收集数据
-
需要让程序不关闭(才有机会采集数据)
-
-
DStream是一系列连续的rdd来表示每个rdd含有一段时间间隔内的数据
-
应用:水位监控
-
注意点:
- streamingcontext启动了,就不能再添加新的stream计算了,即在start方法后,不能做运算了
- streamingcontext已经停止后,他也不能再重启了
- 在同一个jvm内,同一时间内只能启动一个streamingcontext
- 一个sparkcontext可以重用去创建多个Streamingcontext,前提是:以前的streamingcontext已经停掉,并且sparkcontext没有被停掉
-
数据源可以将rdd封装到消息队列中,给DStream采集数据(kafka)
-
streamingcontext内部封装了sparkcontext!
kafka数据源(采集数据的调用者已经改变了)
- 版本选型
- receiverAPI:需要一个专门的executor去接收数据,然后发送给其他的executor做计算。
- 弊端:接收数据的executor和计算的executor速度会有所不同,特别是在接收数据的executor速度大于计算的executor速度,会导致计算数据的节点内存溢出
- directAPI:由计算的executor来主动消费kafka的数据,速度由自身控制
- receiverAPI:需要一个专门的executor去接收数据,然后发送给其他的executor做计算。
0-8版本
-
版本为0-8例子(采集数据的调用者已经改变了),receiverAPI会有专门的executor采集数据,其他的executor计算
-
val conf: SparkConf = new SparkConf().setAppName("kafka-sparkstreaming").setMaster("local[*]") val ssc: StreamingContext = new StreamingContext(conf, Seconds(3)) //不是用ssc来收集信息了,而是用kafkautils /** * 数据来源已经改变了,不是ssc了,而是kafkautils * 四个参数: * 1.ssc上下文 * 2.zookeeper所在的位置,主机名:端口号 * 3.消费者组名 * 4.主题 Map(主题名,分区数) */ val kafkaDs: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream( ssc, "Centos2:2181,slave1:2181,slave2:2181,slave3:2181",//zookeeper所在的位置,主机名:端口号 "first",//消费者组名 Map("kafka_sparkstreaming" -> 2)//map(主题名->分区数) ) //我们需要的是value val lineDs: DStream[String] = kafkaDs.map(_._2) //wordcount思路 val res: DStream[(String, Int)] = lineDs.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _) //打印结果 res.print() //开始采集 ssc.start() //保持不关闭程序 ssc.awaitTermination()
-
kafkautils采集数据调用createStream方法,里面四个参数如下:
-
* 四个参数: * 1.ssc上下文 * 2.zookeeper所在的位置,主机名:端口号 * 3.消费者组名 * 4.主题 Map(主题名,分区数)
-
-
该程序停掉之后,重新开启是无法拿到了,但是能够到zookeeper去找到相对应的数据(即offset维护在zk)这是receiverAPI的维护offset
-
-
0-8directAPI:
-
自动维护offset模式:offset维护在检查点,需要手动添加检查点:ssc.checkpoint(“dir”)
-
//定义kafka参数:两个,kafka消费者的bootstrap(主机名:端口号),kafka消费者组名 val kafkaParams: Map[String, String] = Map[String, String]( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "Centos2:9092,slave1:9092,slave2:9092,slave3:9092", ConsumerConfig.GROUP_ID_CONFIG -> "first" ) //采集者不是ssc,还是kafkautils val kafkaDs: InputDStream[(String, String)] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder]( ssc, kafkaParams, Set("first") )
-
注意点:
-
/** * 三个参数: * 1.ssc上下文 * 2.kafka参数(两个,一个是kafka的bootstrap,一个是消费者组名),类型为map[string,string] * 3.set类型的主题名 * * createDirectStream需要加参数:共四个 * 1.topic的key数据类型 * 2.topic的value数据类型 * 3.序列化的编码:StringDecoder * 4.反序列化的编码类型:StringDecoder */
-
-
但是尽管设置了检查点,但是你并没有从检查点拿数据(思维:先从检查点拿,然后再从kafka数据源拿数据!)
-
先从检查点拿数据,然后再从kafka拿数据
-
System.setProperty("HADOOP_USER_NAME","root") /** * StreamingContext调用getActiveOrCreate方法从检查点先拿数据,然后再从kakfa拿数据,参数有两个 * 1.检查点的目录,字符串类型 * 2.kafkaUtils拿数据到处理数据的逻辑思维体现 * * 但是,start方法和awaitTermination方法放在getActiveOrCreate方法外 * (因为从检查点开始采集,没有不代表结束,还会到kafka拿数据。知道kafka不发数据了才会停止) */ val ssc = StreamingContext.getActiveOrCreate("/tryagain", () => getscc) ssc.start() ssc.awaitTermination() } def getscc:StreamingContext = { val conf = new SparkConf().setAppName("duowenjian").setMaster("local[*]") val ssc = new StreamingContext(conf, Seconds(3)) ssc.checkpoint("/tryagain") val kafkaparmes = Map[String, String]( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "slave1:9092,slave2:9092,slave3:9092", ConsumerConfig.GROUP_ID_CONFIG -> "bigdata" ) val kafkasources = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder]( ssc, kafkaparmes, Set("big") ) val datas = kafkasources.map(_._2) val flatmapDs = datas.flatMap(_.split(" ")) val mapDs = flatmapDs.map((_, 1)) val res = mapDs.reduceByKey(_ + _) res.print() ssc }
-
自动维护的缺点:
- 大量小文件产生(每隔一个批处理间隔时间就会产生小文件)
- 从停止到重新开始这段时间长度里,会以批处理间隔时间执行一遍。
-
-
手动维护offset模式:
- createDirectStream参数里面为fromoffset参数,类型不是主题了,而是(那个主题,那个分区,偏移量为多少)
- 需要更新offset:利用transform算子操作底层rdd的一个实例asinstanceof
-
- 0-10directAPI
- 调用createDirectStream方法,参数为ssc、位置策略(执行器executor机器与kafka节点机器)#调用的是一个常量、消费策略(需要调用subscribe方法订阅主题,subscribe方法参数:主题,为set类型,还有kafka参数)
- 需要指明泛型:subscribe方法需要指明泛型,是kv类型,还有createDirectStream也要指明泛型,也是kv泛型
- offset维护在_consumer_offset主题中
- 0-8版本的receiverAPI想要增加消费并行度的话,需要多个流union
- 0-8版本的directAPII想要增加消费并行度的话,增加executor数量即可
- 0-10版本的directAPI想要增加消费并行度的话,增加executor数量即可
-
transform:将dstream转换为rdd
-
val lineDs: ReceiverInputDStream[String] = ssc.socketTextStream("Centos2", 9999) lineDs.transform{ rdd =>{ val wordRdd: RDD[String] = rdd.flatMap(_.split(" ")) val mapRdd: RDD[(String, Int)] = wordRdd.map((_, 1)) val reducebyKeyRdd: RDD[(String, Int)] = mapRdd.reduceByKey(_ + _) val sortbykeyRdd: RDD[(String, Int)] = reducebyKeyRdd.sortByKey() sortbykeyRdd //返回rdd } }
-
-
updatestatebykey:用于将历史结果应用到当前批次,该操作允许在使用新信息不断更新状态的同时能够保留她的状态
- 参数为函数,然后函数有两个参数,第一个参数为目前窗口的数据集合,第二个参数是缓冲区数据
- 从缓冲区拿数据:getOrElse(初始值)函数
- 思维类似于累加器、强类型的udaf函数
- 该缓冲区的数据保存在检查点,需要手动设置。ssc.checkpoint
-
输出操作
- print:打印到控制台
- saveastextfile:保存为text文件,里面参数可以设置前缀和后缀
- DStream.saveAsTextFile(“dir”,preffix)
- saveasHadoopfiles:将数据保存为hadoop files,参数也可以设置前后缀
- saveasobjectfiles:以java序列化的方式将数据保存为sequencefiles。
-
foreachRdd:和transform算子类似,但是foreachRDD是没有返回值的,transform是有返回值的
- foreachRDD:将函数用于产生于stream的每一个rdd,其中传入的函数应该事先每一个rdd中数据推送到外部系统
- 使用注意点:
- 连接不能写在driver层面(序列化)
- 如果是涉及到连接数据库,以分区来操作好点,即foreachRDDPartition算子优于foreachRDD
窗口函数
-
var kvDs = lineDs.flatMap(_.split(" ")).map((_,1)) val windowDs: DStream[(String, Int)] = kvDs.window(Seconds(6), Seconds(3))
DStream中也有累加器和广播变量,使用方法和rdd的一致
sparksql操作sparkstreaming
- 利用foreachRDD算子,对rdd进行转换为dataframe,然后注册临时表,使用sql查询
caching和persistence
- 如果DStream中的数据多次被计算,可以使用缓存
spark相关的案例:top n
ubscribe方法订阅主题,subscribe方法参数:主题,为set类型,还有kafka参数)
- 需要指明泛型:subscribe方法需要指明泛型,是kv类型,还有createDirectStream也要指明泛型,也是kv泛型
- offset维护在_consumer_offset主题中
- 0-8版本的receiverAPI想要增加消费并行度的话,需要多个流union
- 0-8版本的directAPII想要增加消费并行度的话,增加executor数量即可
- 0-10版本的directAPI想要增加消费并行度的话,增加executor数量即可
-
transform:将dstream转换为rdd
-
val lineDs: ReceiverInputDStream[String] = ssc.socketTextStream("Centos2", 9999) lineDs.transform{ rdd =>{ val wordRdd: RDD[String] = rdd.flatMap(_.split(" ")) val mapRdd: RDD[(String, Int)] = wordRdd.map((_, 1)) val reducebyKeyRdd: RDD[(String, Int)] = mapRdd.reduceByKey(_ + _) val sortbykeyRdd: RDD[(String, Int)] = reducebyKeyRdd.sortByKey() sortbykeyRdd //返回rdd } }
-
-
updatestatebykey:用于将历史结果应用到当前批次,该操作允许在使用新信息不断更新状态的同时能够保留她的状态
- 参数为函数,然后函数有两个参数,第一个参数为目前窗口的数据集合,第二个参数是缓冲区数据
- 从缓冲区拿数据:getOrElse(初始值)函数
- 思维类似于累加器、强类型的udaf函数
- 该缓冲区的数据保存在检查点,需要手动设置。ssc.checkpoint
-
输出操作
- print:打印到控制台
- saveastextfile:保存为text文件,里面参数可以设置前缀和后缀
- DStream.saveAsTextFile(“dir”,preffix)
- saveasHadoopfiles:将数据保存为hadoop files,参数也可以设置前后缀
- saveasobjectfiles:以java序列化的方式将数据保存为sequencefiles。
-
foreachRdd:和transform算子类似,但是foreachRDD是没有返回值的,transform是有返回值的
- foreachRDD:将函数用于产生于stream的每一个rdd,其中传入的函数应该事先每一个rdd中数据推送到外部系统
- 使用注意点:
- 连接不能写在driver层面(序列化)
- 如果是涉及到连接数据库,以分区来操作好点,即foreachRDDPartition算子优于foreachRDD
窗口函数
-
var kvDs = lineDs.flatMap(_.split(" ")).map((_,1)) val windowDs: DStream[(String, Int)] = kvDs.window(Seconds(6), Seconds(3))
DStream中也有累加器和广播变量,使用方法和rdd的一致
sparksql操作sparkstreaming
- 利用foreachRDD算子,对rdd进行转换为dataframe,然后注册临时表,使用sql查询
caching和persistence
- 如果DStream中的数据多次被计算,可以使用缓存
spark相关的案例:top n