Spark Core

1 RDD概述

1.1 什么是RDD

RDD(Resillient Distributed Dataset)弹性分布式数据集,是Spark中最基本的数据抽象。

RDD在代码中是一个抽象类,他代表一个弹性的、不可变、可分区、里面的元素可以并行计算的集合。

WordCount工作流程

在这里插入图片描述

所有任务计算相关操作都在Executor端执行,代码编写内容都在Driver端执行

1 弹性
存储的弹性:内存与磁盘自动切换
容错的弹性:数据丢失可以自动恢复
计算的弹性:计算出错重试机制
分片的弹性:克根据需要重新分片

2 分布式
数据存储在大数据集群不同节点上

3 数据集 不存储数据
RDD封装了计算逻辑,并不保存数据

4 数据抽象
RDD是抽象类,需要予以具体实现

5 不可变
RDD封装了计算逻辑,是不可以改变的,想要改变,只能产生新的RDD,在新的RDD里面封装计算逻辑

6 可分区、并行计算

1.2 RDD五大特性

在这里插入图片描述

2 RDD编程

2.1 RDD的创建

在Spark中创建RDD的方式有三种

  1. 从集合中创建RDD
  2. 从外部存储创建RDD
  3. 从其它RDD创建

2.1.1 IEDA环境准备

<!- 创建文件夹和Scala框架支持 ->

<!- spark-core依赖和scala编译插件 ->
<dependencies>
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-core_2.12</artifactId>
        <version>3.1.3</version>
    </dependency>
</dependencies>

<build>
    <finalName>SparkCoreTest</finalName>
    <plugins>
        <plugin>
            <groupId>net.alchim31.maven</groupId>
            <artifactId>scala-maven-plugin</artifactId>
            <version>3.4.6</version>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>testCompile</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
# 调整打印日志等级,仅打印结果

log4j.rootCategory=ERROR, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n

# Set the default spark-shell log level to ERROR. When running the spark-shell, the log level for this class is used to overwrite the root logger's log level, so that the user can have different defaults for the shell and regular Spark apps.
log4j.logger.org.apache.spark.repl.Main=ERROR

# Settings to quiet third party logs that are too verbose(冗长 冗余)
log4j.logger.org.spark_project.jetty=ERROR
log4j.logger.org.spark_project.jetty.util.component.AbstractLifeCycle=ERROR
log4j.logger.org.apache.spark.repl.SparkIMain$exprTyper=ERROR
log4j.logger.org.apache.spark.repl.SparkILoop$SparkILoopInterpreter=ERROR
log4j.logger.org.apache.parquet=ERROR
log4j.logger.parquet=ERROR

# SPARK-9183: Settings to avoid annoying messages when looking up nonexistent UDFs in SparkSQL with Hive support
log4j.logger.org.apache.hadoop.hive.metastore.RetryingHMSHandler=FATAL
log4j.logger.org.apache.hadoop.hive.ql.exec.FunctionRegistry=ERROR

2.1.2 从集合创建

package com.atguigu.create

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

object Test01_FromList {
  def main(args: Array[String]): Unit = {
    // 1.创建sc的配置对象
    val conf: SparkConf = new SparkConf()
      .setAppName("sparkCore").setMaster("local[*]")

    // 2. 创建sc对象
    val sc = new SparkContext(conf)

    // 3. 编写任务代码
    val list = List(1, 2, 3, 4)

    // 从集合创建rdd
    val intRDD: RDD[Int] = sc.parallelize(list)
    intRDD.collect().foreach(println)

    // 底层调用parallelize   推荐使用  比较好记
    val intRDD1: RDD[Int] = sc.makeRDD(list)
    intRDD1.collect().foreach(println)

    // 4.关闭sc
    sc.stop()
  }
}

// 两种函数parallelize和makeRDD,两种方法大致一样,不过makeRDD还有Seq的构造方法

2.1.3 从外部存储系统的数据集创建

Spark支持从外部存储系统的数据集创建RDD:

  1. 本地文件系统
  2. Hadoop支持的数据集(HDFS、HBase)
package com.atguigu.create

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

object Test02_FromFile {
  def main(args: Array[String]): Unit = {
    // 1.创建sc的配置对象
    val conf: SparkConf = new SparkConf()
      .setAppName("sparkCore").setMaster("local[*]")

    // 2. 创建sc对象
    val sc = new SparkContext(conf)

    // 3. 编写任务代码
    // 不管文件中存的是什么数据  读取过来全部当做字符串处理
    val lineRDD: RDD[String] = sc.textFile("input/1.txt")

    lineRDD.collect().foreach(println)

    // 4.关闭sc
    sc.stop()
  }
}

2.1.4 从其它RDD创建

2.1.5 创建IEDA快捷键

2.2 分区规则

创建RDD的方式不一样,分区规则不一样。

2.2.1 从集合创建RDD

  1. 默认的分区数取决于分配给应用的CPU的核数

  2. 如果指定分区,那么最终分区数就为指定的数目

数据读取规则:利用整数除机制 左闭右开:
0 => start 0*5/2 end 1*5/2 [0,2)
1 => start 1*5/2 end 2*5/2 [2,5)

2.2.2 从文件创建RDD

  1. 指定最小分区数num和环境的核数取小值 math.min(defaultparallellism, num)
  2. 具体的分区数需要计算:首先获取文件的总长度 totalSize;计算平均长度 goalSize = totalSize / numSplits;获取块大小 128M;计算切分大小 splitSize = Math.max(minSize, Math.min(goalSize, blockSize));最后使用splitSize 按照1.1倍原则切分整个文件 得到几个分区就是几个分区;实际开发中,只需要看 文件总大小 / 填写的分区数 和 块大小比较 谁小拿谁进行切分
sc.textFile("input")

注意:getSplits文件返回的是切片规划,真正读取是在compute方法中创建LineRecordReader读取的,有两个关键变量: 
start = split.getStart()
end = start + split.getLength
①分区数量的计算方式:
totalSize = 10
goalSize = 10 / 3 = 3(byte) 表示每个分区存储3字节的数据
分区数= totalSize/ goalSize = 10 /3 => 3,3,4
4子节大于3子节的1.1倍,符合hadoop切片1.1倍的策略,因此会多创建一个分区,即一共有4个分区  3,3,3,1
②Spark读取文件,采用的是hadoop的方式读取,所以一行一行读取,跟字节数没有关系
③数据读取位置计算是以偏移量为单位来进行计算的。
④数据分区的偏移量范围的计算
0 => [0,3]         1@@     012        0 => 1,2
1 => [3,6]         2@@     345        1 => 3        
2 => [6,9]         3@@     678        2 => 4
3 => [9,9]         4         9          3 => 无

