Sparkcore学习(二)

RDD创建

根据本地创建

  1. makeRDD: 底层就是使用的parallelize
  2. parallelize

读取文件创建

  • 根据读取文件创建RDD
    • spark读取文件的方式:
      1. 如果集群配置文件中有配置
        1. HADOOP_CONF_DIR配置,此时默认读取是HDFS文件 【公司一般有配置HADOOP_CONF_DIR】
          1. 读取HDFS文件
            1. sc.textFile("/../...") [默认情况]
            2. sc.textFile("hdfs:///../...")
            3. sc.textFile("hdfs://namenode ip:端口/../..")
          2. 读取本地文件: sc.textFile("file:///../..")
      2. 如果集群配置文件中没有配置HADOOP_CONF_DIR配置,此时默认读取是本地文件
        1. 读取HDFS文件
          1. sc.textFile("hdfs://namenode ip:端口/../..")
        2. 读取本地文件:
          1. sc.textFile("file:///../..")
          2. sc.textFile("/../...") [默认情况]

其他rdd衍生

通过其他RDD(计算逻辑)推导出的

拓展

  • -–master yarn 状态下读取本地文件时报错,时因为任务是集群上运行的,可能在某台机器上可以读取到文件,而有些机器没法督导文件,因此报错。

RDD分区数

通过集合创建的RDD分区数

  1. parallelize方法numSlices参数有设置, 分区数 = numSlices的值
  2. parallelize方法numSlices参数没有设置, 分区数 = defaultParallelism
    • defaultParallelism的值分为两种情况:
      1. 有配置spark.default.parallelism参数,分区数 = spark.default.parallelism参数值
      2. 没有配置spark.default.parallelism参数,
        1. master=local, 分区数 = 1
        2. master=local[N], 分区数 = N
        3. maser=local[*] ,分区数 = cpu个数
        4. master=spark://.../.. ,分区数 = math.max( 本次任务的所有executor cpu总个数 , 2 )

根据文件创建的RDD的分区数,

分区数大于等于MinPartitions

  • minPartitions默认 = math.min(defaultParallelism, 2)
  • 分区数最终是多少个由文件的切片决定。

通过其他RDD衍生的RDD

根据其他rdd衍生出新RDD的分区数 = 依赖的第一个rdd的分区数,即和父RDD一致

算子

  • spark rdd算子分为两大类:
    1. 转换算子[Transformation]: 不会触发任务的计算,只是封装数据的计算过程,转换算子的结果类型是RDD
    2. 行动算子[Action]: 会触发任务的计算,行动算子的结果不是RDD类型

Transformation转换算子

value类型

map* * * *

  • 语法:map(func: RDD元素类型=> B ): 一对一映射
    • map里面的方法是针对RDD每个元素操作,返回一个结果,rdd有多少元素,函数就调用多少次
    • map生成的新的RDD中元素个数 = 原RDD元素个数
    • map的应用场景: 对数据进行值/类型转换,即数据类型转换/值转换[一对一]
    • spark map算子是针对每个分区并行计算
    • 总的来讲和Scala中类似

mapPartitions* * * *

  • 语法:mapPartitions(func: Iterator[RDD元素类型]=>Iterator[B]): 一对一映射[ 一个分区经过mapPartitions里面的函数计算之后返回一个新的分区 ]
    • mapPartitions里面的函数是针对RDD每个分区操作,rdd有多少分区,函数就调用多少次
    • mapPartitions生成新的RDD,新RDD的分区的元素是由函数返回的,因此新RDD元素个数不一定等于原RDD元素个数
    • mapPartitions的应用场景: 一般用于从外部查询数据[mysql],减少资源链接创建与销毁的次数

