spark-4 RDD

RDD基本概念

RDD是spark core的核心,是spark的基本数据抽象,它代表一个不可变、可分区、里面元素可并行计算的集合

  • RDD(Resilent Distributed Dataset)也称为弹性分布式数据集,是spark中最基本的数据抽象。它具有以下特点
    • Dataset:表示一个集合,用来存储数据
    • Distributed :表示RDD内部数据元素进行了分布式存储
    • Resilent:表示存储位置弹性,可以存储在内存或者磁盘
    • 不可变:当前RDD的元素不可以改变
    • 可分区:对于(K,V)类型的元素,可以蛇者不同的元素去往不同分区

RDD五大属性

RDD源码注释
在这里插入图片描述

A list of partitions

一个分区列表:一个RDD的数据集的基本组成单位为分区

  • 一个RDD有很多个分区(RDD是分布式存储),每一个分区包含了该RDD的部分数据
  • spark任务以task线程为基本单位运行,一个分区对应一个task(同一个stage的不同RDD的相同分区也对应一个task,详情参考Spark DAG)

RDD分区数

  • 对于读取HDFS文件: RDD的分区数=max(文件的block个数,defaultMinPartitions)
    • 当只有一个block时,RDD分区为2
  • 对于读取本地文件:RDD分区数=max(文件的split个数,defaultMinPartitions)
    • 当只有一个split时,RDD分区为2

更多分区数相关参考下面的链接:
Spark分区数

A function for computing each split

spark中RDD的计算是以分区为单位的,每个RDD都会实现compute计算函数

A list of dependencies on other RDDs

一个RDD会依赖于其他多个RDD。

  • spark的容错机制利用了这个特性(血统)

示例

# 后面的RDD依赖于前面的RDD
val rdd1=sc.textFile("/words.txt")
val rdd2=rdd1.flatMap(x=>x.split(" "))   ------>x=>x.split(" ")
val rdd3=rdd2.map(x=>(x,1))              ------>x=>(x,1)  

val rdd6=rdd4.join(rdd5)

Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)

对于K-V类型的数据,可以选择使用分区函数对其进行分区

  • spark支持两种分区函数(默认使用哈希分区
    • 基于哈希的HashPartitioner,(key.hashcode % 分区数= 分区号)。
    • 基于范围的RangePartitioner

Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)

spark同样采用了移动计算的思想,即在启动任务计算时,会优先考虑在存有数据的本地节点开启任务,来达到减小网络传输,提升计算效率的目的。

基于spark的单词统计程序剖析rdd的五大属性

  • 需求

    HDFS上有一个大小为300M的文件,通过spark实现文件单词统计,最后把结果数据保存到HDFS上
    
  • 代码

    sc.textFile("/words.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).saveAsTextFile("/out")
    
  • 流程分析
    在这里插入图片描述

RDD创建方式

  • 通过已经存在的scala集合构建,一般用于前期测试。关键字parallelize

    val rdd1=sc.parallelize(List(1,2,3,4,5))
    val rdd2=sc.parallelize(Array("hadoop","hive","spark"))
    val rdd3=sc.makeRDD(List(1,2,3,4))
    
  • 加载外部的数据源构建

    val rdd1=sc.textFile("/words.txt")
    
  • 从已经存在的RDD进行转换生成**新的RDD

    val rdd2=rdd1.flatMap(_.split(" "))
    val rdd3=rdd2.map((_,1))
    

RDD算子

RDD的算子可以分为两类

  • transformation
  • action

transformation算子

transformation算子根据已经存在的RDD转换生成新的RDD,延迟加载不会立即执行

transformation算子

转换含义
map(func)返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成
filter(func)返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成
flatMap(func)类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素)
mapPartitions(func)类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U]
mapPartitionsWithIndex(func)类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U]
union(otherDataset)对源RDD和参数RDD求并集后返回一个新的RDD
intersection(otherDataset)对源RDD和参数RDD求交集后返回一个新的RDD
distinct([numTasks]))对源RDD进行去重后返回一个新的RDD
groupByKey([numTasks])在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD
reduceByKey(func, [numTasks])在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置
sortByKey([ascending], [numTasks])在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
sortBy(func,[ascending], [numTasks])与sortByKey类似,但是更灵活
join(otherDataset, [numTasks])在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD
cogroup(otherDataset, [numTasks])在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD
coalesce(numPartitions)减少 RDD 的分区数到指定值。
repartition(numPartitions)重新给 RDD 分区
repartitionAndSortWithinPartitions(partitioner)重新给 RDD 分区,并且每个分区内以记录的 key 排序