2.3 Transformation转换因子

RDD整体上分为Value、双Value和Key-Value类型

2.3.1 Value类型

2.3.1.1 Map() 映射
Map算子
def map[U: ClassTag](f: T => U): RDD[U]

功能说明:参数f是一个函数,它可以接收一个参数。当某个RDD执行map方法时,会遍历该RDD中的每一个数据项,并依次应用f函数,从而产生一个新的RDD。即,这个新RDD中的每一个元素都是原来RDD中每一个元素依次应用f函数而得到的。

val mapRDD: RDD[Int] = IntRDD.map(_ * 2)
2.3.1.2 MapPartitions() 以分区为单位执行Map
MapPartitions算子

def mapPartitions[U: ClassTag](
	f: iterator[T] => iteratot[U]
    preservesPartitioning: Boolean = false
): RDD[U]

功能说明:f函数把每一个分区的数据分别放入到迭代器中,批处理
preservesPartitioning: 是否保留上油RDD的分区信息,默认false
Map一次处理一个元素,而MapPartitions一次处理一个分区数据

val rdd1 = rdd.mapPartitions(x => x.map(_ * 2))

MapPartitions特点:

  1. 将RDD中的一个分区作为几个集合,进行转换结构
  2. 只是将一个分区一次性计算,最终还是修改单个元素的值
  3. 可以将RDD中的元素个数减少,只需要保证一个几个对应一个输出集合即可
2.3.1.3 map()和mapPartitions()区别

在这里插入图片描述

2.3.1.4 mapPartitionsWithIndex() 带分区号
mapPartitionsWithIndex算子
def mapPartitionsWithIndex[U: ClassTag](
	f: (Int, Iterator[T]) => Iterator[U]
    preserverPartitioning: Boolean = false
): RDD[U]

功能说明:Int表示分区号,功能类似于mapPartitions,逼mapPartitions多一个整数参数表示分区号

val indexRDD = rdd.mapPartitionsWithIndex((index, items) => {
    items.map((index, _))
})
2.3.1.5 flatMap()扁平化
def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]

功能说明:与map()操作类似,将RDD中的每一个元素通过引用f函数一次转换为新的元素,并封装到RDD中。
区别:在flatMap操作中,f函数的返回值是一个集合,并且会将每一个该集合中的元素拆分出来放到新的RDD中。

listRDD.flatMap(list => list)
2.3.1.6 groupBy() 分组
groupBy算子
def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, iterable[T])]

功能说明:按照传入函数的返回值进行分组,将相同key对应的值放入一个迭代器

rdd.groupBy(x => x * 2)
rdd.groupBy(_ % 2)
rdd.groupBy(str => strsubstring(0, 1))

groupBy会存在shuffle过程

shuffle:将不同的分区数据打乱进行重组的过程

shuffle一定会落盘,可以在local模式下执行程序,通过local:4040查看效果

2.3.1.7 filter() 过滤
filter算子
def filter(f: T => Boolean): RDD[T]

功能说明:接收一个返回值为Boolean类型的函数作为参数,当某个RDD调用filter方法时,会对该RDD中每一个元素引用f函数,如果返回值类型为true,则该元素会被添加到新的RDD中。

rdd.filter(_ % 2 == 0)
2.3.1.8 distinct() 去重
distinct算子
def distinct(): RDD[T]
def distinct(numPartition: Int)(inplicit ord: Ordering[T] = null): RDD[T] = withScope{
    map(x => (x, null)).reduceByKey((x, y) => x, numPartitions).map(_._1)
}

功能说明:对内部元素去重,并将去重后的元素放到新的RDD中。默认情况下,distinct()会生成与原RDD分区个数一致的分区数,distinct会存在shuffle过程
用分布式的方式去重比HashSet集合方式不容易OOM

rdd.distinct(2)
Int为并发度
2.3.1.9 coalesce() 合并分区

Coalesce算子包括:配置执行Shuffle和不配置Shuffle两种方式

coalesce算子
1 不执行Shuffle方式
def coalesce(numPartitions: Int, shuffle: Boolean = false,
            partitonCoalescer: Option[PartitionCoalescer] = Option.empty)
(implicit ord: Ordering[T] = null): RDD[T]

功能说明:缩减分区数,用于大数据集过滤后,提高小数据集的执行效率,默认false,不执行Shuffle
rdd.coalesce(2)

2 执行Shuffle方式
将参数设置为true
2.3.1.10 repartition() 重新分区(执行Shuffle)
repartition算子
def repartition(numPartitions: Int)(inplicit ord: Ordering[T] = null): RDD[T] = withScope {
    coalesce(numPartitions, shuffle = true)
}

功能说明:该操作内部其实执行的是coalesce操作,参数shuffle的默认值为true。无论是将分区数多的RDD转换为分区数少的RDD,还是将分区数少的RDD转换为分区数多的RDD,repartition操作都可以完成,因为无论如何都会经过shuffle过程。区分规则不是hash,因为平时使用的分区都是按照hash来实现的,repartition一般是对hash结果不满意,想要打散重新分区。

rdd.repartition(2)
2.3.1.11 coalesce和repartiiton区别
  1. coalesce重新分区,可以选择是否进行shuffle过程。由参数shuffle: Boolean = false | true决定
  2. repartition实际上是对用的coalesce,进行shuffle
  3. coalesce一般为缩减分区,如果扩大分区,不使用shuffle是没有意义的,repartition扩大分区执行shuffle
2.3.1.12 sortBy() 排序
sortBy算子
def sortBy[K](f: (T) => K,
             ascending: Boolean = true,
             numPartitions: Int = this.partitions.length)
(implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]

功能说明:该操作用于排序数据,在排序之前,可以将数据通过f函数进行处理,之后按照f函数处理的结果进行排序,默认为正序排列。排序后性产生的RDD的分区后与原RDD的分区数一致。

