静下心来学spark02

本文深入探讨了Spark中RDD(弹性分布式数据集)的基本概念,包括其五大特性、依赖关系及缓存策略。阐述了RDD如何通过转换和行动操作处理大规模数据,并分析了窄依赖与宽依赖的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

RDD的类型总结

Spark中的RDD(Resilient Distributed Dataset)就是弹性分布式数据集,是Spark中基本的数据抽象。每个RDD都被分为多个分区,这些分区运行在集群中的不同节点。创建RDD的两种方式:读取一个外部数据集,或者在驱动器里面分发驱动器程序中的对象集合。(在任何时候都能够进行重算是我们为什么把RDD描述为"弹性"的原因--来自Spark快速大数据分析 p22)
	RDD支持两种类型的操作:转化操作(transformation)和行动操作(action),转化操作会返回一个新的RDD(惰性计算的,只有第一次在行动操作中用到时才会真正计算)
	步入正题:RDD的五大特性(来自spark-core_2.11-2.20-sources.jar  RDD.scala)
/**
- A list of partitions
- A function for computing each split
- A list of dependencies on other RDDs
- Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
- Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
*/
翻译过来就是
1.一组分片(Partition),即数据集的基本组成单位。对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户在创建RDD时指定RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认制。默认值就是所分配的 CPU core的数目(也有讲到,分片数不能少于blocks数量)
2.一个计算每个分区的函数。Spark中的RDD的计算是以分片为单位的,每个RDD都会实现compute函数以达到这个目的。compute函数会对迭代器进行复合,不需要保存每次计算的结果。(RDD抽象类要求其所有子类都必须实现compute方法,该方法介绍的参数之一是一个Partition对象,目的是计算该分区中的数据)
3.RDD之间的依赖关系。RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系。在部分数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。(参照下面说的宽窄依赖)
4.一个Partitioner,即RDD的分片函数。当前Spark中实现类两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。(只有对于k-v类型的RDD才会有Partitioner,非k-v得RDD的Partitioner的值是None)。Partitioner函数不但决定了本身的分片数量,也决定了parent RDD Shuffle 输出时的分片数量。
5.一个列表,存储存取每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照“移动数据不如移动计算”的理念,Spark在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。

RDD的依赖关系的理解

RDD中有两种两种依赖关系:窄依赖和宽依赖。
窄依赖指的是每一个父RDD的Partition最多被子RDD的一个Partition使用(也有人形象的比喻为独生子女)
宽依赖指的是多个子RDD的Partition会依赖同一个父RDD的Partition(也有人形象的比喻为超生)

宽窄依赖

学科pv案例的实现

import java.net.URL
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * 实现思路
  * 1.计算出每个学科的各个模块的访问量
  * 2.按照学科进行分组
  * 3.组内排序,并取topN
  */
object SubjectAccessCount {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("SubjectAccessCount").setMaster("local[2]")
    val sc = new SparkContext(conf)

    //获取数据
    val logs = sc.textFile("D:\\data/subjectaccess")

    //将用户访问日志进行切分并返回url
    val url: RDD[String] = logs.map(line => line.split("\t")(1))

    //将url生成元组,便于聚合
    val tupUrl: RDD[(String, Int)] = url.map((_, 1))

    //获取每个学科的各个模块的访问量
    val reduceUrl: RDD[(String, Int)] = tupUrl.reduceByKey(_ + _)

    //通过上面的数据来获取学科信息
    val subjectAndUrlInfo: RDD[(String, (String, Int))] = reduceUrl.map(tup => {
      val url = tup._1 //用户请求的url
      val count = tup._2 //url对应的pv
      val subject = new URL(url).getHost

      (subject, (url, count))
    })

    //按照学科信息进行分组
    val grouped: RDD[(String, Iterable[(String, Int)])] = subjectAndUrlInfo.groupByKey

    //组内降序排序
    val sorted: RDD[(String, List[(String, Int)])] = grouped.mapValues(_.toList.sortBy(_._2).reverse)
    //求topN
    val res: RDD[(String, List[(String, Int)])] = sorted.mapValues(_.take(3))
    //    println(subjectAndUrlInfo.collect.toBuffer)

    println(res.collect.toBuffer)
    sc.stop()
  }
}

缓存的应用场景(持久化)

使用最多的是MEMORY_ONLY 和 MEMORY_AND_DISK(内存不够会溢写到磁盘)
Spark RDD是转换算子是惰性求值的,而有时候我们希望能够多次使用同一个RDD。如果简单的对RDD调用行动操作,Spark每次都会重算RDD以及它的所有依赖。这个迭代算法中消耗格外大,为了避免多次计算同一个RDD,可以让Spark对数据进行持久化。当我们持久化存储一个RDD时,计算出RDD的节点会分别保存他们所求出的分区的数据。如果希望节点故障的情况下,不会拖累执行速度,我们可以把数据备份到多个节点。--(可以通过在存储级别的末尾加上 "_2" 来持久化数据分为两份)
	persist()方法调用本身不会强制求值。
	如果要缓存的数据过多,内存放不下就要使用LRU缓存策略把最老的分区从内存中移除(MEMORY_ONLY) 但是对于使用使用内存和磁盘的被移除的分区都会被写入磁盘。

关于Driver端和Executor端执行逻辑的区分

Driver(驱动器节点)时执行程序main()方法的进程,执行用户编写的用来创建SparkContext,创建RDD,以及进行RDD的转换操作和行动操作的代码,在Spark shell中会预先加载SparkContext对象。驱动器在Spark中有两个职责:
	1.把用户程序转换为任务
	2.为执行器节点调度任务
执行器节点是一个工作进程,负责在Spark作业中运行任务,任务间相互独立。Spark应用启动时,执行节点就被同时启动,并且始终伴随整个Spark应用的生命周期。
	两大作用:
	1.负责运行组成Spark应用的任务,并将结果返回给驱动程序
	2.他们通过自身的块管理器(Block Manager)为用户程序中要求缓存的RDD提供内存式存储。RDD是直接缓存在执行器进程内的,因此任务可以在运行时充分利用缓存数据加速运算。

几个问题

1、SparkContext是在哪一端生成的?
  Driver
2、RDD是在哪一端生成的?
  Driver
3、调用RDD的算子(Transformation和Action)是在哪一端调用的
  Driver
4、RDD在调用Transformation和Action时需要传入一个函数,函数是在哪一端声明和传入的?
  Driver
5、RDD在调用Transformation和Action时需要传入函数,请问传入的函数是在哪一端执行了函数的业务逻辑?
  Executor
6、自定义的分区器这个类是在哪一端实例化的?
  Driver
7、分区器中的getParitition(获取分区号)方法在哪一端调用的呢?
  Executor
8、DAG是在哪一端被构建的?
  Driver
9、DAG是在哪一端构建好的并被划分为一到多个Stage的
  Driver
10、DAG是哪个类完成的切分Stage的功能?
  Driver
11、DAGScheduler将切分好的Stage以什么样的形式给TaskScheduler?
  Driver
12、Task是在哪一端生成的呢? 
  Driver
13、广播变量是在哪一端调用的方法进行广播的?
  Driver
14、要广播的数据应该在哪一端先创建好再广播呢? 
  Driver

stage的划分过程

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

任务提交的四个阶段

任务提交的四个阶段

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值