Spark入门必知:DAG、Cache、RDD、CheckPoint

目录

前言

关键字

DAG(有向无环图)

概念

DAG 解决了什么问题

工作流程

Cache 缓存

为什么要用cache

cache的使用

注意点

cache存储级别

如何选择存储级别

checkPoint(检查点)

cache和checkpoint的区别

RDD(弹性数据集)

宽窄依赖

1.窄依赖(1对1 | N对1)

2.宽依赖(一对多)

分区优化

Stage (阶段)

概念

划分规则

Spark案例


前言

这里只向大家介绍几个关键的关键字知识点,详细系统学习还是建议去B站

关键字

DAG(有向无环图) cache(缓存)  checkpoint(检查点) RDD(弹性数据集) 算子

其中算子可以理解为Java里方法的调用,每 点. 一次就是调用一次方法,Spark的算子与Scala的算子有很多共同之处,在学习Spark之前可以参考我前面 Scala常用算子 博客,里面对算子进行了分类

DAG(有向无环图)

概念

DAG 是一组顶点和边的组合。顶点代表了 RDD, 边代表了对 RDD 的一系列操作。

DAG Scheduler 会根据 RDD 的 transformation 动作,将 DAG 分为不同的 stage,每个 stage 中分为多个 task,这些 task 可以并行运行。

DAG 数据结构不是链表,而是 Graph 图。Graph 上有很多节点,也叫做 Vertices 顶点,连接两个节点的叫做 edges 边。没错,就是咱们中小学数学课上学到的顶点和边的概念。链表,Tree ,图是三个复杂度递进的数据结构。链表就是一条有方向的线。Tree 是有分叉的,但是任意两个节点间只有一条路径能到达另外一点,也就是不能形成闭合的图形。

DAG 解决了什么问题

DAG 的出现主要是为了解决 Hadoop MapReduce 框架的局限性。那么 MapReduce 有什么局限性呢?

主要有两个:

  • 每个 MapReduce 操作都是相互独立的,HADOOP不知道接下来会有哪些Map Reduce。
  • 每一步的输出结果,都会持久化到硬盘或者 HDFS 上。

当以上两个特点结合之后,我们就可以想象,如果在某些迭代的场景下,MapReduce 框架会对硬盘和 HDFS 的读写造成大量浪费。

而且每一步都是堵塞在上一步中,所以当我们处理复杂计算时,会需要很长时间,但是数据量却不大。

所以 Spark 中引入了 DAG,它可以优化计算计划,比如减少 shuffle 数据。

工作流程

  1. 解释器是第一层。Spark 通过使用Scala解释器,来解释代码,并会对代码做一些修改。
  2. 在Spark控制台中输入代码时,Spark会创建一个 operator graph, 来记录各个操作。
  3. 当一个 RDD 的 Action 动作被调用时, Spark 就会把这个 operator graph 提交到 DAG scheduler 上。
  4. DAG Scheduler 会把 operator graph 分为各个 stage。 一个 stage 包含基于输入数据分区的task。DAG scheduler 会把各个操作连接在一起。
  5. 这些 Stage 将传递给 Task Scheduler。Task Scheduler 通过 cluster manager 启动任务。Stage 任务的依赖关系, task scheduler 是不知道的。
  6. 在 slave 机器上的 Worker 们执行 task。

Cache 缓存

为什么要用cache

spark中一个job是由RDD的一连串transformation操作和一个action操作组成。只有当执行到action操作代码时才会触发生成真正的job,从而根据action操作需要的RDD及其依赖的所有RDD转换操作形成实际的任务。也就是会从源头输入数据开始执行整个计算过程,并没有如我们想的单机程序那样达到RDD复用的目的。

为了达到RDD复用的目的,就需要对想要复用的RDD进行cache,RDD的缓存与释放都是需要我们显示操作的。

cache的使用

spark的cache使用简单,只需要调用cache或persist方法即可,而且可以看到两个方法实际都是调用的都是persist方法

def cache(): this.type = persist()
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)

注意点

cache之后一定不能直接去接算子。因为cache后有算子的话,它每次都会重新触发这个计算过程,从而导致cache失效。

cache操作需要当第一个使用到它的job执行后才会生效,而不是cache后马上可用,这是spark框架的延迟计算导致的。可能粗想起来也不会有什么问题,但是不正确的使用unpersist操作,也可能会导致cache失效。如下例子所示,在action操作之前就把缓存释放掉:

val data = sc.textFile(“data.csv”)
.flatMap(.split(“,”))
.cache() val data1 = data.map(word => (word, 1))
.reduceByKey( + ) val data2 = data.map(word => (word, word.length)).reduceByKey( + _)

data.unpersist()

val wordCount1 = data1.count()

val wordCount2 = data2.count()

