一、RDD的依赖关系
Lineage:血统、遗传
1.RDD最重要的特性之一,保存了RDD的依赖关系
2.RDD实现了基于Lineage的容错机制
依赖关系
1.宽依赖
一个父RDD的分区被子RDD的多个分区使用
2.窄依赖
一个父RDD的分区被子RDD的一个分区使用
宽依赖对比窄依赖:
1.宽依赖对应shuffle操作,需要在运行时将同一个父RDD的分区传入到不同的子RDD分区中,不同的分区可能位于不同的节点,就可能涉及多个节点间数据传输
2.当RDD分区丢失时,Spark会对数据进行重新计算,对于窄依赖只需重新计算一次子RDD的父RDD分区
结论:相比于宽依赖,窄依赖对优化更有利,即资源占用较少,运行更快
常见算子依赖关系:
窄依赖:map、flarMap、filter、union
宽依赖:distinct、reduceByKey、groupByKey、sortByKey、join
二、DAG工作原理
- 根据RDD之间的依赖关系,形成一个DAG(有向无环图)
- DAGScheduler将DAG划分为多个Stage
1)划分依据:是否发生宽依赖(Shuffle)
2)划分规则:从后往前,遇到宽依赖切割为新的Stage
3)每个Stage由一组并行的Task组成
- 为什么需要划分Stage
1)数据本地化
1.移动计算,而不是移动数据
2.保证一个Stage内不会发生数据移动
2)最佳实践
1.尽量避免Shuffle
2.提前部分聚合减少数据移动
三、Spark Shuffle过程
在分区之间重新分配数据
- 父RDD中同一分区中的数据按照算子要求重新进入子RDD的不同分区中
- 中间结果写入磁盘
- 由子RDD拉取数据,而不是由父RDD推送
- 默认情况下,Shuffle不会改变分区数量
代码对比:
代码块一:
sc.textFile("hdfs:/data/test/input/names.txt")
.map(name=>(name.charAt(0),name)) //窄依赖
.groupByKey() //宽依赖
.mapValues(names=>names.toSet.size) //窄依赖
.collect()
代码块二:
sc.textFile("hdfs:/data/test/input/names.txt")
.distinct(numPartitions=6) //宽依赖
.map(name=>(name.charAt(0),1)) //窄依赖
.reduceByKey(_+_) //宽依赖
.collect()
两个代码块运行结果相同,相比而言,代码块一要优于代码块二,代码块二宽依赖多于代码块二,运行时所占用资源多
四、RDD优化
一、RDD持久化
cache缓存
- RDD缓存机制:缓存数据至内存/磁盘,可大幅度提升Spark应用性能
- cache=persist(MEMORY ONLY)
- persist
- 缓存策略StorageLevel
- MEMORY_ONLY(默认)
- MEMORY_AND_DISK
- DISK_ONLY
- ……
- 缓存应用场景
- 从文件加载数据之后,因为重新获取文件成本较高
- 经过较多的算子变换之后,重新计算成本较高
- 单个非常消耗资源的算子之后
- 使用注意事项
- cache()或persist()后不能再有其他算子
- cache()或persist()遇到Action算子完成后才生效
对比示例:
//装载文件
val rdd = sc.textFile("in/users.csv")
println("---rdd未缓存,求读取count时间---")
var start = System.currentTimeMillis()
println(rdd.count())
var end = System.currentTimeMillis()
println("读取时间:"+(end-start))
println("---将rdd缓存后,求读取count时间---")
//启用缓存
rdd.cache()
//行动算子,生成缓存
rdd.collect
var start1 = System.currentTimeMillis()
println(rdd.count())
var end1 = System.currentTimeMillis()
println("读取时间:"+(end1-start1))
//缓存失效
println("---rdd缓存失效后,读取count时间---")
rdd1.unpersist()
var start2 = System.currentTimeMillis()
println(rdd.count())
var end2 = System.currentTimeMillis()
println(读取时间:"+(end2-start2))
Checkpoint检查点
检查点类似于快照
//设置检查点路径
sc.setCheckpointDir("file:///C:/Users/Lenovo/Desktop/checkpoint")
val rdd = sc.parallelize(List(("a",1),("a",2),("b",1),("b",2)))
rdd.checkpoint()
rdd.collect() //行动算子生成快照
println(rdd.isCheckpointed)
println(rdd.getCheckpointFile)
检查点与缓存的区别:
- 检查点会删除RDD lineage,而缓存不会
- SparkContext被销毁后,检查点数据不会被删除
二、RDD共享变量
广播变量:允许开发者将一个只读变量(Driver端)缓存到每个节点(Executor)上,而不是每个任务传递一个副本
//定义广播变量
val broad=sc.broadcast(Array(1,2,3))
//访问方式
broad.value
注意事项:
1、Driver端变量在每个Executor每个Task保存一个变量副本
2、Driver端广播变量在每个Executor只保存一个变量副本
示例:
val arr=Array("hello","hi","good")
//定义广播
val broad = sc.broadcast(arr)
val rdd = sc.makeRDD(List((1,"张三"),(2,"李四"),(3,"王五")))
val rdd2 = rdd.mapValues(x => {
println(x)
//不使用广播,会一个一个传,在每个Task中保存副本,因此效率上要慢
println(arr.toList)
arr(2)+":"+x
//使用广播,在整个excutor中保存一个副本
println(broad.value.toList)
broad.value(0) + ":" + x
})
rdd2.foreach(println)
不使用广播:
使用广播:
三、RDD分区设计
- 分区大小限制为2GB
- 分区太少
- 不利于并发
- 更容易受数据倾斜影响
- groupBy, reduceByKey, sortByKey等内存压力增大
- 分区过多
- Shuffle开销越大
- 创建任务开销越大
- 经验
- 每个分区大约128MB
- 如果分区小于但接近2000,则设置为大于2000
- 如果分区大于2000,Shuffle会使用不同的数据结构
四、数据倾斜
数据倾斜时指分区中的数据分配不均匀,数据集中在少数分区中
- 严重影响性能
- 通常发生在groupBy,join等之后
解决方案:
使用新的Hash值(如对key加盐) 重新分区
详细优化方案:Spark性能优化指南——高级篇