map与mapPartitions的区别:

  1. 函数的参数针对的对象不一样
    • map里面的函数是针对RDD每个元素操作,元素有多少个,函数就调用多少次
    • mapPartitions里面的函数是针对RDD每个分区操作,分区有多少个,函数就调用多少次
  2. 函数返回值不一样
    • map里面的函数是针对原RDD每个元素操作,操作完成之后返回一个新的元素
    • mapPartitions里面的函数是针对RDD每个分区所有数据的的迭代器操作,操作完成之后返回一个新的迭代器作为新RDD一个分区的所有数据
  3. 内存回收的时机不一样
    • map里面的函数是针对原RDD每个元素操作,元素操作完成之后就可以进行垃圾回收
    • mapPartitions里面的函数是针对RDD每个分区所有数据的的迭代器操作,此时单个元素操作完之后不能立即回收,必须等到该迭代器里面的元素全部操作完成之后才能回收,所以如果RDD分区数据特别多,可能出现内存溢出,出现内存溢出可以用map代替。

mapPartitionsWithIndex* * * * * * * *

  • 语法 :mapPartitionsWithIndex( func: ( Int, Iterator[RDD元素类型] ) => Iterator[B] )
    • mapPartitionsWithIndex里面的函数第一个参数是分区号,第二个参数对应该分区数据的迭代器
    • mapPartitionsWithIndex与mapPartitions的区别:
      • mapPartitions的函数只有一个参数,参数是就是分区数据的迭代器
      • mapPartitionsWithIndex的函数相比mapPartitions里面的函数多了一个分区号

flatmap* * * *

  • 语法:flatMap(func: RDD元素类型=>集合 ) = map + flatten
    • flatMap里面的函数是针对RDD每个元素操作,元素有多少个,函数就调用多少次
    • flatMap的应用场景: 一对多[一个元素返回一个或者多个结果]

glom

  • glom: 将每个分区所有数据用数组封装起来
    • glom生成的新的RDD的元素类型是Array[原RDD元素类型]
    • glom生成的新的RDD的元素个数 = 原RDD分区数

groupby* * * *

  • groupBy(func: RDD元素类型=>K): 按照指定字段分组
    • groupBy里面的函数是针对RDD每个元素操作
    • groupBy后续是按照函数的返回值进行分组
    • groupBy生成的新的RDD中的元素类型是KV键值对
      • K: 就是函数的返回值
      • V:是K在原RDD中对应的所有元素的集合
    • groupBy会产生shuffle操作
    • spark shuffle阶段: 写入缓冲区[分组 [排序] ] -> [combiner] ->溢写 -> 合并小文件 -> 分区拉取数据 -> 归并[排序]
  • 复习拓展:
    • MR的执行过程: 数据->InputFormat->map->写入环形缓冲区[80% 分组排序] -> [combiner] ->溢写 -> 合并小文件 -> reduce拉取数据 -> 归并排序 -> reduce -> outputformat -> 磁盘
    • MR shuffle阶段: 写入环形缓冲区[80% 分组排序] -> [combiner] ->溢写 -> 合并小文件 -> reduce拉取数据 -> 归并排序

filter* * * *

  • 语法filter(func: RDD元素类型=>Boolean ): 按照指定条件过滤
    • filter中的函数是针对RDD每个元素操作,元素有多少个,函数就调用多少次
    • filter保留的是函数返回值为true的数据

sample* * * * * * * *

  • sample(withReplacement,fraction[,seed]): 采样
    • withReplacement: 同一个元素是否可以被多次采样 <工作中一般设置为false>
    • fraction
      • withReplacement=false, fraction代表每个元素被采样的概率[0,1] <工作中一般设置为0.1-0.2左右>
      • withReplacement=true, fraction代表每个元素期望被采样的次数[>0]
  • 应用场景:sample在工作中一般只用于数据倾斜场景。 通过采样的数据映射整个数据集的情况
    出现数据倾斜之后,一般先通过sample采样数据,再统计采样数据中每个key的个数从而得知哪个key出现了数据倾斜(后续采样之后可以通过countByKey这个action算子统计采样结果集中每个key出现了多少次,从而判断哪个key出现了数据倾斜)

distinct* * * *

  • distinct会产生shuffle操作
  • 数据分布在不同块上,需要进行shuffle过程
    • 需要将相同数据聚到一个分区中才能进行去重,不然会导致结果不准确

coalesce* * * *

  • coalesce: 合并分区
    • coalesce: 默认只能合并分区,此时不会产生shuffle操作
    • 如果想要增大分区数,此时需要设置coalesce的shuffle参数=true,此时会使用shuffle重分区
    • 如果后续想要减少分区数,此时推荐使用coalesce,可以避免shuffle操作。