Action算子

action算子触发任务执行。一个action触发的任务对应一个job。

action算子

动作含义
reduce(func)reduce将RDD中元素前两个传给输入函数,产生一个新的return值,新产生的return值与RDD中下一个元素(第三个元素)组成两个元素,再被传给输入函数,直到最后只有一个值为止。
collect()在驱动程序中,以数组的形式返回数据集的所有元素
count()返回RDD的元素个数
first()返回RDD的第一个元素(类似于take(1))
take(n)返回一个由数据集的前n个元素组成的数组
takeOrdered(n, [ordering])返回自然顺序或者自定义顺序的前 n 个元素
saveAsTextFile(path)将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本
saveAsSequenceFile(path)将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。
saveAsObjectFile(path)将数据集的元素,以 Java 序列化的方式保存到指定的目录下
countByKey()针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。
foreach(func)在数据集的每一个元素上,运行函数func
foreachPartition(func)在数据集的每一个分区上,运行函数func

RDD算子运行地点

  • 所有涉及RDD内部元素计算的数据全部在executor端运行
  • 需要汇总所有RDD数据的计算或者对RDD自身的操作返回driver端运行,如collect。
    • 因为要汇总大量的数据,需要driver端有足够的内存,如10G。否则容易OOM

RDD的依赖关系

RDD的依赖关系有两种:

  • 窄依赖(naroow dependecies)
  • 宽依赖(wide dependecies)

在这里插入图片描述

依赖视频
与视频对应的文档

窄依赖

  • 正确定义:每一个父RDD的partition最多被子RDD的一个partition使用
  • 错误定义:一个子RDD的任意一个partition至多只同时依赖同一个RDD的一个partition

常见的窄依赖算子

  • map
  • flatMap
  • Filter
  • union
  • mapValues
  • mapPartitions
  • mapPartitionsWithIndex

所有的窄依赖都不会产生shuffle

宽依赖

  • 正确定义:多个子RDD的partition依赖同一个父RDD的partition
  • 错误定义:存在一个子RDD的至少一个partition同时依赖一个父RDD的多个partition

常见的宽依赖算子

  • reduceByKey
  • sortByKey
  • groupBy
  • groupByKey

所有的宽依赖都会产生shuffle

关于宽依赖和债依赖定义的补充(重要)

之前我一直对RDD依赖关系,一直是按照错误定义理解的。直到考虑到一种极端情况,子RDD只有一个且只有一个分区。可以使用colaesce算子实现。

  • colaesce算子不产生shuffle,是一个导致窄依赖的算子。
  • 这种情况下,会导致多个同一父RDD的不同partition被同一个子RDD的算子使用。是宽/窄依赖错误定义的反例

在这里插入图片描述

join算子的依赖关系

join算子既可能产生宽依赖也可能产生窄依赖,产生的结果主要看子RDD依赖的父RDD的是否有相同的分区函数,即分区规则

生成依赖的源码

  • 可以在此处加断点看生成的是什么依赖
  • 当然也可以直接看生成的DAG图
override def getDependencies: Seq[Dependency[_]] = {
  rdds.map { rdd: RDD[_ <: Product2[K, _]] =>
    if (rdd.partitioner == Some(part)) {
      logDebug("Adding one-to-one dependency with " + rdd)
      new OneToOneDependency(rdd)
    } else {
      logDebug("Adding shuffle dependency with " + rdd)
      new ShuffleDependency[K, Any, CoGroupCombiner](rdd, part, serializer)
    }
  }
}

join的窄依赖

  • 子RDD3依赖父RDD1和父RDD2
  • 父RDD1和父RDD2有相同分区函数

在这里插入图片描述

join的宽依赖

  • 子RDD3依赖父RDD1和父RDD2
  • 父RDD1和父RDD2有不同分区函数

在这里插入图片描述

join依赖源码分析
join依赖举例分析

RDD的血统

