zoukankan      html  css  js  c++  java
  • 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,决定这个值是属于那个分区的。

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

    作者:Ya
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    Spark权威指南(中文版)----第5章 结构化API基本操作
    Spark权威指南(中文版)----第2章 Spark简介
    Spark权威指南(中文版)----第4章 结构化API概述
    Spark权威指南(中文版)----第1章Apache Spark是什么
    Java读写锁的实现原理
    【进阶之路】动态代理与字节码生成
    如何写好技术文档——来自Google十多年的文档经验
    谈谈 C++ STL 中的迭代器
    面试官疯狂问我联表查询怎么办? 愣着干嘛?进来白嫖啊!
    面试问题记录 三 (JavaWeb、JavaEE)
  • 原文地址:https://www.cnblogs.com/1463490Ya/p/15519987.html
Copyright © 2011-2022 走看看