repartition* * * *

  • repartition: 重分区
    • repartition默认既可以增大分区也可以减少分区,都会产生shuffle,底层就是使用的coalesce(分区数,shuffle=true).
    • 如果想要增大分区,推荐使用repartition,使用简单

repartition与coalesce的区别:

  • coalesce默认只能合并分区,此时不会产生shuffle操作,如果想要增大分区数,此时需要设置coalesce的shuffle参数=true,此时会使用shuffle重分区 <如果想要减少分区推荐使用coalesce,一般是搭配filter使用,可以避免shuffle>
  • repartition默认既可以增大分区也可以减少分区,但是都会产生shuffle <如果想要增大分区推荐使用repartition,因为更简单>
    t

sortBy* * * *

  • 语法sortBy(func: RDD元素类型=> K ): 按照指定字段排序
    • sortBy里面的函数是针对RDD单个元素操作
    • sortBy是根据函数的返回值进行排序
    • sortBy默认是升序,如果想要降序将ascding参数设置为false既可以
  • sortBy会产生shuffle操作,是全局排序

pipe

  • pipe: 调用脚本
    • pipe是每个分区调用一次脚本
    • pipe后续会将一个分区所有数据传递给脚本作为参数
    • pipe会生成新的RDD,新RDD的元素是在脚本中通过echo返回的

双value类型交互

intersection交集

  • 函数签名:def intersection(other: RDD[T]): RDD[T]

    //使用
    val rdd1 = sc.parallelize(List(1,2,3,4,5))
    val rdd2 = sc.parallelize(List(4,5,6,7,8))
    val rdd3 = rdd1.intersection(rdd2)
    
  • 会产生shuffle过程,且产生两次

union并集

  • 函数签名:def union(other: RDD[T]): RDD[T]

    //使用
    val rdd1 = sc.parallelize(List(1,2,3,4,5))
    val rdd2 = sc.parallelize(List(4,5,6,7,8))
    val rdd3 = rdd1.union(rdd2)
    
  • 不会产生shuffle,合并后的分区数等于两个父RDD分区数之和

subtract差集

  • 函数签名:def subtract(other: RDD[T]): RDD[T]

    //使用
    val rdd1 = sc.parallelize(List(1,2,3,4,5))
    val rdd2 = sc.parallelize(List(4,5,6,7,8))
    val rdd3 = rdd1.subtract(rdd2)
    
  • 会产生shuffle过程,且产生两次

zip拉链

  • 函数签名:def zip[U: ClassTag](other:
    RDD[U]): RDD[(T, U)]

  • 两个RDD要想拉链必须元素个数与分区数都相同

    val rdd = sc.parallelize(List("lisi","wangwu","zhoaliu"),2)
    val rdd2 = sc.parallelize(List(20,30,40),2)
    val rdd3 = rdd.zip(rdd2)
    

Key-Value类型

partitionBy()按照K重新分区

  • 函数签名:def partitionBy(partitioner: Partitioner): RDD[(K, V)]
  • partitionBy: 根据指定的分区器重分区
  • 将RDD[K,V]中的K按照指定Partitioner重新进行分区;
  • 如果原有的RDD和新的RDD是一致的话就不进行分区,否则会产生Shuffle过程。

自定义分区

  • 要实现自定义分区器,需要继承org.apache.spark.Partitioner抽象类,并实现下面三个方法。
    1. numPartitions: Int:返回创建出来的分区数。
    2. getPartition(key: Any): Int:返回给定键的分区编号(0到numPartitions-1)。
    3. equals():Java 判断相等性的标准方法。这个方法的实现非常重要,Spark需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样Spark才可以判断两个RDD的分区方式是否相同

groupByKey()按照K重新分组

  • 函数签名:def groupByKey(): RDD[(K,
    Iterable[V])]
  • groupByKey: 根据key分组
    • groupBykey生成的新RDD的元素是[K,V]键值对
      • K: 是分组的K
      • V: 是K在原RDD中对应的所有的value值的集合