rdd.sortBy(num => num) //默认正序排列
rdd.sortBy(num => num, false) //倒序排列
rdd.sortBy(num => num.toInt) //按照字符的int值排序
rdd.sortBy(t => t) //按照tuple的第一个值排序,相等再按照第二个值排序

2.3.2 双Value类型交互

2.3.2.1 intersection() 交集
intersection算子
def intersection(other: RDD[T]): RDD[T]

功能说明:对原RDD和参数RDD求交集后返回一个新的RDD
利用shuffle的原理进行求交集,需要将所有的数据落盘shuffle效率很低,不推荐使用

rdd1.intersection(rdd2)
2.3.2.2 union() 并集不去重
union算子
def union(other: RDD[T]): RDD[T]

功能说明:对原RDD和参数RDD求并集后返回一个新的RDD

rdd1.union(rdd2)
2.3.2.3 subtract() 差值
substract算子
def substract(other: RDD[T]): RDD[T]

功能说明:计算差的一种函数,去除两个RDD中相同的元素,不同的RDD将保留下来。
同样使用shuffle的原理,将两个RDD的数据写入到相同的位置,进行求差集。
需要走shuffle,效率低,不推荐使用

rdd1.substract(rdd2)
2.3.2.4 zip() 拉链
zip算子
def zip[U: ClassTag](other: RDD[U]): RDD[(T, U)]

功能说明:该操作可以将两个RDD中的元素,以KV对的形式进行合并。其中,键值对中的Key为第1个RDD中的元素,Value为第2个RDD中的元素。将两个RDD组合成Key/Value形式的RDD,这里默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常。
当两个RDD元素数量不同的时候,不能拉链

rdd1.zip(rdd2)

2.3.3 Key-Value类型

2.3.3.1 partitionBy() 按照Key重新分区
partitionBy算子
def partitionBy(partitioner: Partitioner): RDD[(K, V)]

功能说要:将RDD[K, V]中的K按照指定Partitioner重新进行分区,如果原有的RDD和新的RDD分区数一致的话,就不进行分区,否则会产生Shuffle过程。

rdd.partitionBy(new org.apache.spark.HashPartitioner(2))
2.3.3.2 自定义分区
// HashPartitioner源码
class HashPartitioner(partitions: Int) extends Partitioner {

    require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")
    
    def numPartitions: Int = partitions
    
    def getPartition(key: Any): Int = key match {
        case null => 0
        case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
    }
    
    override def equals(other: Any): Boolean = other match {
        case h: HashPartitioner =>
            h.numPartitions == numPartitions
        case _ =>
            false
    }
    
    override def hashCode: Int = numPartitions
}
// 自定义分区器
要实现自定义分区器,需要继承org.apache.spark.Partitioner类,并实现下面三个方法:
1 numPartitions: Int:  返回创建出来的分区数
2 getPartition(key: Any): Int:  返回给定键的分区编号(0 - numPartitions-13 equals(): Java  判断相等性的标准方法,这个方法的实现非常重要,Spark需要用这个方法来检查你的分区器对象时否和其他分区器实例相同,这样Spark才可以判断两个RDD的分区方式是否相同。

class MyPartitioner(num: Int) extends Partitioner {
    // 设置分区数
    override def numPartitions: Int = num
    
    // 具体分区逻辑
    // 根据传入数据的key,输出目标的分区号
    // Spark中不能根据value进行分区,只能根据key进行分区
    override def getPartition(key: Any): Int = {
        // 使用模式匹配,对类型进行推断
        key match {
            case s: String => 0
            case i: Int => i % numPartitions
            case _ => 0
        }
    }
}

rdd.partitionBy(new MyPartitioner(2))
2.3.3.3 groupByKey() 按照Key重新分组
groupByKey算子
def groupByKey(): RDD[(K, Iterable[V])]

功能说明:groupByKey对每个key进行操作,但只生成一个seq,并不进行聚合。该操作可以指定分区器或分区数(默认使用HashPartitioner)

rdd.groupByKey()
rdd.map(t => (t._1, t._2.sum))
2.3.3.4 reduceByKey() 按照Key聚合Value
reduceByKey算子
def reduceByKey(func: (V, V) => V): RDD[(K, V)]
def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]

功能说明:该操作可以将RDD[K, V]中的元素按照相同的Key对Value进行聚合,其存在多种重载形式,还可以设置新RDD的分区数。

rdd.reduceByKey((v1, v2) => v1 + v2)
2.3.3.5 reduceByKey和groupByKey的区别
  1. reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[K, V]

  2. groupByKey:按照key进行分组,直接进行shuffle

  3. 开发指导:在不影响业务逻辑的前提下,优先选用reduceByKey。求和操作不影响业务逻辑,求平均值影响业务逻辑,后续会学习功能更加强大的规约算子,能够在预聚合的情况下实现求平均值。

2.3.3.6 aggregateByKey() 分区内和分区间逻辑不同的规约
aggregateByKey算子
def aggregateByKey[U: ClassTag](zeorVale: U)(seqOp: (U, V) => U, combOp: (U, U) => U): RDD[(K, U)]
zeroValue(初始值):给每一个分区中的每一种key一个初始值
seqOp(分区内):函数用于在每个分区中用初始值逐步迭代value
combOp(分区间):函数用于合并每个分区中的结果

rdd.aggregateByKey(0)(math.max(_, _), _ + _)
2.3.3.7 sortByKey() 按照Key进行排序
sortByKey算子
def sortByKey(
	ascending: Boolean = true,
    numPartitions: Int = self.partitions.length
): RDD[(K, V)]

功能说明:在一个(K, V)的RDD上调用,key必须实现Ordered接口,返回一个按照key进行排序的(K, V)的RDD,默认正序。

rdd.sortByKey(true)
rdd.sortBySKey(false)  // 按照key来倒序

只会按照key来排序,最终的结果是key有序,value不会排序。Spark的排序是全局排序,不会进行hash shuffle处理
2.3.3.8 mapValues() 只对Value进行操作
mapValues算子
def mapValues[U](f: V => U): RDD[(K, U)]

功能说明:针对于(K, V)形式的类型只对Value进行操作