如何释放cache缓存:unpersist,它是立即执行的。persist是lazy级别的(没有计算),unpersist是eager级别的。RDD cache的生命周期是application级别的,也就是如果不显示unpersist释放缓存,RDD会一直存在(虽然当内存不够时按LRU算法进行清除),如果不正确地进行unpersist,让无用的RDD占用executor内存,会导致资源的浪费,影响任务的效率。

cache存储级别

每个持久化的 RDD 可以使用不同的存储级别进行缓存,例如,持久化到磁盘、已序列化的 Java 对象形式持久化到内存(可以节省空间)、跨节点间复制、以 off-heap 的方式存储在 Tachyon。这些存储级别通过传递一个 StorageLevel 对象给 persist() 方法进行设置。

详细的存储级别介绍如下

MEMORY_ONLY

使用未序列化的Java对象格式,将数据保存在内存中。如果内存不够存放所有的数据,则数据可能就不会进行持久化。那么下次对这个RDD执行算子操作时,那些没有被持久化的数据,需要从源头处重新计算一遍。如果RDD中数据量比较大时,会导致JVM的OOM内存溢出异常。这是RDD的默认持久化级别。

MEMORY_AND_DISK

使用未序列化的Java对象格式,优先尝试将数据保存在内存中。如果内存不够存放所有的数据,会将数据写入磁盘文件中,下次对这个RDD执行算子时,持久化在磁盘文件中的数据会被读取出来使用。这是DataFrame的默认持久化级别。

MEMORY_ONLY_SER/MEMORY_AND_DISK_SER

基本含义同MEMORY_ONLY/MEMORY_AND_DISK。唯一的区别是,会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组。这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。

这两种策略都是对MEMORY_ONLY/MEMORY_AND_DISK策略的补充。

DISK_ONLY

使用未序列化的Java对象格式,将数据全部写入磁盘文件中。

MEMORY_ONLY_2/MEMORY_AND_DISK_2 etc..

对于上述任意一种持久化策略,如果加上后缀_2,代表的是将每个持久化的数据,都复制一份副本,并将副本保存到其他节点上。这种基于副本的持久化机制主要用于进行容错。假如某个节点挂掉,节点的内存或磁盘中的持久化数据丢失了,那么后续对RDD计算时还可以使用该数据在其他节点上的副本。如果没有副本的话,就只能将这些数据从源头处重新计算一遍了。

通过源码,实际上一共有12中缓存级别

object StorageLevel {
val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(true, true, true, false, 1)
.....

}

如何选择存储级别

针对占用内存不是很大的中间计算结果优先采用MEMORY_ONLY,它的性能最好(前提是内存要足够),也是RDD的默认策略。如果中间结果数据量稍大,可以采用MEMORY_ONLY_SER策略进行补充。但在实际的生产环境中,大多数情况下数据量都是超出内存容量的,这可能会导致JVM的OOM内存溢出异常。

如果内存无法容纳中间数据,那么建议使用MEMORY_AND_DISK_SER策略,该策略会优先将数据缓存在内存中,只有当内存不够时才会把数据写入磁盘。另外对于分布式任务,IO资源往往比CPU资源更加紧张,序列化后的数据可以节省内存和磁盘的空间开销。

通常很少使用DISK_ONLY级别,它表示数据量已经非常大,远大于内存的容量。这个时候需要慎重权衡重新计算RDD的消耗和从磁盘加载RDD的消耗。

除非对于高可用性的任务,否则不建议使用后缀为_2的级别。因为在内存中复制多份数据很难有足够的内存资源满足,而对于HDFS文件本身已经有多备份保证数据的可靠性。

对于实际缓存的效果,可以查看spark UI中的storage页面,里面详细描述了缓存的每个RDD的数据缓存分布情况。

checkPoint(检查点)

spark计算里面 计算流程DAG特别长,服务器需要将整个DAG计算完成得出结果,

如果中间算出的数据突然丢失了,spark又会根据RDD的依赖关系从头到尾计算一遍,这样子就很费性能

当然我们可以将中间的计算结果通过cache或者persist放到内存或者磁盘中,但是这样也不能保证数据完全不会丢失,存储的这个内存出问题了或者磁盘坏了,也会导致spark从头再根据RDD计算一遍

所以就有了checkpoint,其中checkpoint的作用就是将DAG中比较重要的中间数据做一个检查点将结果存储到一个高可用的地方(HDFS,利用HDFS的默认存储策略)

cache和checkpoint的区别:

缓存(cache)把 RDD 计算出来然后放在内存中,但是RDD 的依赖链(相当于数据库中的redo 日志),也不能丢掉,当某个点某个 executor 宕了,上面cache 的RDD就会丢掉,需要通过依赖链重放计算出来。不同的是,checkpoint是把 RDD 保存在 HDFS中, 是多副本可靠存储,所以依赖链就可以丢掉了,就斩断了依赖链, 是通过复制实现的高容错。