val rdd = sc.parallelize(List( ("aa",10) ,("cc",20),("aa",100),("dd",40),("cc",300)  ))
val rdd2 = rdd.groupByKey()
//结果
List((aa,CompactBuffer(10, 100)), (dd,CompactBuffer(40)), (cc,CompactBuffer(20, 300)))

reduceByKey()按照K聚合V* * * *

  • 语法:reduceByKey(func: (value值类型, value值类型)=>value值类型 ): 按照key分组之后,对每个组所有的value值聚合
  • reduceByKey在combiner阶段针对每个组所有的value值第一次聚合的时候,函数的第一个参数的初始值 = 第一个value值
  • reduceByKey的combiner阶段与reducer阶段的聚合逻辑是完全一样,逻辑就是传入的func函数
    • groupByKey与reduceByKey的区别:
      • groupByKey: 只是单纯分组, shuffle过程中没有预聚合功能, shuffle效率比较低
      • reduceByKey: 分组+聚合, shuffle过程中有预聚合功能, shuffle效率更高
    • 工作中尽量使用高性能的shuffle算子

combineByKey()转换结构后分区内和分区间操作

  • 函数签名: def combineByKey[C] (createCombiner: V => C, mergeValue: (C, V) => C,mergeCombiners: (C, C) => C): RDD[(K, C)]
  • combineByKey(createCombiner: RDDvalue值类型 => C,mergeValue: (C, RDDvalue值类型) => C,mergeCombiners: (C, C) => C)
    • createCombiner: 在combiner阶段对每个组第一个value值进行转换
    • mergeValue: combiner聚合逻辑
    • mergeCombiners: reducer聚合逻辑

foldByKey()分区内

  • foldByKey(默认值)(func: (value值类型,value值类型)=>value值类型) : 按照key分组,对每个组所有value值聚合
  • foldByKey在combiner阶段针对每个组第一次计算的时候, 函数第一个参数的初始值 = 默认值

aggregateByKey()分区间相同的

  • aggregateByKey(默认值)(seqOp,combOp)
    • seqOp: combiner聚合函数
      • combiner阶段针对每个组第一个计算的时候, seqOp函数的第一个参数的初始值 = 默认值
    • combOp: reducer聚合函数

combineByKey()转换结构后分区内和分区间操作

  • 函数签名:def combineByKey[C] (createCombiner: V => C,mergeValue: (C, V) => C,mergeCombiners: (C, C) => C): RDD[(K, C)]

reduceByKey、foldByKey、aggregateByKey、combineByKey的区别

  1. reduceBykey:

combineByKeyWithClassTag[V] ((v: V) => v, func, func, partitioner) 第一个初始值不变

  1. combineByKey:
    combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners)(null) 分区内和分区间规则一致
  2. foldByKey:
    combineByKeyWithClassTag[V] ((v: V) => cleanedFunc(默认值, v),cleanedFunc, cleanedFunc, partitioner) 第一个初始值和分区内处理规则一致
  3. aggregateBykey:
    combineByKeyWithClassTag[U] ((v: V) => cleanedSeqOp(默认值, v),cleanedSeqOp, combOp, partitioner) 把第一个值变成特定的结构
  • reduceByKey、foldByKey、combineByKey、aggregateByKey区别:
    • reduceByKey: 在combiner阶段对每个组所有value值第一次计算的时候,函数的第一个参数的初始值 = 该组第一个value值, combiner与reducer计算逻辑完全一样
    • foldByKey: 在combiner阶段对每个组所有value值第一次计算的时候,函数的第一个参数的初始值 = 默认值, combiner与reducer计算逻辑完全一样
    • combinerByKey: 在combiner阶段对每个组所有value值第一次计算的时候,函数的第一个参数的初始值 = 第一个函数的转换结果
    • aggregateByKey: 在combiner阶段对每个组所有value值第一次计算的时候,函数的第一个参数的初始值 = 默认值

sortByKey()按照K进行排序

  • 函数签名:

    def sortByKey(ascending: Boolean = true, numPartitions: Int =elf.partitions.length)  : RDD[(K, V)]// 默认,升序
    
  • sortByKey: 根据key排序

  • 在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD

  • 可以通过sortBy完成sortByKey操作