rdd.mapValues(_ + "|||")
2.3.3.9 join() 等同于SQL里的内连接,关联上的留下,关联不上的舍去
join算子
def join[W](other: RDD[(K, V)]): RDD[(K, (V, W))]
def join[W](other: RDD[(K, V)], numPartitions: Int): RDD[(K, (V, W))]

功能说明:在类型为(K, V)(K, W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K, (V, W))的RDD,如果某些key是一个RDD特有的,这个key不会关联。

rdd1.join(rdd2)
2.3.3.10 cogroup() 类似于SQL的全连接,但是在同一个RDD中对key聚合
cogroup聚合
def cogroup[W](other: RDD[(K, W)]): RDD[K, (Iterable[V], Iterable[W])]

功能说明:在类型为(K, V)(K, W)的RDD上调用,返回一个(K, (Iterable<V>, Iterable<W>))类型的RDD。操作两个RDD中的KV元素,每个RDD中相同key中的元素分别聚合成一个集合。

rdd1.cogroup(rdd2)

2.3.4 案例实操(省份广告被点击Top3)

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

object Test01_DemoTop3 {
  def main(args: Array[String]): Unit = {
    // 1 创建sc配置对象
    // local | yarn
    val conf = new SparkConf()
      .setAppName("sparkCore").setMaster("local[*]")

    // 2 创建sc对象
    val sc = new SparkContext(conf)

    // 3 编写任务代码
    val lineRDD: RDD[String] = sc.textFile("input/agent.log")
    // 过滤出需要的数据,将一行的数据转换为((省份, 广告id), 1)
    val tupleRDD: RDD[((String, String), Int)] = lineRDD.map(line => {
      val data = line.split(" ")
      ((data(1), data(4)), 1)
    })

    // 对((省份, 广告id), count) 进行wordcount
    val provienceCountRDD: RDD[((String, String), Int)] = tupleRDD.reduceByKey(_ + _)

    // 转换数据格式,便于下一步按省份聚合,((省份, 广告id), count) => (省份, (广告id, count))
    // 偏函数写法
    val provienceCountRDD1: RDD[(String, (String, Int))] = provienceCountRDD.map({
      case ((province, id), count) => (province, (id, count))
    })
    // 匿名函数写法
    val provienceCountRDD2: RDD[(String, (String, Int))] = provienceCountRDD.map(tuple => (tuple._1._1, (tuple._1._2, tuple._2)))

    // 将相同省份聚合在一起,(省份, 广告id), count)
    val provienceRDD: RDD[(String, Iterable[(String, Int)])] = provienceCountRDD1.groupByKey()

    // 进行排序,对一个元素里面的value值进行排序
    val resule: RDD[(String, List[(String, Int)])] = provienceRDD.mapValues(list => {
      list.toList.sortBy(_._2)(Ordering[Int].reverse).take(3)
    })

    resule.collect().foreach(println)
    // 4 关闭sc
    sc.stop()
  }
}

2.4 Action行动算子

行动算子是触发了整个作业的执行,因为转换算子都是懒加载,并不会立即执行。

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

collect算子
def collect(): Array[T]

功能说明:在驱动程序上,以数组Array的形式返回数据集的所有元素,此时所有的数据都会被拉取到Driver端,慎重使用。

rdd.collect().foreach(println)

在这里插入图片描述

2.4.2 count() 返回RDD中元素个数

count算子
def count(): Long

功能说明:返回RDD中元素的个数

rdd.count()

在这里插入图片描述

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

first算子
def first(): T

功能说明:赶回RDD中的第一个元素

rdd.first()

在这里插入图片描述

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

take算子
def take(num: Int): Array[T]

功能说明:返回一个由RDD的前n个元素组成的数组

rdd.take(3)
2.4.5 takeOrdered() 返回该RDD排序后前n个元素组成的数组
takeOrdered算子
def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T] = withScope {
    ......
    if (mapRDDs.partitions.length == 0) {
        Array.empty
    } else {
        mapRDDs.reduce { (queue1, queue2) =>
          queue1 ++= queue2
          queue1
        }.toArray.sorted(ord)
    }
}

功能说明:返回该RDD排序后的前n个元素组成的数组,底层调用的是sorted()

rdd.takeOrdered(2)

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

countByKey算子
def countByKey(): Map[K, Long]

功能说明:统计每种key的个数

rdd.countByKey()

2.4.7 save相关算子

1 saveAsTextFile(path) 保存成Text文件
功能说明:将数据集的元素以textfile的形式保存到HDFS文件系统或其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它转换为文件中的文本

2 saveAsSequenceFile(path) 保存成Sequencefile文件
功能说明:将数据集中的元素以Hadoop Sequencefile的格式保存到指定的目录下,可以使用HDFS或者其它Hadoop支持的文件系统。
只有kv类型RDD有该操作,单值对象没有。

3 saveAsObjectFile(path) 序列化成对象保存到文件
功能说明:用于将RDD中的元素序列化成对象,存储到文件中。

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

foreach算子
def foreach(f: T => Unit): Unit

功能说明:遍历RDD中每一个元素,并一次应用f函数。

rdd.collect().foreach(println)
rdd.foreach(println)

区别:collect()是有序的,排序后输出,foreach是无序的,Driver直接输出

2.5 RDD序列化

在实际开发中我们往往需要自己定义一些对于RDD的操作,那么此时性要注意的是,初始化的工作是在Driver端进行的,而实际运行程序是在Executor端进行的,这就涉及到了跨进程通信,是需要序列化的。

2.5.1 闭包检查

有闭包就需要进行序列化

object serializable01_object {

    def main(args: Array[String]): Unit = {

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        //3.创建两个对象
        val user1 = new User()
        user1.name = "zhangsan"

        val user2 = new User()
        user2.name = "lisi"

        val userRDD1: RDD[User] = sc.makeRDD(List(user1, user2))

        //3.1 打印,ERROR报java.io.NotSerializableException
        //userRDD1.foreach(user => println(user.name))
        

        //3.2 打印,RIGHT (因为没有传对象到Executor端)
        val userRDD2: RDD[User] = sc.makeRDD(List())
        //userRDD2.foreach(user => println(user.name))

        //3.3 打印,ERROR Task not serializable 
//注意:此段代码没执行就报错了,因为spark自带闭包检查
        userRDD2.foreach(user => println(user.name+" love "+user1.name))

        //4.关闭连接
        sc.stop()
    }
}