RDD的血统(Lineage),是一种依赖关系。

  • 血统会记录RDD的元数据信息和转换行为(因此可以根据最原始的根数据加上一系列的转换行为),血统保存了RDD的依赖关系。
  • RDD只支持粗粒度转换。即只记录单个块上的单个操作

血统的生存周期

当一个job结束时,其对应的血统就消失了

  • 一个application对应多个job
  • 每个action触发一个Job

RDD的缓存机制

可以把一个RDD的数据缓存起来,后续有其他job需要用到该RDD的数据,可以直接从缓存中获取。
没有缓存的数据需要根据血统从最源头的RDD重新计算。

RDD支持两种缓存:

  • persist
  • cache
  /**
   * Persist this RDD with the default storage level (`MEMORY_ONLY`).
   */
  def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)

  /**
   * Persist this RDD with the default storage level (`MEMORY_ONLY`).
   */
  def cache(): this.type = persist()
  • persist和cache都不是设置后立刻调用,都是在action算子触发任务执行时才被调用。
  • cache实际上调用的是persist,两种缓存的默认缓存级别都是memory_only

缓存级别

  • 带2的表示缓存两份
    • DISK_ONLY_2。表示仅仅保存在磁盘中,在磁盘中保留2份
  • MEMORY_AND_DISK :表示优先存在内存中,内存存不下才存在disk
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)

缓存级别 释义
在这里插入图片描述

如何选择缓存级别

Spark的多个存储级别意味着在内存利用率和cpu利用效率间的不同权衡。我们推荐通过下面的过程选择一个合适的存储级别:

  • 如果你的RDD适合默认的存储级别(MEMORY_ONLY),就选择默认的存储级别。因为这是cpu利用率最高的选项,会使RDD上的操作尽可能的快。

  • 如果不适合用默认的级别,选择MEMORY_ONLY_SER。选择一个更快的序列化库提高对象的空间使用率,但是仍能够相当快的访问。

  • 除非函数计算RDD的花费较大或者它们需要过滤大量的数据,不要将RDD存储到磁盘上,否则,重复计算一个分区就会和重磁盘上读取数据一样慢。

  • 如果你希望更快的错误恢复,可以利用重复(replicated)存储级别。所有的存储级别都可以通过重复计算丢失的数据来支持完整的容错,但是重复的数据能够使你在RDD上继续运行任务,而不需要重复计算丢失的数据。

  • 在拥有大量内存的环境中或者多应用程序的环境中,OFF_HEAP具有如下优势:

    • 它运行多个执行者共享Tachyon中相同的内存池
    • 它显著地减少垃圾回收的花费
    • 如果单个的执行者崩溃,缓存的数据不会丢失

缓存设置时机

  • 当某个RDD的数据后期被使用多次时,可以考虑设置缓存

  • 某个RDD的结果数据经过大量计算

    val rdd2=rdd1.flatMap(函数).map(函数).reduceByKey(函数).xxx.xxx.xxx.xxx.xxx
    

在这里插入图片描述
如上图所示

  • 上图有2个Job,处在同一个application中。两个job分别计算得到RDD3和RDD4。
  • 第一次使用RDD2做相应的计算得到RDD3,会先从HDFS读取文件,然后从RDD1开始计算得到RDD2,RDD2计算得到RDD3。此时RDD3对应job完成,其对应的血统消失
  • 计算RDD4的时候如果没有做缓存,会从读取HDFS上的文件开始重新走一遍流程。计算得出RDD4后,其对应job完成,其对应的血统消失
  • 此时可以考虑对RDD2做缓存,计算RDD4时直接从RDD2获取结果。

缓存设置示例

val rdd1=sc.textFile("/words.txt")
val rdd2=rdd1.flatMap(_.split(" "))
val rdd3=rdd2.cache
rdd3.collect

val rdd4=rdd3.map((_,1))
val rdd5=rdd4.persist(缓存级别)
rdd5.collect

cache和persist的区别

  • cache: 默认是把数据缓存在内存中,其本质就是调用persist方法;
  • persist:可以把数据缓存在内存或者是磁盘,有丰富的缓存级别,这些缓存级别都被定义在StorageLevel这个object中。