mapValues()只对V进行操作

  • map(func: 元素=>新元素)
  • mapValues(func: 元素的value值=>元素的新value值): 一对一的映射[将原来的value值做转换,返回新的vlaue值]
    • mapValues里面的函数是针对元素的value值操作, 元素有多少个,函数就调用多少次

Join连接

  1. join: 两个rdd只有key相同的才能连接,join生成的RDD的元素类型是 (key,(左rdd value值,右rdd value值)) 【类似sql inner join】
  2. leftOuterJoin: 两个rdd key相同的能连接+ 左rdd不能连接的数据,join生成的RDD的元素类型是 (key,(左rdd value值,Option(右rdd value值)))
  3. rightOuterJoin: 两个rdd key相同的能连接+ 右rdd不能连接的数据,join生成的RDD的元素类型是 (key,(Option(左rdd value值),右rdd value值))
  4. fullOuterJoin: 两个rdd key相同的能连接+ 右rdd不能连接的数据+左rdd不能连接的数据,join生成的RDD的元素类型是 (key,(Option(左rdd value值),Option(右rdd value值)))

cogroup()类似全连接

  • 类似 先对两个rdd实现groupByKey ,然后针对两个rdd 的分组结果执行全外连接

Action行动算子

reduce()聚合* * * * * *

  • 函数签名:def reduce(f: (T, T) => T) : T
  • reduce是对rdd所有元素聚合
  • reduce是先对每个分区所有数据聚合,将每个分区的聚合结果发给Driver,由Driver统一汇总

collect()以数组的形式返回数据集

  • 函数签名:def collect(): Array[T]
  • 将rdd每个分区的数据收集到Driver端口
  • 特别提醒:所有的数据都会被拉取到Driver端,慎用
    • 如果rdd分区数据量比较大,而Driver内存默认只有1G,所以可能出现内存溢出
    • 在工作中一般都需要调整Driver的内存大小,一般调整为5-10G.
    • 在spark-submit的是可以通过--driver-memory调整Driver内存大小

count()返回RDD中元素个数

  • 函数签名:def count(): Long
  • 返回RDD中元素的个数

first()返回RDD中的第一个元素

  • 函数签名:def first(): T
  • 返回RDD中的第一个元素

take()返回由RDD前n个元素组成的数组

  • def take(num: Int): Array[T]
  • 返回一个由RDD的前n个元素组成的数组

takeOrdered()返回该RDD排序后前n个元素组成的数组

  • 函数签名:def takeOrdered(num:Int) (implicit ord: Ordering[T]): Array[T]
  • 返回该RDD排序后的前n个元素组成的数组
  • 猜测会产生shuffle过程

fold()

  • 函数签名:def fold(zeroValue: T) (op: [T , T]): T
  • fold(默认值)(func: (RDD元素类型,RDD元素类型)=> RDD元素类型): 对RDD所有元素聚合
    • fold首先会对每个分区所有元素中进行聚合,然后将每个分区的聚合结果发给Driver进行汇总
      • fold在每个分区中第一次计算的时候,函数第一个参数的初始值 = 默认值
      • fold在Driver第一次计算的时候,函数第一个参数的初始值 = 默认值

aggregate()

  • 函数签名:def aggregate(U: ClassTag) (zeroValue: U) (seqOp: (U , T): U,combOp:(U : U)=>U):U
  • aggregate(默认值)(seqOp: (默认值类型,RDD元素类型)=> 默认值类型 , comop: ( 默认值类型,默认值类型 )=>默认值类型 ) : 对RDD所有元素聚合
    • seqOp: 是在每个分区中对所有元素聚合逻辑
    • comop: 在driver中对所有分区的聚合结果的汇总逻辑

fold与aggregate的区别:

  • fold的分区内聚合逻辑与driver汇总逻辑一样
  • aggregate的分区内聚合逻辑与driver汇总逻辑可以不一样

countByKey()统计每种key的个数* * * * * *

  • 函数签名:def countByKey(): Map[K, Long]
  • countByKey一般结合sample一起使用
    • 后续如果出现了数据倾斜,一般先用sample采样数据,然后用countByKey统计采样结果,从而得知哪个key出现了数据倾斜

save相关算子