//case class User() {
//    var name: String = _
//}
class User extends Serializable {
    var name: String = _
}

2.5.2 Kryo序列化框架

https://github.com/EsotericSoftware/kryo

Java的序列化能够序列化任何的类。但是比较重,序列化后对象的体积也比较大。

Spark出于性能的考虑,Spark2.0开始支持另外一种Kryo序列化机制。Kryo速度是Serializable的10倍。当RDD在Shuffle数据的时候,简单数据类型、数组和字符串类型已经在Spark内部使用Kryo来序列化。

object serializable02_Kryo {

    def main(args: Array[String]): Unit = {

        val conf: SparkConf = new SparkConf()
                .setAppName("SerDemo")
                .setMaster("local[*]")
                // 替换默认的序列化机制
                .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
                // 注册需要使用kryo序列化的自定义类
                .registerKryoClasses(Array(classOf[Search]))

        val sc = new SparkContext(conf)

        val rdd: RDD[String] = sc.makeRDD(Array("hello world", "hello atguigu", "atguigu", "hahah"), 2)

        val search = new Search("hello")
        val result: RDD[String] = rdd.filter(search.isMatch)

        result.collect.foreach(println)
}
  // 关键字封装在一个类里面
  // 需要自己先让类实现序列化  之后才能替换使用kryo序列化
  class Search(val query: String) extends Serializable {
    def isMatch(s: String): Boolean = {
      s.contains(query)
    }
  }
}

2.6 RDD依赖关系

2.6.1 查看血缘关系

RDD只支持粗粒度转化,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,他可以根据这些信息来重新运算和恢复丢失的数据分区。

通过 toDebugString方法,查看RDD血缘关系。圆括号中的数字表示RDD的并行度,也就是有几个分区。

在这里插入图片描述

object Lineage01 {

def main(args: Array[String]): Unit = {

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        val fileRDD: RDD[String] = sc.textFile("input/1.txt")
        println(fileRDD.toDebugString)
        println("----------------------")

        val wordRDD: RDD[String] = fileRDD.flatMap(_.split(" "))
        println(wordRDD.toDebugString)
        println("----------------------")

        val mapRDD: RDD[(String, Int)] = wordRDD.map((_,1))
        println(mapRDD.toDebugString)
        println("----------------------")

        val resultRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
        println(resultRDD.toDebugString)

        resultRDD.collect()

        //4.关闭连接
        sc.stop()
    }
}

2.6.2 查看依赖关系

通过dependencies方法,查看RDD依赖关系

在这里插入图片描述

object Lineage02 {

    def main(args: Array[String]): Unit = {

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        val fileRDD: RDD[String] = sc.textFile("input/1.txt")
        println(fileRDD.dependencies)
        println("----------------------")

        val wordRDD: RDD[String] = fileRDD.flatMap(_.split(" "))
        println(wordRDD.dependencies)
        println("----------------------")

        val mapRDD: RDD[(String, Int)] = wordRDD.map((_,1))
        println(mapRDD.dependencies)
        println("----------------------")

        val resultRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
        println(resultRDD.dependencies)

        resultRDD.collect()

        // 查看localhost:4040页面,观察DAG图
Thread.sleep(10000000)

        //4.关闭连接
        sc.stop()
    }
}

// 
List(org.apache.spark.OneToOneDependency@f2ce6b)
----------------------
List(org.apache.spark.OneToOneDependency@692fd26)
----------------------
List(org.apache.spark.OneToOneDependency@627d8516)
----------------------
List(org.apache.spark.ShuffleDependency@a518813)

// 全局搜索(ctrl+n)org.apache.spark.OneToOneDependency
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
    override def getParents(partitionId: Int): List[Int] = List(partitionId)
}

注意:要想理解RDDS是如何工作的,最重要的就是理解Transformations。
RDD之间的关系可以从两个维度来理解:一个是RDD是从哪些RDD转换而来,也就是 RDD的parent RDD(s)是什么(血缘); 另一个就是RDD依赖于parent RDD(s)的哪些Partition(s),这种关系就是RDD之间的依赖(依赖)。

2.6.3 窄依赖

窄依赖表示每一个父RDD的Partition最多被子RDD的一个Partition使用(一对一 or 多对一),窄依赖我们形象的比喻为独生子女。

2.6.4 宽依赖

宽依赖表示同一个父RDD的Partition被多个子RDD的Partition依赖(只能是一对多),会引起Shuffle,宽依赖我们形象的比喻为超生。

具有宽依赖的transformations包括:sort、reduceByKey、groupByKey、join和调用rePartition函数的任何操作。

宽依赖对Spark去评估一个transformations有更加重要的影响,比如对性能的影响。在不影响业务要求的情况下,要尽量避免使用有宽依赖的转换算子,因为有宽依赖,就一定会走shuffle,影响性能。

2.6.5 Stage任务划分

2.6.5.1 DAG有向无环图

DAG(Ditected Acyclic Graph)有向无环图时由点和线组成的拓扑图形,该图形具有方向,不会闭环。

2.6.5.2 任务运行的整体流程

在这里插入图片描述
在这里插入图片描述

2.6.5.3 RDD任务切分

在这里插入图片描述

  1. Application:初始化一个SparkContext,即生成一个Application
  2. Job:一个Action算子就会生成一个Job
  3. Stage:Stage等于宽依赖的个数 + 1
  4. Task:一个Stage阶段中,最后一个RDD的分区数就是Task的个数

Application > Job > Stage > Task 每一层都是1对n的关系

object Lineage03 {
  def main(args: Array[String]): Unit = {
    //TODO 1 创建SparkConf配置文件,并设置App名称
    val conf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
    //TODO 2 利用SparkConf创建sc对象
    //Application:初始化一个SparkContext即生成一个Application
    val sc = new SparkContext(conf)

    //textFile,flatMap,map算子全部是窄依赖,不会增加stage阶段
    val lineRDD: RDD[String] = sc.textFile("D:\\IdeaProjects\\SparkCoreTest\\input\\1.txt")
    val flatMapRDD: RDD[String] = lineRDD.flatMap(_.split(" "))
    val mapRDD: RDD[(String, Int)] = flatMapRDD.map((_, 1))

    //reduceByKey算子会有宽依赖,stage阶段加1,2个stage
    val resultRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_ + _)