RDD(弹性数据集)

Resilient Distributed Datasets 弹性分布式数据集,是一个容错的、并行的数据结构,可以让用户显式地将数据存储到磁盘和内存中,并能控制数据的分区。同时,RDD还提供了一组丰富的操作来操作这些数据。RDD是只读的记录分区的集合,只能通过在其他RDD执行确定的转换操作(transformation操作)而创建。RDD可看作一个spark的对象,它本身存在于内存中,如对文件计算是一个RDD,等。

一个RDD可以包含多个分区,每个分区就是一个dataset片段。RDD可以相互依赖。如果RDD的每个分区最多只能被一个Child RDD的一个分区使用,则称之为narrow dependency;若多个Child RDD分区都可以依赖,则称之为wide dependency。

宽窄依赖

RDD的依赖关系分为两种模型,一种是窄依赖(narrow dependency)和宽依赖(wide dependency)。

1.窄依赖(1对1 | N对1)

指父RDD的每一个分区最多被一个子RDD的分区所用,表现为一个父RDD的分区对应于一个子RDD的分区(第一类),或者是多个父RDD的分区对应于一个RDD的分区(第二类),也就是说一个父RDD的一个分区不可能对应于一个子RDD的多个分区。

如下图所示,对输入进行协同划分(co-partitioned)的join属于第二类。当子RDD的分区依赖于单个父RDD的分区的时候,分区的结构不会发生改变,如下图中的map,filter等操作,相反的,对于一个子RDD的分区依赖于多个RDD的分区的时候,分区的结构会发生改变

2.宽依赖(一对多)

宽依赖是值子RDD的每一个分区都要依赖于所有父RDD的所有分区或者多个分区。也就是说存在一个父RDD的一个分区对应着一个子RDD的多个分区。如下图的groupByKey就属于宽依赖。其中宽依赖会触发shuffle操作

分区优化

计算数据规模

一般单分区数据规模一般为128M(一个Block一个分区,完全做到本地化) 借助Hadoop FileSystem 统计文件字节大小 Long FILE_SIZE

如果 FILE_SIZE/BLOCK_SIZE > 物理线程核心core * 2(线程) 则设置为物理线程核心core * 2(线程) 否则设置 Math.ceil(FILE_SIZE*1.0/BLOCK_SIZE)

Stage (阶段)

概念

1.一个 job,就是由一个 rdd 的 action 触发的动作,可以简单的理解为,当你需要执行一个 rdd 的 action 的时候,会生成一个 job。

2.stage : stage 是一个 job 的组成单位,就是说,一个 job 会被切分成 1 个或 1 个以上的 stage,然后各个 stage 会按照执行顺序依次执行。

3.task :即 stage 下的一个任务执行单元,一般来说,一个 rdd 有多少个partition,就会有多少个 task,因为每一个 task 只是处理一个partition 上的数据。

划分规则

1.从后向前推理,遇到宽依赖就断开,遇到窄依赖就把当前的RDD加入到Stage中;

2.每个Stage里面的Task的数量是由该Stage中最后 一个RDD的Partition数量决定的;

3.最后一个Stage里面的任务的类型是ResultTask,前面所有其他Stage里面的任务类型都是ShuffleMapTask;

4.代表当前Stage的算子一定是该Stage的最后一个计算步骤;

总结:由于spark中stage的划分是根据shuffle来划分的,而宽依赖必然有shuffle过程,因此可以说spark是根据宽窄依赖来划分stage的。

Spark案例

WordCount 、 求每个用户的观影次数

数据提供,提取码 arie

object App {
  def main(args: Array[String]): Unit = {
    val config: SparkConf = new SparkConf()
      .setAppName("spark_rdd_03")
      .setMaster("local[*]")
    val sc = new SparkContext(config)
    //Word Count
   wordCount
    sc.textFile("hdfs://single01:9000//spark/Broken to Harness.txt",3)
        .mapPartitions(it=>{
          it
            .filter(_.trim.size>0)
            .flatMap(_.replaceAll(",|\\.|!|\\?|;|\"","")
            .replaceAll("\\s{2,}"," ")
            .split(" ")
              .map((_,1))
                .groupBy(_._1)
                .map(tp2=>(tp2._1,tp2._2.size))
            )
        }).reduceByKey(_+_)
        .foreach(println)



    //每个用户的观影次数
    sc.textFile("hdfs://single01:9000//spark/tags.csv",3)
        .mapPartitionsWithIndex((index,it)=>{
          if (index==0) it.drop(1)  //删除第一个分区的表头
          it
            .map(line=>{
            val ps:Array[String]=line.split(",")
            (ps(0),1)
          }).toArray
            .groupBy(_._1)
            .map(tp2=>(tp2._1,tp2._2.size))
            .toIterator

        })
      .reduceByKey(_+_)
        .foreach(println)

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值