saveAsTextFile(path)保存成Text文件

  • 将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本

saveAsSequenceFile(path)保存成Sequencefile文件

  • 将数据集中的元素以Hadoop Sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。
  • 注意:只有kv类型RDD有该操作,单值的没有

saveAsObjectFile(path) 序列化成对象保存到文件

  • 用于将RDD中的元素序列化成对象,存储到文件中。

foreach(f)遍历RDD中每一个元素

  • 函数签名:def foreach(f:T=>Unit): Unit
  • 遍历RDD中每一个元素
  • foreach(func: RDD元素类型=>Unit): Unit
    • foreach里面的函数是针对单个元素操作,元素有多少个,函数就调用多少次,没有返回值
    • foreach是并行执行
    • foreach与map的区别:
      • map是转换算子,会生成一个新的RDD
      • foreach是行动算子,不会生成新的RDD,会触发任务计算,得到具体的结果

foreachPartition()* * * * * *

  • foreachPartition(func: Iterator[RDD元素类型]=>Unit):Unit
    • foreachPartition里面的函数是针对每个分区操作,函数的参数是每个分区所有数据的迭代器,rdd有多少分区,函数就执行多少次。
    • foreachPartition一般用于将数据保存到mysql,hbase,redis等存储介质中,可以减少资源链接的创建与销毁的次数。

序列化

闭包

  • 函数体中使用了外部变量的函数称之为闭包.

spark序列化原因:

  • spark算子里面的函数是在executor中的task执行的,spark算子外面的代码是在driver执行的,如果spark算子里面的函数体中使用了dirver定义的变量
  • 此时spark会将该driver变量序列化之后传给task使用,所以就必须要求该driver变量类型必须能够序列化才行。

spark里面的有两种序列化: java序列化[默认使用],kryo序列化

  • java序列化: 在序列化对象的时候会将对象的全类名、属性、属性类型、继承信息等全部都会序列化进去
  • kryo序列化: 只会序列化类名、属性名、属性值、属性类型信息[序列化性能比java序列化高10倍左右]
  • 在实际工作中一般选择使用kryo序列化