    //Job:一个Action算子就会生成一个Job,2个Job
    //job0打印到控制台
    resultRDD.collect().foreach(println)
    //job1输出到磁盘
    resultRDD.saveAsTextFile("D:\\IdeaProjects\\SparkCoreTest\\out")

    //阻塞线程,方便进入localhost:4040查看
    Thread.sleep(Long.MaxValue)

    //TODO 3 关闭资源
    sc.stop()
  }
}

如果存在Shuffle过程,系统会自动缓存,UI界面显示skipped的部分。

2.7 RDD持久化

2.7.1 RDD Cache缓存

RDD通过Cache或Persist方法将前面的计算结果缓存。默认情况下会把数据以序列化的形式缓存在JVM的堆内存中,但是并不是这两个方法被调用时立即缓存,而是除法后面的action算子时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
在这里插入图片描述

object cache01 {

    def main(args: Array[String]): Unit = {

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        //3. 创建一个RDD,读取指定位置文件:hello atguigu atguigu
        val lineRdd: RDD[String] = sc.textFile("input1")

        //3.1.业务逻辑
        val wordRdd: RDD[String] = lineRdd.flatMap(line => line.split(" "))

        val wordToOneRdd: RDD[(String, Int)] = wordRdd.map {
            word => {
                println("************")
                (word, 1)
            }
        }

        //3.5 cache缓存前打印血缘关系
        println(wordToOneRdd.toDebugString)

        //3.4 数据缓存。
//cache底层调用的就是persist方法,缓存级别默认用的是MEMORY_ONLY
        wordToOneRdd.cache()

        //3.6 persist方法可以更改存储级别
        // wordToOneRdd.persist(StorageLevel.MEMORY_AND_DISK_2)

        //3.2 触发执行逻辑
        wordToOneRdd.collect().foreach(println)
        
        //3.5 cache缓存后打印血缘关系
//cache操作会增加血缘关系,不改变原有的血缘关系
        println(wordToOneRdd.toDebugString)

        println("==================================")
        
        //3.3 再次触发执行逻辑
        wordToOneRdd.collect().foreach(println)

        Thread.sleep(1000000)

        //4.关闭连接
        sc.stop()
    }
}
// 源码解析

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

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) // 堆外缓存

默认的存储级别都是仅在内存存储一份,在存储级别的末尾加上 “_2”表示持久化的数据存储为两份。SER:表示序列化。
在这里插入图片描述

缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存数据丢失也能保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,由于RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部的Partition。

自带缓存算子

Spark会自动对一些Shuffle操作的中间数据做持久化操作(比如reduceByKey)。这样做的目的是为了当一个节点Shuffle失败时,避免重新计算整个输入。但是,在实际使用的时候,如果想要重用数据,仍然建议调用persist或cache。

object cache02 {

    def main(args: Array[String]): Unit = {

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        //3. 创建一个RDD,读取指定位置文件:hello atguigu atguigu
        val lineRdd: RDD[String] = sc.textFile("input1")

        //3.1.业务逻辑
        val wordRdd: RDD[String] = lineRdd.flatMap(line => line.split(" "))

        val wordToOneRdd: RDD[(String, Int)] = wordRdd.map {
            word => {
                println("************")
                (word, 1)
            }
        }

        // 采用reduceByKey,自带缓存
        val wordByKeyRDD: RDD[(String, Int)] = wordToOneRdd.reduceByKey(_+_)

        //3.5 cache操作会增加血缘关系,不改变原有的血缘关系
        println(wordByKeyRDD.toDebugString)

        //3.4 数据缓存。
        //wordByKeyRDD.cache()

        //3.2 触发执行逻辑
        wordByKeyRDD.collect()

        println("-----------------")
        println(wordByKeyRDD.toDebugString)

        //3.3 再次触发执行逻辑
        wordByKeyRDD.collect()

        Thread.sleep(1000000)

        //4.关闭连接
        sc.stop()
    }
}

访问http://localhost:4040/jobs/页面,查看第一个和第二个job的DAG图。说明:增加缓存后血缘依赖关系仍然有,但是,第二个job取的数据是从缓存中取的。

2.7.2 RDD CheckPoint检查点

检查点:通过RDD中间结果写入磁盘

设置检查点的原因:由于血缘依赖过长会曹成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销。

检查点存储路径:Checkpoint的数据通常是存储在HDFS等高容错高可用的文件系统。

检查点数据存储格式为:二进制文件

检查点切断血缘:在Checkpoint的过程中,该RDD的所有依赖与父RDD中的信息将全部被移除

检查点触发时间:对RDD进行Checkpoint操作并不会马上被执行,必须执行Action操作才能除法。但是检查点为了数据安全,会从血缘关系的最开始执行一遍。
在这里插入图片描述

设置检查点步骤:

  1. 设置检查点数据存储路径:sc.setCheckpointDir(“./checkppoint1”)
  2. 调用检查点方法:wordToOneRdd.checkpoint()
object checkpoint01 {

    def main(args: Array[String]): Unit = {

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        // 需要设置路径,否则抛异常:Checkpoint directory has not been set in the SparkContext
        sc.setCheckpointDir("./checkpoint1")

        //3. 创建一个RDD,读取指定位置文件:hello atguigu atguigu
        val lineRdd: RDD[String] = sc.textFile("input1")

        //3.1.业务逻辑
        val wordRdd: RDD[String] = lineRdd.flatMap(line => line.split(" "))

        val wordToOneRdd: RDD[(String, Long)] = wordRdd.map {
            word => {
                (word, System.currentTimeMillis())
            }
        }

        //3.5 增加缓存,避免再重新跑一个job做checkpoint
//        wordToOneRdd.cache()

        //3.4 数据检查点:针对wordToOneRdd做检查点计算
        wordToOneRdd.checkpoint()

        //3.2 触发执行逻辑
        wordToOneRdd.collect().foreach(println)
        // 会立即启动一个新的job来专门的做checkpoint运算

        //3.3 再次触发执行逻辑
        wordToOneRdd.collect().foreach(println)
        wordToOneRdd.collect().foreach(println)

        Thread.sleep(10000000)

        //4.关闭连接
        sc.stop()
    }
}

