RDD:弹性分布式数据集
RDD类似于java中的io流,RDD中不存储数据,当触发任务计算的时候拿到数据处理完成之后马上交给下一层RDD
特性:
不存储数据(RDD中只是封装了数据的处理逻辑)
不可变(RDD是不可以改变的,想要改变,只能产生新的RDD,在新的RDD里封装逻辑)
弹性:(存储、容错、计算、分片的弹性)
分布式(存储在集群的不同节点)
数据抽象(RDD是一个抽象类,需要子类实现)
注意:
所有的RDD算子操作(Transformation转换算子)都是在Executor端执行的,
RDD算子之外的操作(action行动算子)都是在Driver端执行
只有遇到行动算子,才会执行RDD的计算操作(延迟计算))
特点:
一组分区列表
作用于每个分区的计算函数
对于其他的RDD的依赖关系
分区器
优先位置
RDD编程:
在pom文件中添加spark-core的依赖和scala的编译插件
<dependencies>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
<build>
<finalName>SparkCoreTest</finalName>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.4.6</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
RDD创建:
import org.apache.spark.{SparkConf, SparkContext}
val sc = new SparkContext(new SparkConf().setMaster("local[4]").setAppName("test"))
/**
* RDD的创建方式
* 1、通过本地集合创建
* 2、通过读取文件创建
* 3、通过其他RDD衍生
*/
/**
* 通过集合创建RDD 【工作中一般用于测试】
* sc.makeRDD(集合)
* sc.parallelize(集合)
*/
@Test //@Test注解必须用在class中,标准的方法必须是无返回值的方法
def createRddByLocalCollection(){
new $01_RDDCreate
val list = List(1,4,3,2,5)
//val rdd = sc.makeRDD(list)
val rdd = sc.parallelize(list)
val arr = rdd.collect()
println(arr.toList)
}
/**
* 通过读取文件创建RDD
*/
@Test
def createRddByFile(): Unit ={
//System.setProperty("HADOOP_USER_NAME","atguigu")
val rdd = sc.textFile("datas/product.txt")
//读取HDFS文件
//val rdd = sc.textFile("hdfs://hadoop102:8020/datas/products.txt")
println(rdd.collect().toList)
}
/**
* 3、通过其他RDD衍生
*/
@Test
def createRddByRdd(): Unit ={
val rdd = sc.textFile("datas/product.txt")
val rdd2 = rdd.flatMap(_.split("\t"))
println(rdd2.collect().toList)
}
RDD分区数:
根据本地集合创建的RDD的分区数:
如果有设置numSlices参数,此时RDD的分区数 = 设置的numSlice的值
val rdd: RDD[Int] = sc.parallelize(List(1,3,6,5,2,7,9,10),8)
工作中通过设置spark.default.parallelism的值来设置默认分区数
val sc = new SparkContext(new SparkConf() /*.set("spark.default.parallelism","6")*/ .setMaster("local[4]").setAppName("test"))
查看RDD的分区数:
rdd.getNumPartitions/rdd.partitions.length
根据读取文件创建RDD的分区数:
如果有设置minPartitions的值,RDD的分区数>= minPartitions的值
如果没有设置minPartitins的值,RDD的分区数>=math.min(defaultParallelism, 2)
通过读取文件创建RDD的分区数最终由文件的切片数决定,一个切片一个分区
由其他RDD衍生出的新RDD的分区数 :
依赖的第一个父RDD的分区数
Transformation转换算子:
*** value值类型:
map:一对一映射
@Test
def map(): Unit ={
val rdd = sc.parallelize(List(1,3,2,4,5,6,8,10))
val rdd2 = rdd.map(x=> {
println(s"${Thread.currentThread().getName} --${x}")
x * 10
})
println(rdd2.collect().toList)
}
mapPatitions:(func:Iterator[RDD元素类型] => Iterator[B])(原RDD一个分区计算得到新RDD一个分区)
有多少个分区就操作多少次
应用场景:一般用于mysql、Redis、hbase等存储介质查询数据,此时如果直接将建立连接提到map方法外,但是由于connect没有序列化方法会报错,可以减少资源连接创建与销毁时间提高效率
*** :迭代器iterator只能用一次,使用后就失效
map与mapPartitions的区别:
1、函数针对的对象不同(map是每个分区中每个元素,mappartitions是针对每个分区)
2、函数返回值不同:(map是返回新元素结果,mapPatitions是针对每个分区所有数据的迭代器操作,要求返回一个新的迭代器,新的迭代器是一个分区的所有数据,所以新RDD个数不一定等于原RDD元素个数)
3、对象内存回收的时间不一样(map是针对元素操作,元素操作完成之后就内存回收了,而mapPartitions是对分区的迭代器,所以必须等到该迭代器中所有数据都操作完成才能整体回收。此时如果RDD一个分区数据量特别大,可能出现内存溢出,此时可以使用map代替)
mapPartitionsWithIndex(Int,Iterator[RDD元素类型]=>Iterator[B])
mapPartitions与mapParititonsWithIndex的区别:
mapPartitionWithIndex里面的函数相比mapPartitions里面的函数多了个分区号的参数
flatMap:扁平化数据(func:RDD元素类型=>集合)
@Test
def flatMap(): Unit ={
val rdd = sc.parallelize(List("hello java spark","spark hadoop flume","flume kafka spark","spark hadoop"))
val rdd2 = rdd.flatMap(x=> x.split(" "))
println(rdd2.collect().toList)
}
filter:过滤数据,返回值为true时过滤
groupBy:
有shuffle操作(判断条件:是否将各个分区的数据聚合,父RDD的数据被多个子RDD使用,只作用于有k-v键值对的数据)
distinct:去重
rdd1.distinct(rdd ) -> 有shuffle操作
coalesce:改变分区数
减少分区:rdd.coalesce(分区数) -> (*没有shuffle操作,父RDD的数据直接写到一个子RDD中)
增加分区:rdd.coalesce(分区数,true) ->(有shuffle操作,调用了hashpartition)
repartition:重分区
增加/减少分区:rdd.repartition() -> 都有shuffle操作
repartition与coalesce区别:
coalesce默认只能减少分区数,想增大需要shuffle=true,会产生shuffle(一般和filter搭配过滤减少分区数)
repartition可增可减,但是都会有shuffle(一般用于增大分区数)
sortBy(func:集合元素类型=>K):按照指定字段排序
sortBy是按照函数的返回值对元素进行排序,有shuffle操作
升序:rdd1.sortBy(rdd)
降序:rdd1.sortBy(rdd,false)->设置ascending参数为false
双value值类型:
intersection:交集 rdd3 = rdd1.intersection(rdd2)(两次shuffle,rdd1数据到rdd3,rdd2到rdd3)
subtract:差集(两次shuffle)
union:并集(无shuffle)不去重
zip:拉链(无shuffle)要求元素个数与分区数一致
key-value键值对类型:
partitionBy:按照key值重新分区
** 自定义分区:继承partitioner抽象类,重写numpartitions和getpartition方法
class UserDefinedPartitioner(num:Int) extends Partitioner{
//spark后续内部调用获取重分区的分区数
override def numPartitions: Int = if(num<5) 5 else num
//根据key获取分区号,后续shuffle的时候该key的数据放入分区号对应的分区中
override def getPartition(key: Any): Int = key match {
case "aa" =>0
case "dd" =>1
case x =>
val r = x.hashCode() % num
if(r <0) r+num
else r
groupByKey:按照key进行分组
返回的元素类型是kv键值对,K是原RDD元素的key(此时k里是唯一一条数据),V是集合,里面是K对应的原RDD所有value值
*** reduceByKey(func:(value,value)=>value):按照key分组并且对key对应的value值聚合,相比于map和groupBy效率要高
rdd1.reduceByKey((a, b) => a + b)
** combineByKey:转换结构和分区间操作,针对相同k,将v合并成一个集合
rdd.combineByKey(createCombiner,mergeValue,mergeCombiners)
createCombiner:转换数据结构,对每个组第一个value值转换 Value值类型=>B类型
mergeValue:分区内聚合combiner阶段 (B,value类型)=>B
mergeCombiners:分区间聚合reducer阶段 (B,B)=>B
foldByKey:和reduceByKey的区别就是聚合初始值为默认值0,reduce为第一个元素
rdd1.foldByKey(默认值)((a,b)=>{a+b})
aggregateByKey:和reduceByKey的区别就是聚合初始值为默认值(0,0),reduce为第一个元素
rdd1.aggregateByKey(默认值)((a,b)=>{a+b})
四种ByKey的区别:
reduceByKey:combiner与reducer计算逻辑一样,用的同一个计算函数
combineByKey:两个阶段计算逻辑可以自定义,可以不一致
sortByKey:按照key排序,和sortBy原理一样
案例处理:
1、读取数据->2、是否过滤、列裁剪、去重
action行动算子:
collect:用来收集RDD每个分区数据并将数据用数组封装之后返回给Driver
如果RDD所有分区数据量过大,collect数据是返回放在Driver内存中,内存默认为1G,所以,工作中Driver一般为5-10G,通过bin/spark-submit --driver-memory 10G设置。
take:获取RDD前N个元素 rdd.take(N)
take是首先从0号分区尝试获取N个元素,如果0号分区没有N个元素,会再启动一个job获取剩余的元素(可能启动两个job)
countByKey:统计RDD中每个Key的个数
** foreach:对RDD每个元素遍历(func:RDD元素类型=>Unit)
调用Scala中的foreach方法,多线程并行
*** foreachPartition:对分区的所有元素的迭代器处理
如果不需要返回值,一般用于将数据保存在mysql/hbase/redis等位置。也可以用来减少连接创建和销毁的次数
RDD序列化:
Spark算子里面的代码是在Executor中的task执行的,Spark算子外面的代码是在Driver执行的,如果spark算子使用了Driver定义的对象(类似于闭包)
就必须要求Driver将该对象序列化之后传递给task才能使用
spark序列化分为两种:
java序列化:序列化将类的继承信息,全类名,属性名,属性类型,属性值的信息全部序列化
Kry序列化:序列化只将类的全类名,属性名,属性类型,属性值序列化
Kryo序列化要快十倍左右,但是spark默认使用java序列化,Kryo需要设置
*** 配置Kryo序列化:(工作一般要配置)
在创建sparkconf的时候设置spark默认的序列化方式:new SparkConf().set("spark.serializer","ora.apache.spark.serializer.KryoSerializer")
RDD依赖关系:
血统:指从第一个RDD到当前RDD的链条,使用toDebugString可以查看血统
父子RDD的关系:
通过dependencies查看
有shuffle的称为宽依赖、没有shuffle的称为窄依赖
*** Application:应用,一个sparkContext称之为一个应用
Job:任务(一般一个action算子产生一个job)
stage:阶段(一个job中stage的个数 = job中shuffle分数 + 1),job中前面的stage先执行,后面的后执行
stage切分:根据最后一个RDD的依赖从后往前一次查找,一直找到第一个RDD为止,在查询的过程中遇到宽依赖则切分
task:task并行的前提是task之间不能存在依赖(不能存在shuffle),所以task有串行与并行两种模式,一个stage中task个数 = stage最后一个RDD的分区个数
为什么RDD是惰性的
RDD是迭代器不存数据,封装的是运算的逻辑,当collect的时候才一层层往前请求数据
RDD持久化:
问题场景1、RDD在多个job中重复使用
此时默认该RDD之前的处理步骤是每个job都会执行,数据重复处理影响效率
解决方案:将RDD数据保存下来供其他job执行,则不用重复执行了
2、当一个job依赖链条很长
如果依赖料条太长,某个环节计算出错需要重新计算得到数据,浪费时间
解决方案:将RDD数据保存,后续计算出错拿出保存数据计算结果,减少计算时间
如何持久化:
方案1:缓存
保存位置:保存所在分区的内存或磁盘中( rdd.cache()或rdd.persist() ,两个方法区别:cache全部保存在内存中,persist根据设置的存储级别分别保存在内存或磁盘中(rdd.persist(StorageLevel.级别))
persist常用存储级别:MEMORY——ONLY(只保存在磁盘,一般用于数据量小场景)、MEMORY_AND_DISK(一部分保存在内存,一部分保存在磁盘,一般用于大数据量场景)
数据保存时机:在执行job的时候就保存
方案2:checkpoint
场景:缓存是将数据保存在服务器本地磁盘/内存中,如果服务器宕机数据丢失,后续job执行的时候需要根据RDD的关系重新执行得出数据
所以需要将数据保存到可靠的存储介质hdfs中,不会出现数据丢失的问题
配置:1、设置checkpoint数据保存路径 -> sc.setCheckpoint(path)
2、保存rdd数据 -> rdd.checkpoint()
数据保存时机:在执行完job之后,另外单独启动一个job,计算checkpoint的到rdd的数据保存
checkpoint操作会单独触发一个job执行得到数据再保存,此时该checkpoint rdd之前的数据处理会重复执行一次,所以为了避免重复执行,会先rdd.cache,在本地内存保存,然后再rdd.checkpoint,就可以直接读取本地缓存的数据
所以checkpoint一般是配合cache使用
shuffle算子相当于rdd.persist(StorageLevel.DISK),shuffle会落盘,已经缓存一次了
RDD数据分区:
HashPartitioner
分区规则:key.hashCode % 分区数
弊端:可能导致每个分区数据量不均匀
rdd2 = rdd1.partitionBy(new HashPartitioner( 3 ))
RangePartitioner( sortBy底层分区就是用的Rangepartitioner )
分区规则:1、对RDD所有数据的key抽样,通过采样的结果确定分区数-1个key
2、通过这些采样的key确定RDD的每个分区的边界
3、后续拿到数据的key之后与分区边界对比,如果可以处于分区边界范围内则将数据放入该分区中
rdd2 = rdd1.partitionBy( new Rangepartitioner( 3 ,rdd1 ) )
*** 累加器:
应用场景:只能用于聚合场景并且聚合之后数据量不是特别大的场景
好处 -> 使用累加器可以避免shuffle操作(shuffle少,速度快,避免写磁盘)
原理:现在每个分区中对数据累加,然后将累加的结果发给Driver汇总
默认累加器:
val sc = new SparkContext(new SparkConf().setAppName("test").setMaster("local[4]"))
val acc = sc.longAccumulator("acc_sum")
val rdd = sc.parallelize(List(1, 2, 3, 4, 5, 6))
rdd.foreach(x => acc.add(x))
println(acc.value)
自定义累加器:
1、创建class继承AccumulatorV2 [IN,OUT]
IN:代表task中累加的元素类型
OUT:代表driver汇总之后的最终结果类型
2、重写抽象方法
自定义add方法和merge方法
3、使用自定义累加器:
1、创建自定义累加器对象:val acc = new XXXX
2、将创建的对象注册到sparkcontext中:sc.register(acc,"累加器名称“)
3、将executor中的数据累加rdd.foreach(x=>acc.add(x))
4、返回累加器结果:acc.value
*** 广播变量:
场景:1、spark行动算子使用到Driver数据 -> 工作中的RDD的分区数一般设置为任务总CPU个数的2-3倍,当task需要使用driver中的数据时,driver需要向所有task发送数据,占用内存过大。
所以需要一个广播变量,将Driver的数据广播给Executor,后续task需要数据时从Executor获取使用即可,减小内存空间占用
2、大表join小表 -> 好处:默认会产生shuffle操作,此时小表广播后可避免
用法:1、广播driver数据 -> val bc = sc.broadcast (数据)
2、获取广播变量使用 -> bc.value