Spark分布式计算机原理

一、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工作原理

  1. 根据RDD之间的依赖关系,形成一个DAG(有向无环图)
  2. DAGScheduler将DAG划分为多个Stage
    1)划分依据:是否发生宽依赖(Shuffle)
    2)划分规则:从后往前,遇到宽依赖切割为新的Stage
    3)每个Stage由一组并行的Task组成

在这里插入图片描述

  1. 为什么需要划分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缓存

  1. RDD缓存机制:缓存数据至内存/磁盘,可大幅度提升Spark应用性能
  • cache=persist(MEMORY ONLY)
  • persist
  1. 缓存策略StorageLevel
  • MEMORY_ONLY(默认)
  • MEMORY_AND_DISK
  • DISK_ONLY
  • ……
  1. 缓存应用场景
  • 从文件加载数据之后,因为重新获取文件成本较高
  • 经过较多的算子变换之后,重新计算成本较高
  • 单个非常消耗资源的算子之后
  1. 使用注意事项
  • 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分区设计
  1. 分区大小限制为2GB
  2. 分区太少
  • 不利于并发
  • 更容易受数据倾斜影响
  • groupBy, reduceByKey, sortByKey等内存压力增大
  1. 分区过多
  • Shuffle开销越大
  • 创建任务开销越大
  1. 经验
  • 每个分区大约128MB
  • 如果分区小于但接近2000,则设置为大于2000
  • 如果分区大于2000,Shuffle会使用不同的数据结构
四、数据倾斜

数据倾斜时指分区中的数据分配不均匀,数据集中在少数分区中

  • 严重影响性能
  • 通常发生在groupBy,join等之后
    在这里插入图片描述
    解决方案:
    使用新的Hash值(如对key加盐) 重新分区

详细优化方案:Spark性能优化指南——高级篇

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值