访问http://localhost:4040/jobs/页面,查看4个job的DAG图。其中第2个图是checkpoint的job运行DAG图。第3、4张图说明,检查点切断了血缘依赖关系。
在这里插入图片描述

1 只增加checkpoint,不增加Cache缓存打印
第1个job执行完,触发了checkpoint,第2个job运行checkpoint,并把数据存储在检查点上,第34个job,数据从检查点上直接获取。
(hadoop,1577960215526)
。。。。。。
(hello,1577960215526)
(hadoop,1577960215609)
。。。。。。
(hello,1577960215609)
(hadoop,1577960215609)
。。。。。。
(hello,1577960215609)

2 增加checkpoint,也增加Cache缓存打印
第1个job执行完,数据就保存到Cache里面了,第2个job运行checkpoint,直接读取Cache里面的数据,并把数据存储在检查点上,第34个job,数据从检查点上直接读取
(hadoop,1577960642223)
。。。。。。
(hello,1577960642225)
(hadoop,1577960642223)
。。。。。。
(hello,1577960642225)
(hadoop,1577960642223)
。。。。。。
(hello,1577960642225)

2.7.3 缓存和检查点的区别

  1. Cache缓存只是将数据保存下来,不切断血缘依赖。Checkpoint检查点切断血缘依赖。
  2. Cache缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint存储的数据通常存储在HDFS等高容错、高可用的文件系统,可靠性高。
  3. 建议对checkpoint()的RDD使用Cache缓存,这样checkpoint的job只需从Cache缓存中读取数据即可,否则需要再从头计算一次RDD。
  4. 如果使用完了缓存,可以通过unpersist()方法释放缓存

2.7.4 检查点存储到HDFS

如果检查点数据存储到HDFS集群,要注意配置访问集群的用户名,否则会报访问权限异常。

object checkpoint02 {

    def main(args: Array[String]): Unit = {

        // 设置访问HDFS集群的用户名
        System.setProperty("HADOOP_USER_NAME","atguigu")

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        // 需要设置路径.需要提前在HDFS集群上创建/checkpoint路径
        sc.setCheckpointDir("hdfs://hadoop102:8020/checkpoint")

        //3. 创建一个RDD,读取指定位置文件:hello atguigu atguigu
        val lineRdd: RDD[String] = sc.textFile("input1")

        //3.1.业务逻辑
        val wordRdd: RDD[String] = lineRdd.flatMap(line => line.split(" "))

        val wordToOneRdd: RDD[(String, Long)] = wordRdd.map {
            word => {
                (word, System.currentTimeMillis())
            }
        }

        //3.4 增加缓存,避免再重新跑一个job做checkpoint
        wordToOneRdd.cache()

        //3.3 数据检查点:针对wordToOneRdd做检查点计算
        wordToOneRdd.checkpoint()

        //3.2 触发执行逻辑
        wordToOneRdd.collect().foreach(println)

        //4.关闭连接
        sc.stop()
    }
}

2.8 键值对RDD数据分区

Spark目前支持Hash分区、Range分区和用户自定义分区。Hash分区为当前的默认分区,分区器直接决定了RDD中分区的个数,RDD中每条数据经过Shuffle后进入那个分区和Reduce的个数。

  1. 只有Key-Value类型的RDD才有分区器,非Key-Value类型的RDD分区的值是None (调用RDD.partitioner查看)
  2. 每个RDD的分区ID范围:0 ~ numPartitions-1 ,决定这个值是属于那个分区的。
  3. 非KV类型的RDD转换成KV类型的RDD,也不会有分区器
object partitioner01_get {

    def main(args: Array[String]): Unit = {

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        //3 创建RDD
        val pairRDD: RDD[(Int, Int)] = sc.makeRDD(List((1,1),(2,2),(3,3)))

        //3.1 打印分区器
        println(pairRDD.partitioner)

        //3.2 使用HashPartitioner对RDD进行重新分区
        val partitionRDD: RDD[(Int, Int)] = pairRDD.partitionBy(new HashPartitioner(2))

        //3.3 打印分区器
        println(partitionRDD.partitioner)

        //4.关闭连接
        sc.stop()
    }
}

2.8.1 Hash分区

HashPartitioner分区的原理:对于给定的key,计算hashcode,并除以分区数取余,如果余数小于0,则用余数 + 分区的个数(否则+0),最后返回的值就是这个key所属的分区ID。

在这里插入图片描述

HashPartitioner分区弊端:因为其随机机制,可能导致每个分区中数据量不均,极端情况下某些分区拥有RDD的全部数据。

2.8.2 Ranger分区

RangePartitioner作用:将一定范围内的数据映射到某一个分区内,尽量保证每个分区中数据量均衡,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或大,但是分区内的元素不能保证顺序。简单来说就是将一定范围内的数据映射到某一个分区内。

实现过程:

  1. 先从整个RDD中采用水塘抽样算法,抽取出样本数据,将样本数据排序,计算出每个分区的最大key值,形成一个Array[KEY]类型的数组变量rangeBounds;
  2. 判断key在rangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标,该分区器要求RDD中的key类型必须是可以排序的

2.8.3 自定义分区

见2.3.3.2

3 累加器

累加器:分布式共享只写变量(Executor和Executor之间不能互相读取数据)

累加器正是为了解决这一问题,可以将Executor端变量信息聚合到Driver端,在Driver端定义一个变量,在Executor端的每个task都会得到这个变量的一份新的副本,每个task更新这些副本的值后,传会Driver端进行合并计算。

累加器使用
1 定义 SparkContext.accumulator(initialValue)方法
val sum: LongAccumulator = sc.longAccumulator("sum")

2 累加器添加数据(累加器.add方法)
sum.add(count)

3 累加器获取数据(累加器.value)
sum.value
object accumulator01_system {
package com.atguigu.cache
import org.apache.spark.rdd.RDD
import org.apache.spark.util.LongAccumulator
import org.apache.spark.{SparkConf, SparkContext}

object accumulator01_system {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]") 
val sc = new SparkContext(conf)

    val dataRDD: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("a", 2), ("a", 3), ("a", 4)))
    //需求:统计a出现的所有次数 ("a",10)

    //普通算子实现 reduceByKey 代码会走shuffle 效率低
    //val rdd1: RDD[(String, Int)] = dataRDD.reduceByKey(_ + _)