如何配置spark使用kryo序列化:

  1. 在sparkconf中配置spark默认使用的序列化器: new SparkConf().set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
  2. 注册哪些类使用kryo序列化【可选】: new SparkConf().registerKryoClasses(Array(classOf[待序列化的类的类名])
    • 使用registerKryoClasses与不使用egisterKryoClasses的区别:
      1. 使用registerKryoClasses,此时再序列化类的时候不会将类的全类名序列化进去
      2. 没有使用registerKryoClasses,此时在序列化类的时候会将类的全类名序列化进去

RDD依赖关系

查看血缘关系

  • 一个job中多个rdd之间的关系
  • rdd的血统可以通过toDebugString查看

查看依赖关系

要想理解RDDS是如何工作的,最重要的就是理解Transformations。

​ RDD之间的关系可以从两个维度来理解:一个是RDD是从哪些RDD转换而来,也就是 RDD的parent RDD(s)是什么; 另一个就是RDD依赖于parent RDD(s)的哪些Partition(s),这种关系就是RDD之间的依赖。

窄依赖

  • 窄依赖表示每一个父RDD的Partition最多被子RDD的一个Partition使用,最简单的理解就是是否有shuffle过程,没有就是窄依赖。

宽依赖

  • 宽依赖表示同一个父RDD的Partition被多个子RDD的Partition依赖,会引起Shuffle
  • 具有宽依赖的transformations包括:sortreduceByKeygroupByKeyjoin和调用rePartition函数的任何操作。
  • 宽依赖对Spark去评估一个transformations有更加重要的影响,比如对性能的影响。

Stage任务划分

  • stage切分: 根据最后一个rdd的依赖关系向前找父RDD,然后根据父RDD的依赖关系继续向前,依次查找,一直找到第一个RDD位置。在中间查询的过程中如果父子依赖关系是宽依赖则切分stage.
  • stage的执行: 一个job中的stage是从前往后执行,先执行第一个stage,在执行后面的stage
  • Application: 应用, 一个sparkcontext称之为一个应用。
    • job: 任务, 一般一个action算子产生一个Job [first与take除外,这两个可能产生多个Job]
      • stage: 阶段, job中stage的个数 = shuffle个数+1
      • task: 子任务, 一个stage中task的个数 = stage中最后一个rdd的分区数
  • 一个job中多个stage的执行是串行
  • 一个stage中多个task的执行是并行

  • RDD任务切分中间分为:Application、Job、Stage和Task
    1. Application:初始化一个SparkContext即生成一个Application;
    2. Job:一个Action算子就会生成一个Job;
    3. Stage:Stage等于宽依赖的个数加1;
    4. Task:一个Stage阶段中,最后一个RDD的分区个数就是Task的个数。
      • 注意:Application->Job->Stage->Task每一层都是1对n的关系。

RDD持久化

RDD Cache缓存

  • RDD的持久化过程:RDD通过Cache或者Persist方法将前面的计算结果缓存,默认情况下会把数据以序列化的形式缓存在JVM的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
  • RDD持久化原因: RDD中不存储数据,如果一个rdd在多个job中重复使用,此时默认会重复执行该RDD之前的步骤,如果能将rdd的数据保存下来,后续job就不用重复执行该rdd之前的步骤了。
  • 应用场景:
    1. 一个rdd在多个job中重复使用
    2. job中依赖链条太长,可以避免计算出错导致重新计算

如何持久化

  1. 缓存: 将RDD的数据保存到分区所在机器的内存/磁盘中 <常用>
    • 使用方式:
      • rdd.cache/ rdd.persist(参数…..)
    • cache与persist的区别:
      1. cache是数据只能保存到内存中
      2. persist可以指定将数据保存到内存/磁盘中
    • 常用的存储级别(persist的参数):
      1. MEMORY_ONLY[数据只保存到内存中]: 一般用于小数据量场景
      2. MEMORY_AND_DISK[数据一部分保存到内存中,一部分保存到磁盘中]: 一般用于大数据量场景
  2. checkpoint: 将RDD的数据保存到HDFS中
    • 原因: 缓存是将数据保存到本机的内存/磁盘中,如果服务器宕机,数据丢失,spark会重写启动一个task,然后根据rdd依赖关系重新计算得到数据, 影响效率。
    • 如何使用: rdd.checkpoint
      • 每个job执行完成之后,spark会查看当前job中是否有rdd需要checkpoint,如果有则将该rdd之前的数据处理步骤再次执行得到数据之后保存到指定checkpoint路径。所以checkpoint会触发一次job操作。
      • 所以如果想要避免checkpoint之前的处理重复执行可以将checkpoint操作与cache结合使用: rdd.cache ; rdd.checkpoint()

缓存与checkpoint的区别:

  1. 数据保存位置不一样:
    • 缓存是将RDD的数据保存到分区所在机器的内存/本地磁盘中
    • checkpoint是将RDD的数据保存到HDFS中
  2. 依赖关系是否保留不一样
    • 缓存是将RDD的数据保存到分区所在机器的内存/本地磁盘中,如果服务器宕机,数据丢失之后需要根据依赖关系重新计算得到数据,所以RDD的依赖关系必须保留
    • checkpoint是将RDD的数据保存到HDFS中,数据不会丢失,所以RDD的依赖关系会删除

键值对RDD数据分区

  1. 分区器: 用于shuffle阶段
  • spark自带的分区器:
    • HashPartitioner:
      • 分区规则: key.hashCode % 分区数 < 0 ? key.hashCode % 分区数 + 分区数 : key.hashCode % 分区数
    • RangePartitioner:
      • 分区规则: 会采用采样的方式从数据集中抽取 分区数-1 个key,
      • 通过这几个采样的key确定每个分区的边界,
      • 后续将key与分区边界对比如果在边界范围内则放入对应分区中。
  1. 注意:
    1. 只有Key-Value类型的RDD才有分区器,非Key-Value类型的RDD分区的值是None
    2. 每个RDD的分区ID范围:0~numPartitions-1,决定这个值是属于那个分区的。

转换算子和行动算子中带 * 号的相对重要

posted @ 2021-11-07 15:07  KaneQi  阅读(31)  评论(0编辑  收藏  举报