1、RDD概述
1、什么是RDD?
RDD是弹性分布式数据集,代表的是抽象类、弹性的、不可变、分布式、可分区、分区元素可并行计算的集合。
弹性:
存储的弹性: 内存与磁盘的动态切换[RDD的中间结果是保存在内存中,如果内存不足会自动将数据保存到磁盘]。
容错的弹性: 数据丢失会自动恢复[某个RDD某个分区的数据丢失之后会根据RDD依赖关系找到第一个RDD重新读取数据重新开始计算最终得到丢失的数据]
计算的弹性: 计算出错会重试
分区的弹性: RDD的分区会根据文件的切片动态规划
不可变:
rdd里面只封装了数据的处理逻辑,如果想要重新改变数据,需要生成新的RDD,新RDD中封装新的处理逻辑。
分布式:
数据是分布式存储,计算的时候也是分布式的
不存储数据:
rdd里面只封装了数据的处理逻辑,不存储数据,每个rdd数据处理完成之后会传递给下一个rdd。
可分区与分区元素可并行计算:
rdd为了分布式计算,会根据文件的切片规划分区,一般文件有多少切片,RDD就有多少分区,rdd的分区是分布式并行计算。
RDD是惰性的,只有触发collect这类算子才会触发任务的计算。
2、RDD五大特性
一组分区列表: RDD会有多个分区
作用在每个分区上计算函数: RDD的多个分区是并行计算的,计算逻辑是一样的,计算数据不一样。
一组其他rdd代表的依赖关系: RDD如果计算出错需要根据依赖关系找到父RDD,所以每个RDD都会将父RDD保存。
分区器[可选]: 用于shuffle规划数据放在哪个分区的组件,分区器只能用在元素为KV类型的RDD上。
优先位置[可选]: spark在分配task的时候会考虑将task分配到数据所在的位置,便于数据的拉取。
2、RDD的编程
1、RDD的创建
1、通过集合创建
sc.makeRDD(集合)
sc.parallelize(集合)
2、通过读取文件创建
sc.textFile(path) 相对路径找本地,HDFS需要写全路径 "hdfs://hdaoop:8020/Input/wc.txt"
3、通过其他rdd衍生
val rdd = rdd1.map/flatMap/...
2、RDD分区数
1、通过集合创建RDD的分区数: val rdd = sc.parallelize(集合,[numSlices=defaultParallelism])
1、如果创建rdd的时候有指定numSlices参数,此时创建的rdd分区数 = 指定的numSlices的值
2、如果创建rdd的时候没有指定numSlices参数,此时创建的rdd分区数 = 默认值【defaultParallelism】
1、如果在创建sparkcontext的时候有在sparkconf中设置spark.default.parallelism参数,此时defaultParallelism=spark.default.parallelism参数值。
[spark.default.parallelism 一般都是要指定的,设置为总核数的2-3倍]
2、如果在创建sparkcontext的时候没有在sparkconf中设置spark.default.parallelism参数
1、master=local,defaultParallelism=1
2、master=local[N],defaultParallelism=N
3、master=local[*],defaultParallelism=cpu个数
4、master=spark://.. , defaultParallelism = max([totalCoreCount]所有executor总核数,2) [StandAlone、Yarn 适用]
2、通过读取文件创建RDD的分区数: sc.textFile(path,[minPartitions=defaultMinPartitions])
1、如果创建rdd的时候有指定minPartitions参数,此时rdd的分区数>=指定的minPartitions的值
2、如果创建rdd的时候没有指定minPartitions参数,此时rdd的分区数>=min(defaultParallelism,2)
[读取文件创建的rdd的分区数最终还是由文件的切片数决定。]
3、通过其他rdd衍生出新RDD的分区数 = 依赖的第一个rdd的分区数
3、spark算子
什么时候会产生shuffle
1、相同key、相同的数据要聚在一起 [分组]
2、某段范围的数据要聚在一起 [排序]
3、分区数改变 [可能会产生shuffle]
Spark算子中的函数在Executor执行,Spark算子外的函数是在Driver执行的
spark算子分为两类:
Transformation转换算子: 不会触发任务计算,只会封装数据的处理逻辑,调用之后会生成新的RDD
map(func: RDD元素类型=>B): 一对一映射[原RDD一个元素映射新RDD一个元素] *********
map里面的函数是针对每个元素操作,RDD有多少元素,函数就调用多少次
map生成的新RDD元素个数 = 原RDD元素个数
map的应用场景: 数据类型/值的转换
map方法的函数最终是给每个分区封装数据的Iterator[不可以重复使用]方法使用
mapPartitions(func: Iterator[RDD元素类型]=>Iterator[B]): 一对一映射[原RDD一个分区映射新RDD一个分区] *********
mapPartitions里面的函数是针对每个分区操作,RDD有多少分区,函数就调用多少次
mapPartitions里面的函数的参数是一个分区所有数据的迭代器
mapPartitions的应用场景: 用于读取mysql/hbase/redis等存储介质的数据,此时可以减少链接创建与销毁的次数。
//通过map实现的时候,map里面的函数是每个元素调用一次,所以每次都会创建连接销毁连接,当id特别多的时候会出现程序运行缓慢的问题.
// 可能的解决方案:
//1、将mysql连接的创建与销毁放在map算子外面 【不行】
// 【问题: spark算子中的函数是在executor的task中执行,spark算子函数外的代码是在Driver执行的,所以如果task中需要使用Driver对象的时候,此时spark会将Driver对象序列化之后传给task使用,但是mysql连接没有实现序列化接口】
//2、有没有一种方式能够减少链接创建与销毁的次数[mapPartitions]
mapPartitions与map的区别: *********
1、函数的参数不一样
map里面的函数是针对每个元素操作,RDD有多少元素,函数就调用多少次
mapPartitions里面的函数是针对每个分区操作,RDD有多少分区,函数就调用多少次
2、函数的返回值类型不一样
map里面的函数返回的是新RDD的一个元素,所以map生成新RDD元素个数 = 原RDD元素个数
mapPartitions里面的函数的返回值是新RDD一个分区所有数据的迭代器,所以mapPartitions生成的新RDD元素个数不一定等于原RDD元素个数。
3、元素内存回收的时机不一样
map里面的函数是针对每个元素操作,所以元素操作完成之后,元素就可以回收了
mapPartitions里面的函数是针对分区所有数据的迭代器操作,当某个元素处理完成之后,不会立即回收必须等到该分区迭代器中所有数据全部处理完成才会统一回收,所以如果分区中数据量比较大,可能出现内存溢出的情况,此时可以使用map代替。
mapPartitionsWithIndex(func: (Int,Iterator[RDD元素类型])=> Iterator[B]): 一对一映射[原RDD一个分区映射新RDD一个分区]
mapPartitionsWithIndex里面的函数是针对每个分区操作,RDD有多少分区,函数就调用多少次
mapPartitionsWithIndex里面的函数的第一个参数代表分区号
mapPartitionsWithIndex里面的函数的第二个参数代表分区号对应分区的所有数据的迭代器。
mapPartitionsWithIndex与mapPartitions的区别:
mapPartitionsWithIndex里面的函数的参数相对mapPartitions来说多了个分区号。
flatMap(func: RDD元素类型=>集合) = map + flatten 处理数据之后压平 *********
flatMap里面的函数是针对每个元素操作,RDD有多少元素,函数就调用多少次
flatMap生成的新RDD元素个数>=原RDD元素个数
flatMap的应用场景: 一对多
glom
glom就是将每个分区所有数据转换成一个数组
glom生成新RDD元素个数 = 分区数,新RDD里面的元素类型是Array
groupBy(func: RDD元素类型=>K ): 按照指定字段分组 *********
groupBy里面的函数是针对每个元素操作
groupBy是按照函数的返回值对原RDD所有元素进行分组
groupBy生成新RDD元素类型KV键值对,K是函数的返回值,V是一个集合,集合中装在的是K对应原RDD中的所有元素
groupBy会产生shuffle操作
filter(func: RDD元素类型=>Boolean): 按照指定条件过滤 *********
filter里面的函数是针对每个元素操作,RDD有多少元素,函数就调用多少次
filter保留的是函数返回值为true的数据。
sample(withRelements,fraction): 采样 *********
withRelements:代表同一元素是否可以被多次采样[true代表同一个元素可能被采样多次,false代表同一个元素最多被采样一次]<工作中设置为false>
fraction
withRelements=true, fraction代表元素期望被采样的次数[>0]。
withRelements=false,fraction代表每个元素被采样的概率[0,1] <fraction工作中一般设置为0.1-0.2>
sample一般只用于数据倾斜场景,发生数据倾斜之后,一般通过采样的样本数据确定哪个key出现了数据倾斜,然后针对性进行处理。
工作中一般设置[withRelements=false],[fraction=0.1~0.2]
distinct: 去重 *********
distinct会产生shuffle操作。
coalesce(分区数[,shuffle=false]): 合并分区 *********
coalesce默认只能减少分区数,此时没有shuffle操作
coalesce如果想要增大分区数,必须设置shuffle=true,但是此时会产生shuffle操作。
coalesce一般搭配filter使用减少分区数[filter之后分区数据量变小了]
repartition(分区数): 重分区 *********
repartition既可以增大分区数也可以减少分区数,但是都会产生shuffle操作
repartition底层就是使用的coalesce(分区数,shuffle=true)
repartition一般用于增大分区数,因为使用简单
coalesce与repartition的区别: *********
coalesce默认只能减少分区数,此时没有shuffle操作
repartition既可以增大分区数也可以减少分区数,但是都会产生shuffle操作
sortBy(func: RDD元素类型=>K): 按照指定字段排序 *********
sortBy里面的函数是针对每个元素操作,RDD有多少元素,函数就调用多少次
sortBy后续是按照函数的返回值对原RDD所有元素排序
sortBy会产生shuffle操作
pipe(path): 调用脚本
pipe调用的时候是每个分区调用一次
pipe调用脚本的时候可以在脚本中通过echo返回新RDD一个分区的 数据。
intersection: 交集
intersection会产生shuffle
subtract: 差集
subtract会产生shuffle
union: 并集
union不会产生shuffle
union生成新RDD分区数 = 依赖的两个父RDD分区数之和
zip: 拉链
zip不会产生shuffle
两个RDD要想拉链必须要求分区数和元素个数都一样。
partitionBy(p:Partitioner): 根据指定的分区器重分区
HashPartitioner[partitions:Int]
RangePartitioner
自定义分区器:
1、定义一个class继承Partitioner
2、重写抽象方法
class UserDefinedPartitioner(num:Int) extends Partitioner{
//方法1:获取重分区的分区数
override def numPartitions: Int = num
//方法2:根据key获取分区号
override def getPartition(key: Any): Int = key match{
case x:Int if(x%2==0) => 1
case x:Int if(x%2!=0) => 2
}
}
/**
* new UserDefinedPartitioner 这个类没有在算子内,是在Driver里面定义的
* task后续需要使用这个自定义分区器对象,所以需要在Driver里序列化传给Task
* UserDefinedPartitioner 如果是内部类,则需要序列化父类 且SC加上注解 @transient [不去序列化加了该注解的对象]
*/
/**
* partitionBy(Partitioner): 通过指定的分区器对数据重新洗牌分区
*/
@Test
def partitionBy(): Unit ={
val rdd = sc.parallelize(List(1,4,3,2,5,6,7,9))
val rdd2 = rdd.map(x=>(x,x))
val rdd3 = rdd2.partitionBy(new UserDefinedPartitioner(3))
}
groupByKey: 根据key分组 ****
groupByKey的生成的新RDD元素类型是KV键值对,K是分组的key,V是key对应原RDD的所有value值的集合。
reduceByKey(func: (Value值类型,Value值类型)=>Value值类型): 根据key分组,对每个组所有value值聚合 *********
[案例]
@Test
def reduceByKey(): Unit ={
val rdd = sc.parallelize(List("hello","spark","scala","scala","java","python","spark","python","scala","hello"),2)
val rdd2 = rdd.map(x=>(x,1))
val rdd5 = rdd2.reduceByKey(
(agg,curr)=>{agg+curr}
)
println(rdd5.collect().toList)
}
reduceByKey与groupByKey的区别:
reduceByKey存在combiner预计算过程,combiner之后数据量变小,shuffle性能更高
groupByKey没有预计算过程
combineByKey(createCombiner: Value值类型=>C,mergeValue: (C,Value值类型)=>C,mergeCombine: (C,C)=>C)
createCombiner: 是在combiner阶段对每个组第一个value值进行转换
mergeValue: combiner预聚合计算逻辑,针对每个组第一次计算的时候,函数第一个参数的初始值 = 第一个函数对该组第一个value值的转换结果
mergeCombine: reducer计算逻辑,针对每个组第一次计算的时候,函数第一个参数的初始值 = 该组第一个value值
[案例]
@Test
def combineByKey(): Unit ={
val rdd = sc.parallelize(List( ("语文",60),("数学",80),("语文",90),("英语",100),("数学",70),("数学",100),("语文",100),("语文",80),("数学",90),("英语",70),("英语",70),("数学",100) ),2)
val rdd7 = rdd.combineByKey(
x=>{(x,1)} ,
(agg:(Int,Int) ,curr) =>{(agg._1+curr, agg._2+1)},
(agg:(Int,Int),curr:(Int,Int))=>{(agg._1+curr._1,agg._2+curr._2)}
)
val rdd8 = rdd7.map{
case (name,(score,num)) => (name, score.toDouble/num)
}
println(rdd8.collect().toList)
}
foldByKey(默认值)(func: (Value值类型,Value值类型)=>Value值类型 ): 根据key分组,对每个组所有value值聚合
[案例]
/**
* foldByKey(默认值)(func: (value值类型,value值类型) => value值类型 ): 根据key分组聚合
* foldByKey在combiner阶段针对每个组第一次计算的时候,函数第一个参数的初始值 = 默认值
*/
@Test
def foldByKey(): Unit ={
val rdd = sc.parallelize(List( ("语文",60),("数学",80),("语文",90),("英语",100),("数学",70),("数学",100),("语文",100),("语文",80),("数学",90),("英语",70),("英语",70),("数学",100) ),2)
val rdd2 = rdd.map{
case (name,score)=>(name,(score,1))
}
val rdd3 = rdd2.foldByKey
((0,0))
( (agg,curr)=>{(agg._1+curr._1,agg._2+curr._2)} )
val rdd4 = rdd3.map{
case (name,(score,num)) => (name,score.toDouble/num)
}
println(rdd4.collect().toList)
}
aggregateByKey(默认值)(seqOp: (默认值类型,Value类型)=>默认值类型,comOp: (默认值类型,默认值类型)=>默认值类型):根据key分组,对每个组所有value值聚合
[案例]
@Test
def aggregateByKey(): Unit ={
val rdd = sc.parallelize(List( ("语文",60),("数学",80),("语文",90),("英语",100),("数学",70),("数学",100),("语文",100),("语文",80),("数学",90),("英语",70),("英语",70),("数学",100) ),2)
val rdd2 = rdd.aggregateByKey(
(0,0))
( (agg:(Int,Int),curr:Int)=>{(agg._1 + curr, agg._2+1)},
( agg:(Int,Int),curr:(Int,Int) ) => {(agg._1+curr._1,agg._2+curr._2)}
)
val rdd3 = rdd2.map{
case (name,(score,num)) => (name,score.toDouble/num)
}
println(rdd3.collect().toList)
}
reduceByKey、foldByKey、combineByKey、aggregateByKey的区别:
reduceByKey: combiner与reducer计算逻辑一样, combiner函数针对每个组第一次计算的时候,函数第一个参数的初始值 = 该组第一个value值
foldByKey: combiner与reducer计算逻辑一样, combiner函数针对每个组第一次计算的时候,函数第一个参数的初始值 = 默认值
combineByKey: combiner与reducer计算逻辑可以不一样,combiner函数针对每个组第一次计算的时候,函数第一个参数的初始值 = 第一个函数对该组第一个value值的转换结果
aggregateByKey: combiner与reducer计算逻辑可以不一样,combiner函数针对每个组第一次计算的时候,函数第一个参数的初始值 = 默认值
[reduceByKey(func: (Value值类型,Value值类型)=>Value值类型)]
[foldByKey(默认值)(func: (Value值类型,Value值类型)=>Value值类型 )]
[combineByKey(createCombiner: Value值类型=>C,mergeValue: (C,Value值类型)=>C,mergeCombine: (C,C)=>C)]
[aggregateByKey(默认值)(seqOp: (默认值类型,Value类型)=>默认值类型,comOp: (默认值类型,默认值类型)=>默认值类型)]
sortByKey([ascending=true]): 根据key对数据排序 *****
mapValues(func: value值类型=>B): 一对一映射[原一个value值映射生成新RDD一个value值,key不变]
mapValues里面的函数是针对每个元素的value值操作,元素有多少个,函数就执行多少次
join: 相当于sql内连接,结果数据为: 两个rdd元素key相同才能join上
join生成新RDD里面元素类型是KV键值对
K是join的key,也就是元素的key
V是二元元组,二元元组第一个值是左RDD key对应的value值,二元元组第二个值是右RDD key对应的value值
leftOuterJoin: 相当于sql左连接,结果数据为: 两个rdd元素key相同join的数据 + 左RDD不能join的数据
leftOuterJoin生成新RDD里面元素类型是KV键值对
K是join的key,也就是元素的key
V是二元元组,二元元组第一个值是左RDD key对应的value值,二元元组第二个值是Option(右RDD key对应的value值)
rightOuterJoin: 相当于sql右连接,结果数据为: 两个rdd元素key相同join的数据 + 右RDD不能join的数据
rightOuterJoin生成新RDD里面元素类型是KV键值对
K是join的key,也就是元素的key
V是二元元组,二元元组第一个值是Option(左RDD key对应的value值),二元元组第二个值是右RDD key对应的value值
fullOuterJoin:相当于sql全连接,结果数据为: 两个rdd元素key相同join的数据 + 右RDD不能join的数据 + 左RDD不能join的数据
fullOuterJoin生成新RDD里面元素类型是KV键值对
K是join的key,也就是元素的key
V是二元元组,二元元组第一个值是Option(左RDD key对应的value值),二元元组第二个值是Option(右RDD key对应的value值)
cogroup: 先对两个rdd执行groupByKey,然后将两个rdd groupByKey的结果进行全外连接
Action行动算子: 触发任务计算,调用之后生成的是具体的结果数据
reduce(func: (RDD元素类型,RDD元素类型)=>RDD元素类型): 对RDD所有元素聚合
聚合原理: 先在每个分区中对分区所有数据聚合,然后将每个分区的聚合结果发给Driver进行汇总
reduce没有shuffle
collect: 收集RDD每个分区的数据发给Driver以数组的形式封装 *********
如果RDD分区数据比较大,Driver内存默认只有1G,所以可能出现内存溢出
所以工作中一般需要设置Driver的内存大小为5-10G。
可以通过bin/spark-submit --driver-memory 设置Driver内存大小
count: 统计RDD元素个数
first: 获取RDD第一个元素
[可能会启动多个Job,直到获取到1个元素为止,底层是take(1)]
first首先会启动一个Job从0号分区获取第一个元素,如果0号分区没有数据,会再启动一个Job从其他分区获取数据。
take: 获取RDD前N个元素
[可能会启动多个Job,直到获取到N个元素为止]
take首先会启动一个Job从0号分区获取前N个元素,如果0号分区N个数据,会再启动一个Job从其他分区获取剩余的数据。
takeOrdered: 对RDD数据排序之后取前N个元素
[升序,想要实现倒序需要自定义Ordering,重写Compare方法,第一个参数X 第二个参数Y]
降序 {X<Y= + & X>Y = -} 升序 {X<Y= - & X>Y = +}
fold(默认值)(func: (RDD元素类型,RDD元素类型)=>RDD元素类型 ): 对RDD所有元素聚合
聚合原理: 先在每个分区中对分区所有数据聚合,然后将每个分区的聚合结果发给Driver进行汇总
在每个分区内对分区的数据第一次计算的时候,函数第一个参数的初始值 = 默认值
在Driver中第一次汇总的时候,函数第一个参数的初始值 = 默认值
aggregate(默认值)(seqOp: (默认值类型,RDD元素类型)=>默认值类型, comOp: (默认值类型,默认值类型)=>默认值类型): 对RDD所有元素聚合
聚合原理: 先在每个分区中对分区所有数据聚合,然后将每个分区的聚合结果发给Driver进行汇总
seqOp: 用于分区内的聚合逻辑
seqOp在每个分区中第一次聚合的时候,函数第一个参数的初始值 = 默认值
comOp: 用于Driver汇总逻辑
comOp在Driver中第一次汇总的时候,函数第一个参数的初始值 = 默认值
countByKey: 统计每个key出现的总次数 *********
countByKey一般结合sample算子一起使用
出现数据倾斜之后,一般是先使用sample算子会RDD数据采样,然后使用countByKey对样本数据中每个key进行统计,确定哪个key出现了数据倾斜。
foreach(func: RDD元素类型=> Unit):Unit : 对元素遍历
foreach里面的函数是针对RDD每个元素遍历,元素有多少个,函数就执行多少次
foreachPartition(it: Iterator[RDD元素类型]=>Unit ):Unit : 对分区遍历 *********
foreachPartition里面的函数是针对RDD每个分区操作,分区有多少个,函数就执行多少次
foreachPartition一般用于将数据保存到mysql/hbase/redis等存储介质中,可以减少资源连接的创建与销毁
4、序列化
序列化的原因: spark算子里面的函数是在Executor task中执行的,spark算子函数外面的代码是在Driver中执行的,
所以如果spark算子函数体中使用了Driver定义的对象,此时spark会将Driver定义的对象序列化传给Task使用,所以要求该对象必须能够序列化。
spark序列化方式:
spark序列化方式有两种: Java序列化[重量级]、Kryo序列化[轻量级]
Java序列化:类的继承信息、属性值&类型、全类名等信息
Kryo序列化:属性值&类型、全类名等信息
Java序列化的东西比较多,通过网络传输所花费的时间较长,性能较低[Spark默认使用的序列化]
Kryo序列化的性能比Java序列化高10倍左右,所以工作中一般推荐使用Kryo序列化
如何配置spark使用Kryo序列化:
1、在定义sparkconf的时候配置spark序列化方式: new SparkConf().set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
2、注册使用Kryo序列化的类[可选]: new SparkConf().registerKryoClasses(Array( classOf[类名],classOf[类名],... ))
注册使用Kryo序列化的类可做可不做,区别在于: 如果类注册了,序列化的时候不会序列化全类名,反之会序列化全类名
5、依赖关系
血缘关系: 指一个Job中一些列RDD的依赖链条
可以通过 rdd.toDebugString 查看血缘关系
依赖关系: 指父子RDD的关系
可以通过rdd.dependencys查看依赖关系
RDD的依赖关系分为两种:
宽依赖: 有shuffle的称之为宽依赖 [ShuffleDependency]
窄依赖: 没有shuffle的称之为窄依赖 [OneToOneDependency,RangeDependency(union场景<3 union 3 = 6>),PrueDependency]
Application: 应用[一个SparkContext称之为一个应用]
Job: 任务[ 一般一个action算子产生一个job]
stage: 阶段[一个job中stage个数 = shuffle个数+1]
task: 子任务 [一个stage中task个数 = stage最后一个rdd的分区数]
一个Application中多个Job之间是串行的
一个job中多个stage之间是串行的
一个stage中多个task之间是并行的
job中stage的切分流程[从后往前查询切分]:
通过最后一个调用action算子的rdd的依赖关系查询父RDD,然后根据父RDD的依赖再查询父RDD,循环往复,一直查询到第一个RDD为止,在查询的过程中遇到宽依赖则切分stage
job中stage的执行[从前往后执行]:
先执行前面的stage,再执行后面的stage[因为后面stage的输入数据依赖于前面stage的输出数据]
6、持久化
场景:
1、多个Job中共用一个RDD
好处: 在第一个job执行的过程中如果能够将共用RDD数据保存,后续job在执行的时候就可以直接拿取保存的RDD数据使用不用重复计算了
[避免了数据的重复处理]
2、一个Job中RDD的依赖链条太长
好处: 如果RDD计算出错,在恢复数据的时候不用从头计算了,可以拿取保存RDD的数据再计算得到出错的数据
[减少出错时重新计算的时间]
RDD的持久化方式:
1、缓存
数据保存位置: task所在主机的内存/本地磁盘中[各个RDD的分区对应一个Task,每一个Task保存的是对应分区的数据]
使用方式: rdd.cache/ rdd.persist [保存点在可视化DAG图会变成绿点,storage里面会显示保存到缓存的RDD]
cache与persit的区别:
cache其实就是persist(StorageLevel.MEMORY_ONLY),将数据只保存到内存中
persist可以自己指定存储级别,将数据保存到内存/磁盘中
常用的存储级别:
StorageLevel.MEMORY_ONLY: 将数据只保存到内存中[一般用于小数据量场景]
StorageLevel.MEMORY_AND_DISK:一部分数据保存到内存中,一部分保存到磁盘中[用于大数据量场景]
不常用的存储级别:
StorageLevel.MEMORY_AND_DISK_[SER]_[2]:
SER序列化->序列化后减少数据大小,反序列化会增大CPU负担 2->副本数
StorageLevel.OFF_HEAP:保存在堆外内存
不归JVM管理,避免GC带来的性能影响。数据量过大内存不够用这时会内存溢出
2、checkpoint
有了缓存还需要checkpoint的原因: 缓存是将数据保存在内存/本地磁盘中,数据可能丢失,所以需要将数据保存到可靠的存储介质中
数据保存位置: HDFS
使用方式:
1、设置数据保存位置: sc.setCheckpointDir(path)
2、保存数据: rdd.checkpoint
checkpoint会触发一次job执行:
某个rdd要执行checkpoint时,当该RDD所在的一个job执行完成之后,会检查是否有checkpoint操作,
如果有则触发一次job,该job用于收集checkpoint rdd的数据保存到HDFS.
Checkpoint和Cache一般会结合使用:
某个rdd要checkpoint,checkpoint会触发job执行,该RDD之前的数据处理又会重复执行,所以为了避免数据的重复处理,
一般会将checkpoint与缓存结合使用: rdd.cache;rdd.checkpoint; [直接将内存/磁盘的cache拿来checkpoint]
3、缓存与checkpoint的区别
1、数据保存位置不一样
缓存是将数据保存在内存/本地磁盘中
checkpoint是将数据保存到HDFS
2、RDD的依赖关系是否切除不一样
缓存是将数据保存在内存/本地磁盘中,数据可能丢失,丢失之后必须根据依赖关系重新计算得到数据,所以依赖关系必须保留。
checkpoint是将数据保存到HDFS,数据不会丢失,checkpoint之前的依赖关系会切掉。
7、分区器
spark分区器是shuffle的时候决定数据落入子RDD哪个分区中
HashPartitioner
分区规则: (key.hashCode % 子RDD分区数) < 0 ? (key.hashCode % 子RDD分区数) + 子RDD分区数 : key.hashCode % 子RDD分区数
RangePartitioner
分区规则:
1、首先对RDD数据采样,得到(子RDD分区数-1)个key [假设子RDD分区数为3,确定的key为1,5]
2、通过这几个key确定子RDD每个分区的边界 [0号分区的变量: key<=1 ;1号分区的边界:key>1 and key<=5; 2号分区的边界: key>5]
3、拿到数据后按照数据的key放入符合分区边界范围的对应分区中 [后续拿到数据的key之后与每个分区边界对比,将数据的key按分区边界范围放入对应分区中]
[RangePartitioner特性:分区间有序,分区内无序,使用sortBy把每个分区内的数据排序后就能实现全局有序]
8、数据读取与保存
读取数据
读取文本数据: sc.textFile
读取对象文件数据: sc.objectFile[数据类型]
读取序列文件数据: sc.sequenceFile[K的类型,V的类型]
通过指定InputFormat读取hadoop文件: sc.hadoopFile/sc.newHadoopFile
保存数据
保存为文本: rdd.saveAsTextFile
保存为对象文件: rdd.saveAsObjectFile
保存为序列文件: rdd.saveAsSequenceFile
通过指定的OutputFormat保存数据: rdd.saveAsHadoopFile/saveAsNewApiHadoopFile
3、累加器
聚合原理: 在每个分区中对分区内的数据聚合,然后每个分区的聚合结果发给Driver进行全局汇总
场景:用于聚合场景,而且是聚合结果不太大的场景【因为最终汇总是在Driver中,Driver内存不会特别大】
好处:能够一定程度上避免shuffle
自定义累加器:
1、定义class继承AccumulatorV2[IN,OUT]
IN: 代表分区中累加的元素类型
OUT: 代表累加器最终结果类型
2、重写抽象方法
自定义累加器的使用:
1、创建自定义累加器对象: val acc = new XXX()
2、注册到sparkcontext: sc.register(acc,"累加器名称")
3、在spark算子中使用累加器累加数据: x=> acc.add(...)
4、获取最终累加结果: acc.value
自定义累加器
0、创建一个容器,装载中间结果数据
1、判断累加器是否为空
2、复制累加器
3、重置累加器
4、累加元素[在分区中累加]
5、汇总分区结果[在DRIVER中汇总]
6、返回累加器最终结果
自定义累加器的使用代码
def main(args: Array[String]): Unit = {
import org.apache.spark.{SparkConf, SparkContext}
val sc = new SparkContext( new SparkConf().setMaster("local[4]").setAppName("TEST") )
//创建自定义累加器对象: val acc = new XXX()
val acc = new UserDefinedAccumulator
//注册到sparkcontext: sc.register(acc,"累加器名称")
sc.register(acc,"wcAcc")
val rdd1 = sc.textFile("datas/wc.txt")
val rdd2 = rdd1.flatMap(_.split(" "))
val rdd3 = rdd2.map((_,1))
//在spark算子中使用累加器累加数据: x=> acc.add(...)
rdd3.foreach(x=> acc.add(x))
//获取最终累加结果: acc.value
println(acc.value)
Thread.sleep(10000000)
}
自定义累加器代码:
class UserDefinedAccumulator extends AccumulatorV2[(String,Int),Map[String,Int]]{
//创建一个容器,装载中间结果数据
val map = mutable.Map[String,Int]()
/**
* 判断累加器是否为空
* @return
*/
override def isZero: Boolean = map.isEmpty
/**
* 复制累加器
* @return
*/
override def copy(): AccumulatorV2[(String, Int), Map[String, Int]] = new UserDefinedAccumulator
/**
* 重置累加器
*/
override def reset(): Unit = map.clear()
/**
* 累加元素[在分区中累加]
* @param v
*/
override def add(v: (String, Int)): Unit = {
println(s"add ${Thread.currentThread().getName} -- task累加的某个元素:${v} task累加中间结果:${map}")
//判断单词是否在中间结果容器中存在,存在则累加此时,不存在则直接插入
val num = map.getOrElse(v._1,0) + v._2
map.+=( (v._1,num) )
}
/**
* 汇总分区结果[在DRIVER中汇总]
* @param other
*/
override def merge(other: AccumulatorV2[(String, Int), Map[String, Int]]): Unit = {
println(s"merge ${Thread.currentThread().getName} -- task累加结果:${other.value} Driver中间结果:${map}")
//获取task的累加结果
val taskMap = other.value
//遍历task累加的每个元素
taskMap.foreach(x=> {
//判断hello在acc.map中是否存在,如果存在累加次数,如果不存在直接插入
val num = map.getOrElse(x._1, 0) + x._2
map.put(x._1,num)
})
}
/**
* 返回累加器最终结果 [将可变map 转换成不可变map]
* @return
*/
override def value: Map[String, Int] = map.toMap
}
4、广播变量
场景:
1、spark算子中需要使用到Drvier数据,而且该数还有一定大小[有几十上百兆]
好处: 程序运行过程中数据占用的内存变少
默认情况下,算子函数中使用Driver数据的时候,该数据在程序运行过程中占用的总内存大小 = task个数 * 数据大小。
默认情况下占用的内存过大,将Driver数据广播给Executor后,此时数据在程序运行过程中占用的总内存大小 = executor个数 * 数据大小
2、大表 join 小表
好处: 能够减少shuffle
只需要使用collcet将小表的数据收集到Driver,然后将小表数据广播出去,后续对大表数据使用map算子,在算子中获取广播小表数据对大表数据进行转换。
使用广播变量
1、广播数据: val bc = sc.broadcast(数据)
2、task使用广播数据: bc.value [bc应该是在算子函数体中使用]