//普通变量无法实现 
//结论:普通变量只能从driver端发给executor端,在executor计算完以后,结果不会返回给driver端
/*
    var sum = 0

    dataRDD.foreach{
      case (a,count) => {
        sum += count
        println("sum = " + sum)
      }
    }

    println(("a",sum))
*/
    //累加器实现
    //1 声明累加器
    val accSum: LongAccumulator = sc.longAccumulator("sum")

    dataRDD.foreach{
      case (a,count) => {
        //2 使用累加器累加  累加器.add()
        accSum.add(count)
        // 4 不要在executor端获取累加器的值,因为不准确 
//因此我们说累加器叫分布式共享只写变量
        //println("sum = " + accSum.value)
      }
    }
    //3 获取累加器的值 累加器.value
    println(("a",accSum.value))

    sc.stop()
  }
}

Executor端的任务不能读取累加器的值(如在Executor端调用sum.value,得到的也不是累加器最终的值)。因此,我们说,累加器是一个分布式共享只写变量。

累加器要放在行动算子中

因为转换算子执行的次数取决于job的数量,如果一个Spark引用有多个行动算子,那么转换算子中的累加器可能会发生不止一次更新,导致结果错误。所以,如果想要一个无论是失败还是重新计算时都绝对可靠的累加器,我们都必须把它放在foreach()这样的行动算子中。

对于在行动算子中使用的累加器,Spark只会把每个Job对各累加器的修改执行一次。

object accumulator02_updateCount {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
    val sc = new SparkContext(conf)

    val dataRDD: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("a", 2), ("a", 3), ("a", 4)))
    //需求:统计a出现的所有次数 ("a",10)
    //累加器实现
    //1 声明累加器
    val accSum: LongAccumulator = sc.longAccumulator("sum")

    val mapRDD: RDD[Unit] = dataRDD.map {
      case (a, count) => {
        //2 使用累加器累加  累加器.add()
        accSum.add(count)
        // 4 不要在executor端获取累加器的值,因为不准确 因此我们说累加器叫分布式共享只写变量
        //println("sum = " + accSum.value)
      }
    }

    //调用两次行动算子,map执行两次,导致最终累加器的值翻倍
    mapRDD.collect()
    mapRDD.collect()

    /**
     * 结论:使用累加器最好要在行动算子中使用,因为行动算子只会执行一次,而转换算子的执行次数不确定!
     */ 
    //2 获取累加器的值 累加器.value
    println(("a",accSum.value))
    
    sc.stop()
  }
}

调用两次行动算子,map会执行两次,导致最终累加值翻倍。因此使用累加器最好要在行动算子中使用,因为行动算子只会执行一次,而转换算子的执行次数不确定!

一般在开发中使用的累加器为集合累加器,在某些场景可以减少Shuffle。

// 用累加器实现wordcount

object Test02_Acc {
  def main(args: Array[String]): Unit = {
    //1、创建sparkcontext配置
	val conf = new SparkConf().setMaster("local[4]").setAppName("test")
	//2、创建sparkcontext
	val sc = new SparkContext(conf)
	//3、创建集合累加器,累加元素为Map
	val acc = sc.collectionAccumulator[mutable.Map[String,Int]]
	//4、读取文件
	val rdd1 = sc.textFile("datas/wc.txt")
	//5、切割+转换
	val rdd2 = rdd1.flatMap(x=>x.split(" "))
	//6、转换为KV键值对
	val rdd3 = rdd2.map(x=>(x,1))
	//7、使用foreachPartitions在每个分区中对所有单词累加,将累加结果放入累加器中
	rdd3.foreachPartition(it=> {
  		//创建一个累加Map容器
  		val map = mutable.Map[String,Int]()
  		//遍历分区数据
  		it.foreach(x=>{
    		val num = map.getOrElse(x._1,0)
    		//将单词累加到map容器中
    		map.put(x._1,num+x._2)
  		})
  		//将装载分区累加结果的map容器放入累加器中
  		acc.add(map)

	})
	//为了方便操作,将java集合转成scala集合
	import scala.collection.JavaConverters._
	//获取累加器结果,此时List中的每个Map是之前放入累加器的分区累加结果Map
	val r = acc.value.asScala
	//压平,将所有分区计算结果放入List中
	val pList = r.flatten
	//0按照单词分组
	val rMap = pList.groupBy(x=>x._1)
	//,统计每个单词总个数
	val result = rMap.map(x => (x._1, x._2.map(_._2).sum))

	println(result)

    // 4.关闭sc
    sc.stop()
  }
}

4 广播变量

广播变量:分布式共享只读变量

广播变量用来高效分发较大的对象,向所有工作节点发送一个较大的只读值,以供一个或多个Spark Task操作使用。比如,当引用需要向所有节点发送一个较大的只读查询表,广播变量用起来会很方便,在多个Task并行操作中使用同一变量,但Spark会为每个Task任务分别发送。

使用步骤:

  1. 调用SparkContext.broadcast(广播变量)创建出一个广播对象,任何可序列化的类型都可以这么实现。
  2. 通过广播变量.value。访问该对象的值。
  3. 广播变量只会被发到各个节点一次,作为只读值处理(修改这个值,不会影响到别的节点)。

原理说明:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q9VjPoDQ-1673115442793)(SparkCore.assets/image-20220609114541999.png)]

package com.atguigu.broadcast

import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}


object Test01_broadcast {
  def main(args: Array[String]): Unit = {
    // 1 创建sc配置对象
    // local | yarn
    val conf = new SparkConf()
      .setAppName("sparkCore")
      .setMaster("local[*]")

    // 2 创建sc对象
    val sc = new SparkContext(conf)

    // 3 辨析任务代码
    val listRDD: RDD[String] = sc.makeRDD(List("WARN:Class Not Find", "INFO:Class Not Find", "DEBUG:Class Not Find",
      "111")
      , 4)

    //listRDD.collect().foreach(println)
    val str: String = "WARN"

    val bdStr: Broadcast[String] = sc.broadcast(str)

    val filterRDD: RDD[String] = listRDD.filter {
      //log => log.contains(str)} //WARN:Class Not Find
      log => log.contains(bdStr.value)} //WARN:Class Not Find

    filterRDD.collect().foreach(println)
    // 4 关闭sc
    sc.stop()
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值