缓存数据的清除

  • 自动清除
    • 一个application结束后,对应的缓存数据,无论是内存还是磁盘都会清除
  • 手动清除
    • 调用RDD的unpersist方法

      rdd..unpersist()
      

RDD的checkpoint机制

  • RDD的checkpoint类似于快照,可以将目标数据进行缓存,一般存储在HDFS上
  • 每次运行application生成的checkpoint之间是相互隔离的。
    • 即使不同的application指定了同一个checkpoint目录,各个application会在指定目录下生成各自对应的子目录,达到相互隔离的效果。

如何设置checkpoint

  1. 在HDFS上设置一个checkpoint目录

    sc.setCheckpointDir("hdfs://node01:8020/checkpoint") 
    
  2. 对需要做checkpoint的RDD调用checkpoint方法

    # 一般会先设置cache,然后设置checkpoint提上效率
    val rdd1=sc.textFile("/words.txt")
    rdd1.checkpoint
    val rdd2=rdd1.flatMap(_.split(" ")) 
    
    配置了hadoop和spark整合 路径直接写"/"接口 不需要写node
    
  3. 调用action操作触发任务运行

    rdd2.collect
    只有执行了算子操作,checkpoint才会生效,缓存才会产生
    checkpoint会单独触发一个单独的Job,从源头计算一遍
    action触发任务之后,cache只有一个job,
    

spark streaming的checkpoint

本文主要讨论的是RDD,上面的例子也是举的RDD的列子。
checkpoint除了应用于保存RDD数据,也可以用来保存spark streaming的业务处理逻辑。
即 Metadata checkpointing,当driver端提交的查程序运行失败时,会自动从checkpoint读取程序逻辑运行。

Metadata checkpointing
将流式计算的信息保存到具备容错性的存储上比如HDFS,Metadata Checkpointing适用于当streaming应用程序Driver所在的节点出错时能够恢复,元数据包括:

  • Configuration(配置信息) : 创建streaming应用程序的配置信息
  • Dstream operations : 在streaming应用程序中定义的DStreaming操作
  • Incomplete batches : 在队列中没有处理完的作业

注意
如果更改了driver程序,不会读取新的程序,还是会按照之前备份的逻辑运行

checkpoint的删除

需要手动删除HDFS上的文件: hdfs df rm -r

官网地址

官网地址

cache persist checkpoint三者的区别

  • cache和persist
    • cache默认数据保存在内存中
    • persist可以保存在内存或者磁盘中
    • cache和persist需要action操作触发,但是不会触发新的job
    • cache和persist不会改变RDD的依赖关系
    • 程序运行完 ,缓存的数据自动消失
  • checkpoint
    • 把数据持久化写入到HDFS上
    • 同样需要actionc啊哦做触发,但是会新启动一个Job,从最开始的数据源重新计算一遍得到缓存
    • checkpoint会改变血统,即RDD的依赖关系

数据读取顺序

当后续用到之前的某个RDD时,会按照以下顺序尝试读取

  • 从缓存中读取
  • 从checkpoint读取
  • 都没有读取到,且血统没被checkpoint破坏。根据血统重新计算

示例

sc.setCheckpointDir("/checkpoint")
   val rdd1=sc.textFile("/words.txt")
   val rdd2=rdd1.cache
   rdd2.checkpoint
   val rdd3=rdd2.flatMap(_.split(" "))
   rdd3.collect
   
# checkpoint操作要执行需要有一个action操作,一个action操作对应后续的一个job。
# 该job执行完成之后,它会再次单独开启另外一个job来执行 rdd1.checkpoint操作。
   
# 对checkpoint在使用的时候进行优化,在调用checkpoint操作之前,可以先来做一个cache操作,缓存对应rdd的结果数据
# 后续就可以直接从cache中获取到rdd的数据写入到指定checkpoint目录中


sc.setCheckpointDir("/checkpoint")
val rdd1=sc.textFile("/words.txt")
val rdd2=rdd1.cache
rdd2.cache
rdd2.checkpoint
rdd2.flatMap(_.split(" ")).map((_,1))

# 当rdd2.checkpoint之后,直接从rdd2的缓存中拿到数据,不用从头计算了。提升了性能
# 后续用到rdd2时,优先从cache中找,找不到再从checkpoint存储路径找,最后实在找不到利用血统从源头重新计算
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值