Spark:实时数据微批处理(2.Spark Core:核心)

1.RDD 概述

1.1 什么是 RDD?

RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象。
在代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。

1.2 RDD 的 5 个主要属性(property)

  1. A list of partitions
    多个分区.
    分区可以看成是数据集的基本组成单位.
    对于 RDD 来说, 每个分区都会被一个计算任务处理, 并决定了并行计算的粒度.
    用户可以在创建 RDD 时指定 RDD 的分区数, 如果没有指定, 那么就会采用默认值. 默认值就是程序所分配到的 CPU Core 的数目.
    每个分配的存储是由BlockManager 实现的. 每个分区都会被逻辑映射成 BlockManager 的一个 Block, 而这个 Block 会被一个 Task 负责计算.

  2. A function for computing each split
    计算每个切片(分区)的函数.
    Spark 中 RDD 的计算是以分片为单位的, 每个 RDD 都会实现 compute 函数以达到这个目的.

  3. A list of dependencies on other RDDs
    与其他 RDD 之间的依赖关系
    RDD 的每次转换都会生成一个新的 RDD, 所以 RDD 之间会形成类似于流水线一样的前后依赖关系. 在部分分区数据丢失时, Spark 可以通过这个依赖关系重新计算丢失的分区数据, 而不是对 RDD 的所有分区进行重新计算.

  4. Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
    对存储键值对的 RDD, 还有一个可选的分区器.
    只有对于 key-value的 RDD, 才会有 Partitioner, 非key-value的 RDD 的 Partitioner 的值是 None. Partitioner 不但决定了 RDD 的本区数量, 也决定了 parent RDD Shuffle 输出时的分区数量.

  5. Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
    存储每个切片优先(preferred location)位置的列表.
    比如对于一个 HDFS 文件来说, 这个列表保存的就是每个 Partition 所在文件块的位置. 按照“移动数据不如移动计算”的理念, Spark 在进行任务调度的时候, 会尽可能地将计算任务分配到其所要处理数据块的存储位置.

1.3 理解 RDD

一个 RDD 可以简单的理解为一个分布式的元素集合.
RDD 表示只读的分区的数据集,对 RDD 进行改动,只能通过 RDD 的转换操作, 然后得到新的 RDD, 并不会对原 RDD 有任何的影响
在 Spark 中, 所有的工作要么是创建 RDD, 要么是转换已经存在 RDD 成为新的 RDD, 要么在 RDD 上去执行一些行动操作来得到一些计算结果.
每个 RDD 被切分成多个分区(partition), 每个分区可能会在集群中不同的节点上进行计算.

1.3.1 RDD 特点

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

  2. 分区
    RDD 逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个compute函数得到每个分区的数据。
    如果 RDD 是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据
    如果 RDD 是通过其他 RDD 转换而来,则 compute函数是执行转换逻辑将其他 RDD 的数据进行转换。

  3. 只读
    RDD 是只读的,要想改变 RDD 中的数据,只能在现有 RDD 基础上创建新的 RDD。
    由一个 RDD 转换到另一个 RDD,可以通过丰富的转换算子实现,不再像 MapReduce 那样只能写map和reduce了。
    RDD 的操作算子包括两类,
    •一类叫做transformation,它是用来将 RDD 进行转化,构建 RDD 的血缘关系;
    •另一类叫做action,它是用来触发 RDD 进行计算,得到 RDD 的相关计算结果或者 保存 RDD 数据到文件系统中.

  4. 依赖(血缘)
    RDD 通过操作算子进行转换,转换得到的新 RDD 包含了从其他 RDD 衍生所必需的信息,RDD 之间维护着这种血缘关系,也称之为依赖。
    如下图所示,依赖包括两种,
    •一种是窄依赖,RDDs 之间分区是一一对应的,
    •另一种是宽依赖,下游 RDD 的每个分区与上游 RDD(也称之为父RDD)的每个分区都有关,是多对多的关系。
    在这里插入图片描述

  5. 缓存
    如果在应用程序中多次使用同一个 RDD,可以将该 RDD 缓存起来,该 RDD 只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该 RDD 的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。
    如下图所示,RDD-1 经过一系列的转换后得到 RDD-n 并保存到 hdfs,RDD-1 在这一过程中会有个中间结果,如果将其缓存到内存,那么在随后的 RDD-1 转换到 RDD-m 这一过程中,就不会计算其之前的 RDD-0 了。
    在这里插入图片描述

  6. checkpoint
    虽然 RDD 的血缘关系天然地可以实现容错,当 RDD 的某个分区数据计算失败或丢失,可以通过血缘关系重建。
    但是对于长时间迭代型应用来说,随着迭代的进行,RDDs 之间的血缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。
    为此,RDD 支持checkpoint 将数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为checkpoint 后的 RDD 不需要知道它的父 RDD 了,它可以从 checkpoint 处拿到数据。

2.RDD 编程

2.1 RDD 编程模型

在 Spark 中,RDD 被表示为对象,通过对象上的方法调用来对 RDD 进行转换。
经过一系列的transformations定义 RDD 之后,就可以调用 actions 触发 RDD 的计算
action可以是向应用程序返回结果(count, collect等),或者是向存储系统保存数据(saveAsTextFile等)。
在Spark中,只有遇到action,才会执行 RDD 的计算(即延迟计算),这样在运行时可以通过管道的方式传输多个转换。
要使用 Spark,开发者需要编写一个 Driver 程序,它被提交到集群以调度运行 Worker

在driver中对RDD编程. 

主要就是对RDD做转换和行动!

2.2 RDD 的创建

在 Spark 中创建 RDD 的方式可以分为 3 种:

  • 从集合中创建 RDD
  • 从外部存储创建 RDD
  • 从其他 RDD 转换得到新的 RDD。

1.通过一个集合(scala)来得到一个RDD,一般用于测试和学习.

val list1 = List(30, 50, 70, 60, 10, 20)
val rdd: RDD[Int] = sc.parallelize(list1) //使用parallelize函数创建

val rdd: RDD[Int] = sc.makeRDD(list1) //使用makeRDD函数创建

2.通过外部数据读取数据(文件, hive, jdbc,...), 然后得到RDD, 生产环境都是这种.

Spark 也可以从任意 Hadoop 支持的存储数据源来创建分布式数据集.
可以是本地文件系统, HDFS, Cassandra, HVase, Amazon S3 等等.
Spark 支持 文本文件, SequenceFiles, 和其他所有的 Hadoop InputFormat.

scala> var distFile = sc.textFile("words.txt")
scala> distFile.collect

3.通过另外一个RDD转换得到一个新的RDD

  • 转换研究转换算子

3 RDD 的转换(transformation)

从一个已知的 RDD 中创建出来一个新的 RDD

在 Spark 中几乎所有的transformation操作都是懒执行的(lazy),默认情况下, 每次在一个 RDD 上运行一个action的时候, 前面的每个transformed RDD 都会被重新计算.但是我们可以通过persist (or cache)方法来持久化一个 RDD 在内存中, 也可以持久化到磁盘上, 来加快访问速度

根据 RDD 中数据类型的不同, 整体分为 2 种 RDD:

  1. Value类型
  2. Key-Value类型(其实就是存一个二维的元组)

3.1 Value 类型

map和mapPartitions

  • 都是在做map操作

  • map会每个元素执行一次map中的匿名函数

  • mapPartitions每个分区执行一次,效率会高一些

    注意: mapPartitions 有内存溢出的风险.每次处理一个分区的数据,这个分区的数据处理完后,原 RDD 中该分区的数据才能释放
    如果你把迭代器转成容器式集合(List, Array)的时候, 如果这个分区的数据特别大, 则会内存溢出.
    如果没有内存溢出, 则效率要比map高.

scala的集合如何分区?

  1. 分区数如何定
  • 默认分区数: 总的核心数
  • 指定分区数
  1. 如何切片

    四个分区,左闭右开

mapPartitionsWithIndex(func)

作用: 和mapPartitions(func)类似. 但是会给func多提供一个Int值来表示分区的索引. 所以func的类型是:(Int, Iterator) => Iterator

scala> val rdd1 = sc.parallelize(Array(10,20,30,40,50,60))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24

scala> rdd1.mapPartitionsWithIndex((index, items) => items.map((index, _)))
res8: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[3] at mapPartitionsWithIndex at <console>:27

scala> res8.collect
res9: Array[(Int, Int)] = Array((0,10), (0,20), (0,30), (1,40), (1,50), (1,60))

flatMap(func)

作用: 类似于map,但是每一个输入元素可以被映射为 0 或多个输出元素(所以func应该返回一个序列,而不是单一元素 T => TraversableOnce[U])
在这里插入图片描述
案例:
创建一个元素为 1-5 的RDD,运用 flatMap创建一个新的 RDD,新的 RDD 为原 RDD 每个元素的 平方和三次方 来组成 1,1,4,8,9,27…

scala> val rdd1 = sc.parallelize(Array(1,2,3,4,5))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[5] at parallelize at <console>:24

scala> rdd1.flatMap(x => Array(x * x, x * x * x))
res13: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[6] at flatMap at <console>:27

scala> res13.collect
res14: Array[Int] = Array(1, 1, 4, 8, 9, 27, 16, 64, 25, 125)

glom

把每个分区的数据放入到一个数组中.如果有n个分区, 得到的新的RDD中就有n个数组

val conf: SparkConf = new SparkConf().setAppName("Glom").setMaster("local[2]")
        val sc: SparkContext = new SparkContext(conf)
        val list1 = List(30, 50, 70, 60, 10, 20)
        val rdd1: RDD[Int] = sc.parallelize(list1, 4) //4个分区
        val rdd2 = rdd1.glom()
        rdd2.collect.map(_.toList).foreach(println)
        sc.stop()

// 
List(30)
List(50, 70)
List(60)
List(10, 20)

groupBy

需要shuffle, 因为这个算子会产生宽依赖.

shuffle需要借助于磁盘, 所以效率比较低. 以后慎用.

在分组的时候, 分完组之后, rddkv形式的, 所以, 需要重新分区.

默认的分区器是哈希分区器!!!

val conf: SparkConf = new SparkConf().setAppName("GroupBy").setMaster("local[2]")
        val sc: SparkContext = new SparkContext(conf)
        val list1 = List(3, 50, 7, 6, 1, 20)
        val rdd1: RDD[Int] = sc.parallelize(list1, 2)
        
        val rdd2: RDD[(Int, Iterable[Int])] = rdd1.groupBy(x => x % 2)
        // 计算这个集合中, 所有的奇数的和偶数的和
        //        val rdd3 = rdd2.map(kv => (kv._1, kv._2.sum))
        val rdd3 = rdd2.map {
            case (jo, it) => (jo, it.sum)
        }
        rdd3.collect.foreach(println)
        sc.stop()
//
(0,76)
(1,11)

filter(func)

作用: 过滤. 返回一个新的 RDD 是由func的返回值为true的那些元素组成

案例
创建一个 RDD(由字符串组成),过滤出一个新 RDD(包含“xiao”子串)

scala> val names = sc.parallelize(Array("xiaoli", "laoli", "laowang", "xiaocang", "xiaojing", "xiaokong"))
names: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[0] at parallelize at <console>:24
    
scala> names.filter(_.contains("xiao"))
res3: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[1] at filter at <console>:27

scala> res3.collect
res4: Array[String] = Array(xiaoli, xiaocang, xiaojing, xiaokong)

sample(withReplacement, fraction, seed)

数据的抽样
作用:

  1. 以指定的随机种子随机抽样出比例为fraction的数据,(抽取到的数量是: size * fraction). 需要注意的是得到的结果并不能保证准确的比例.
  2. withReplacement表示是抽出的数据是否放回,true为有放回的抽样,false为无放回的抽样. 放回表示数据有可能会被重复抽取到, false 则不可能重复抽取到. 如果是false, 则fraction必须是:[0,1], 是 true 则大于等于0就可以了.
  3. seed用于指定随机数生成器种子。 一般用默认的, 或者传入当前的时间戳

不放回抽样:

scala> val rdd1 = sc.parallelize(1 to 10)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[15] at parallelize at <console>:24

scala> rdd1.sample(false, 0.5).collect
res15: Array[Int] = Array(1, 3, 4, 7)

放回抽样

scala> rdd1.sample(true, 2).collect
res25: Array[Int] = Array(1, 1, 2, 3, 3, 4, 4, 5, 5, 5, 5, 5, 6, 6, 7, 7, 8, 8, 9)

distinct([numTasks]))

作用:
对 RDD 中元素执行去重操作. 参数表示任务的数量.默认值和分区数保持一致.

scala> val rdd1 = sc.parallelize(Array(10,10,2,5,3,5,3,6,9,1))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[28] at parallelize at <console>:24
    
scala> rdd1.distinct().collect
res29: Array[Int] = Array(6, 10, 2, 1, 3, 9, 5)

coalesce和repartition

用来改变RDD分区数.
coalesce 只能减少分区,不能增加分区. 为啥? 因为coalesce默认是不shuffle
如果启动shuffle, 也可以增加分区.

以后, 如果减少分区, 尽量不要shuffle, 只有增加的分区的时候才shuffle

实际应用:
如果减少分区就使用 coalesce,第二个参数表示是否shuffle, 如果不传或者传入的为false, 则表示不进行shuffer, 此时分区数减少有效, 增加分区数无效
如果是增加分区就是用 repartition

  val conf: SparkConf = new SparkConf().setAppName("Coalasce").setMaster("local[2]")
        val sc: SparkContext = new SparkContext(conf)
        val list1 = List(30, 50, 70, 60, 10, 20)
        val rdd1: RDD[Int] = sc.parallelize(list1, 4)
//        val rdd2: RDD[Int] = rdd1.coalesce(3, true)
        val rdd2 = rdd1.repartition(5)
        rdd2.collect
        println("------")
        println(rdd1.getNumPartitions)
        println(rdd2.getNumPartitions)
        
        sc.stop()
//
4
5

sortBy

整个RDD进行的全局排序
参数1: 排序指标
参数2:是否升序(默认true)
参数3:排序后新的RDD的分区数.默认和排序前的rdd的分区数一致

val conf: SparkConf = new SparkConf().setAppName("SortBy").setMaster("local[2]")
        val sc: SparkContext = new SparkContext(conf)
        // val list1 = List(30, 50, 70, 60, 10, 20)
        val list1 = List("aa", "cc", "abc", "hello", "b", "a", "c")
        val rdd1 = sc.parallelize(list1, 3)
        
        // 长度升, 相等后之后再按照字母表降
        val rdd2 = rdd1.sortBy(x => (x.length, x), true)(
            Ordering.Tuple2(Ordering.Int, Ordering.String.reverse), ClassTag(classOf[(Int, String)]))
        println(rdd2.getNumPartitions)
        rdd2.collect.foreach(println)
        sc.stop()                
//
3
c
b
a
cc
aa
abc
hello

pipe

给我们一个机会, 让linux命令或者脚本去处理RDD中数据

脚本执行情况:

   rdd1.pipe("p1.sh")
每个分区执行一次这个脚本!!!

3.2 双 Value 类型交互

这里的“双 Value 类型交互”是指的两个 RDD[V] 进行交互.

val conf: SparkConf = new SparkConf().setAppName("DoubleVlaue1").setMaster("local[2]")
        val sc: SparkContext = new SparkContext(conf)        
        
        // 并集(分区数相加)
        //        val rdd3: RDD[Int] = rdd1 ++ rdd2
        //        val rdd3: RDD[Int] = rdd1.union(rdd2)
        
        //交集(默认情况: 分区数和前面的rdd中最大那个相等) 去重
        //                val rdd3 = rdd1.intersection(rdd2)
        //                println(rdd3.getNumPartitions)
        // 差集()
        //        val rdd3 = rdd1.subtract(rdd2)
        // 笛卡尔积  一般很少使用.
        //        val rdd3 = rdd1.cartesian(rdd2)

        val list1 = List(30, 50, 70, 60)
        val list2 = List(3, 5, 7, 2, 4, 1)
        val rdd1: RDD[Int] = sc.parallelize(list1, 2)
        val rdd2: RDD[Int] = sc.parallelize(list2, 2)
        // zip:拉链,两个集合元素数量必须相同,否则抛异常
        zip 站在元素的角度来拉:
   		 1. 两个RDD的分区数必须相同
   		 2. 对应的分区必须拥有相同的元素数(总的元素数相同)
               // val rdd3 = rdd1.zip(rdd2)
                val rdd3 = rdd1.zipPartitions(rdd2)((it1, it2) => {
                 it1.zip(it2)  // 是scala的集合的zip
                  it1.zipAll(it2, 100, -1) //it1短用100填补,it2短用-1填补
                })
       // val rdd3: RDD[(Int, Long)] = rdd1.zipWithIndex() //给自己加索引

        rdd3.collect.foreach(println)
        sc.stop()

3.3 kv形式的RDD

如果RDD中存储的是二维元组(key, value), 就是KV形式的RDD

提供了很多算子..ByKey

1. partitionBy

abstract class Partitioner extends Serializable {
  //返回分完区之后的新的RDD的分区数
  def numPartitions: Int
  // 每个键值对如何分区
  // 是由key, 和value没有任何关系
    // 返回的是分区索引
  def getPartition(key: Any): Int
}

注意: 分区的时候, 是根据key来选择分区, 和value没有任何关系

在分区的时候, 如何根据value进行分区?

交换kv

  • 只有kv形式的RDD才能使用分区器进行分区.
  • 使用分区器进行分区的时候, 一般会进行shuffle
  • RDD1[(k, v)] 分区器是 P1, 对RDD1进行重新分区, 使用的分区器和p1相等, 那么这个时候, 不会真正的分区 .

2. 聚合算子

所有聚合类的算子都有预聚合

reduceByKey(使用最多)
聚合算子:
只能用在kv形式的聚合.
按照key进行聚合, 对相同的key的value进行聚合

foldByKey:(很鸡肋)
多一个zero
1. zero的类型必须是v的类型一致
2. zero只在分区内聚合(预聚合, map端)的时候参与运算.
分区间聚合(最终聚合, reduce端)不参与
3. 对一个key, zero最多参与n次 (n是分区数)

foldByKey reduceByKey 共同点:

他们在分区内聚合和分区间的逻辑是一样.

aggregateByKey:(次之)
分区内聚合和分区间的聚合不一样

combineByKey:
combineByKey[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C
createCombiner: 在每个分区内,不同的key来说, 都会执行一次这个方法, 返回一个值, 相当于以前的zero
mergeValue: 分区内聚合
mergeCombiners:分区间的聚合

combineByKey
    combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners)(null)

aggregateByKey
    combineByKeyWithClassTag[U]((v: V) => cleanedSeqOp(createZero(), v),
          cleanedSeqOp, combOp, partitioner)

foldByKey
    combineByKeyWithClassTag[V]((v: V) => cleanedFunc(createZero(), v),
          cleanedFunc, cleanedFunc, partitioner)

reduceByKey
    combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)

使用指导:
    1. 如果分区内和分区间的聚合逻辑不一样, 用 aggregateByKey
    2. 如果分区内和分区间逻辑一样  reduceByKey

3. reduceByKey和groupByKey的区别

  - 如果是聚合应用使用reduceByKey, 因为他有预聚合, 可以提高性能
  - 如果分组的目的不是为了聚合, 这个时候就应该使用groupByKey
  - 如果分组的目的是为了聚合, 则不要使用groupByKey, 因为他没有预聚合.

3.4 排序

  ```
  sortBy	这个使用更广泛, 可以用在任意的RDD上. 用的更多些
  	
  sortByKey 这个只能用在kv上, 按照k进行排序
  ```

3.5 join

  其实就是`sql`中的连接

  - `sql`

    - 内连接

      `on a.id=b.id`

    - 左外

    - 右外

    - 全外(`hive`支持, `mysql`不支持)

  - `spark的rdd中`

    都支持.

    只能用于`kv`形式的`RDD`

    `k相等的连在一起`
 val conf: SparkConf = new SparkConf().setAppName("Join").setMaster("local[2]")
        val sc: SparkContext = new SparkContext(conf)
        var rdd1 = sc.parallelize(Array((1, "a"), (1, "b"), (2, "c"), (4, "d")))
        var rdd2 = sc.parallelize(Array((1, "aa"), (3, "bb"), (2, "cc"), (2, "dd")))
        // 内连接
        //        val rdd3: RDD[(Int, (String, String))] = rdd1.join(rdd2)
        // 左外连接 左边都有, 右边没有的用 None 来替换
        //        val rdd3 = rdd1.leftOuterJoin(rdd2)
        // 右外连接 右边都有, 左边没有的用 None
        //        val rdd3 = rdd1.rightOuterJoin(rdd2)
        val rdd3 = rdd1.fullOuterJoin(rdd2)
    
        rdd3.collect.foreach(println)
        sc.stop()

3.6 cogroup(otherDataset, [numTasks])

作用:在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD

scala> val rdd1 = sc.parallelize(Array((1, 10),(2, 20),(1, 100),(3, 30)),1)
rdd1: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[23] at parallelize at <console>:24

scala> val rdd2 = sc.parallelize(Array((1, "a"),(2, "b"),(1, "aa"),(3, "c")),1)
rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[24] at parallelize at <console>:24

scala> rdd1.cogroup(rdd2).collect
res9: Array[(Int, (Iterable[Int], Iterable[String]))] = Array((1,(CompactBuffer(10, 100),CompactBuffer(a, aa))), (3,(CompactBuffer(30),CompactBuffer(c))), (2,(CompactBuffer(20),CompactBuffer(b))))

3.7 案例演示

/*
需求

  1. 数据结构:时间戳,省份,城市,用户,广告,字段使用空格分割。
    1516609143867 6 7 64 16
    1516609143869 9 4 75 18
    1516609143869 1 7 87 12
  2. 需求: 统计出每一个省份广告被点击次数的 TOP3

倒推法来分析:
=> 元数据做map
=> RDD((pro, ads), 1) reduceByKey
=> RDD((pro, ads), count) map
=> RDD(pro -> (ads, count), …) groupByKey
=> RDD( pro1-> List(ads1->100, abs2->800, abs3->600, …), pro2 -> List(…) ) map: 排序,前3
=> RDD( pro1-> List(ads1->100, abs2->800, abs3->600), pro2 -> List(…) )

*/

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

/**
 * Author jaffe
 * Date 2020/5/6 16:07
 */
object RDDPractice {
    def main(args: Array[String]): Unit = {
        /*
        => 元数据做map
        => RDD((pro, ads), 1)  reduceByKey
        => RDD((pro, ads), count)   map
        => RDD(pro -> (ads, count), ....)      groupByKey
        => RDD( pro1-> List(ads1->100, abs2->800, abs3->600, ....),  pro2 -> List(...) )  map: 排序,前3
        => RDD( pro1-> List(ads1->100, abs2->800, abs3->600),  pro2 -> List(...) )
         */
        val conf: SparkConf = new SparkConf().setAppName("RDDPractice").setMaster("local[2]")
        val sc: SparkContext = new SparkContext(conf)
        // 1. 读取原始数据
        val lineRDD: RDD[String] = sc.textFile("c:/agent.log")
        // 2. 调整成需要格式  ((pro, ads), 1)
        val proAdsAndOneRDD = lineRDD.map(line => {
            val split = line.split(" ")
            ((split(1), split(4)), 1)
        })
        // 3. RDD((pro, ads), count)
        val proAdsAndCountRDD = proAdsAndOneRDD.reduceByKey(_ + _)
        // 4. RDD(pro -> (ads, count), ....)
        val proAndAdsCountRDD = proAdsAndCountRDD.map {
            case ((pro, ads), count) => (pro, (ads, count))
        }
        // 5. RDD( pro1-> List(ads1->100, abs2->800, abs3->600, ....),  pro2 -> List(...) )  map: 排序,前3
        val resultRDD = proAndAdsCountRDD
            .groupByKey()
            .map {
                case (pro, adsCountIt: Iterable[(String, Int)]) =>
                    (pro, adsCountIt.toList.sortBy(-_._2).take(3))
            }
            //            .sortByKey()
            .sortBy(_._1.toInt)
        resultRDD.collect.foreach(println)
        sc.stop()
               
    }
}

4 RDD的 Action 操作

4.1 reduce(func)

通过func函数聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据。
scala> val rdd1 = sc.parallelize(1 to 100)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :24

scala> rdd1.reduce(_ + _)
res0: Int = 5050

scala> val rdd2 = sc.parallelize(Array((“a”, 1), (“b”, 2), (“c”, 3)))
rdd2: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[1] at parallelize at :24

scala> rdd2.reduce((x, y) => (x._1 + y._1, x._2 + y._2))
res2: (String, Int) = (abc,6)

4.2 collect

以数组的形式返回 RDD 中的所有元素.
所有的数据都会被拉到 driver 端, 所以要慎用

4.3 count()

返回 RDD 中元素的个数.

4.4 take(n)

返回 RDD 中前 n 个元素组成的数组.
take 的数据也会拉到 driver 端, 应该只对小数据集使用
scala> val rdd1 = sc.makeRDD(Array(10, 20, 30, 50, 60))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at makeRDD at :24

scala> rdd1.take(2)
res3: Array[Int] = Array(10, 20)

4.5 first

返回 RDD 中的第一个元素. 类似于take(1).

4.6 takeOrdered(n, [ordering])

返回排序后的前 n 个元素, 默认是升序排列.
数据也会拉到 driver 端
scala> val rdd1 = sc.makeRDD(Array(100, 20, 130, 500, 60))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[4] at makeRDD at :24

scala> rdd1.takeOrdered(2)
res6: Array[Int] = Array(20, 60)

scala> rdd1.takeOrdered(2)(Ordering.Int.reverse)
res7: Array[Int] = Array(500, 130)

4.7 aggregate

def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U
aggregate函数将每个分区里面的元素通过seqOp和初始值进行聚合,然后用combine函数将每个分区的结果和初始值(zeroValue)进行combine操作。这个函数最终返回的类型不需要和RDD中元素类型一致
注意:
zeroValue 分区内聚合和分区间聚合的时候各会使用一次.

scala> val rdd1 = sc.makeRDD(Array(100, 30, 10, 30, 1, 50, 1, 60, 1), 2)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[8] at makeRDD at <console>:24

scala> rdd1.aggregate(0)(_ + _, _ + _)
res12: Int = 283

scala> val rdd1 = sc.makeRDD(Array("a", "b", "c", "d"), 2)
rdd1: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[9] at makeRDD at <console>:24

scala> rdd1.aggregate("x")(_ + _, _ + _)
res13: String = xxabxcd

4.8 fold

折叠操作,aggregate的简化操作,seqop和combop一样的时候,可以使用fold

scala> val rdd1 = sc.makeRDD(Array(100, 30, 10, 30, 1, 50, 1, 60, 1), 2)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[10] at makeRDD at <console>:24

scala> rdd1.fold(0)(_ + _)
res16: Int = 283

scala> val rdd1 = sc.makeRDD(Array("a", "b", "c", "d"), 2)
rdd1: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[11] at makeRDD at <console>:24

scala> rdd1.fold("x")(_ + _)
res17: String = xxabxcd

4.9 saveAsTextFile(path)

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

4.10 saveAsSequenceFile(path)

作用:将数据集中的元素以 Hadoop sequencefile 的格式保存到指定的目录下,可以使 HDFS 或者其他 Hadoop 支持的文件系统。

4.11 saveAsObjectFile(path)

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

4.12 countByKey()

作用:针对(K,V)类型的 RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。
应用: 可以用来查看数据是否倾斜

scala> val rdd1 = sc.parallelize(Array(("a", 10), ("a", 20), ("b", 100), ("c", 200)))
rdd1: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[15] at parallelize at <console>:24

scala> rdd1.countByKey()
res19: scala.collection.Map[String,Long] = Map(b -> 1, a -> 2, c -> 1)

4.13 foreach(func)

作用: 针对 RDD 中的每个元素都执行一次func
每个函数是在 Executor 上执行的, 不是在 driver 端执行的.

foreach
    是遍历RDD中的每个元素
    
将来我计算后的数据, 有的时候会存储到外部存储, 比如mysql
可以用来与外部存储进行通信. 把数据写入到外部存储 . 比如mysql

5.RDD 中函数的传递

Spark 进行编程的时候, 初始化工作是在 driver端完成的, 而实际的运行程序是在executor端进行的. 所以就涉及到了进程间的通讯, 数据是需要序列化的.

5.1 传递函数

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

object SerDemo {
    def main(args: Array[String]): Unit = {
        val conf: SparkConf = new SparkConf().setAppName("SerDemo").setMaster("local[*]")
        val sc = new SparkContext(conf)
        val rdd: RDD[String] = sc.parallelize(Array("hello world", "hello jaffe", "jaffe", "hahah"), 2)
        val searcher = new Searcher("hello")
        val result: RDD[String] = searcher.getMatchedRDD1(rdd)
        result.collect.foreach(println)
    }
}
//需求: 在 RDD 中查找出来包含 query 子字符串的元素

// query 为需要查找的子字符串
class Searcher(val query: String){
    // 判断 s 中是否包括子字符串 query
    def isMatch(s : String) ={
        s.contains(query)
    }
    // 过滤出包含 query字符串的字符串组成的新的 RDD
    def getMatchedRDD1(rdd: RDD[String]) ={
        rdd.filter(isMatch)  //
    }
    // 过滤出包含 query字符串的字符串组成的新的 RDD
    def getMatchedRDD2(rdd: RDD[String]) ={
        rdd.filter(_.contains(query))
    }
}

说明:
直接运行程序会发现报错: 没有初始化. 因为rdd.filter(isMatch) 用到了对象this的方法isMatch, 所以对象this需要序列化,才能把对象从driver发送到executor.
在这里插入图片描述
解决方案: 让 Searcher 类实现序列化接口:Serializable

在这里插入图片描述

5.2 传递变量

object SerDemo {
    def main(args: Array[String]): Unit = {
        val conf: SparkConf = new SparkConf().setAppName("SerDemo").setMaster("local[*]")
        val sc = new SparkContext(conf)
        val rdd: RDD[String] = sc.parallelize(Array("hello world", "hello jaffe", "jaffe", "hahah"), 2)
        val searcher = new Searcher("hello")
        val result: RDD[String] = searcher.getMatchedRDD2(rdd)
        result.collect.foreach(println)
    }
}

说明:
传递了一个属性过去. 仍然会报错没有序列化. 因为this仍然没有序列化.

解决方案有 2 种:

  1. 让类实现序列化接口:Serializable
  2. 传递局部变量而不是属性.
    在这里插入图片描述

5.3 kryo 序列化框架

Java 的序列化比较重, 能够序列化任何的类. 比较灵活,但是相当的慢, 并且序列化后对象的提交也比较大.
Spark 出于性能的考虑, 支持另外一种序列化机制: kryo (2.0开始支持). kryo 比较快和简洁.(速度是Serializable的10倍).
从2.0开始, Spark 内部已经在使用 kryo 序列化机制: 当 RDD 在 Shuffle数据的时候, 简单数据类型, 简单数据类型的数组和字符串类型已经在使用 kryo 来序列化.
有一点需要注意的是: 即使使用 kryo 序列化, 也要继承 Serializable 接口.

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

object SerDemo2 {
    def main(args: Array[String]): Unit = {
        val conf: SparkConf = new SparkConf()
            .setAppName("SerDemo")
            .setMaster("local[*]")
            // (可以省)更好序列器  set("spark.serializer", classOf[KryoSerializer].getName)
            .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
            // 注册需要序列化的类
            .registerKryoClasses(Array(classOf[Searcher2]))
       
        val sc = new SparkContext(conf)
        
        
        val rdd: RDD[String] = sc.parallelize(Array("hello world", "hello atguigu", "atguigu", "hahah"), 2)
        // 1. 创建对象: 在driver
        val searcher = new Searcher2("hello") // 查找包含hello子字符串的元素
        // 2. 调用对象的方法: 在driver
        val result: RDD[String] = searcher.getMatchedRDD1(rdd)
        
        result.collect.foreach(println)
        result.collect.foreach(println)
        result.count()
        Thread.sleep(1000000)
       
    }
    
}

//需求: 在 RDD 中查找出来包含 query 子字符串的元素

// query 为需要查找的子字符串
case class Searcher2(val query: String)  {
    // 判断 s 中是否包括子字符串 query
    def isMatch(s: String): Boolean = {
        s.contains(query)
    }
    
    // 过滤出包含 query字符串的字符串组成的新的 RDD
    def getMatchedRDD1(rdd: RDD[String]) = {
        // 调用filter: 在driver
        // 传递进去了一个函数: isMatch, isMatch在什么地方执行: executor上执行
        rdd.filter(isMatch) //
    }
    
    def getMatchedRDD2(rdd: RDD[String]) = {
        // query是类的属性, 则对象也要序列化过去. 所以, 类也要实现序列化接口
        rdd.filter(x => x.contains(query)) //
    }
    
    def getMatchedRDD3(rdd: RDD[String]) = {
        // 其实是一个局部变量, 和当前这个对象有关系吗? 没有
        val q = query
        rdd.filter(x => x.contains(q)) //
    }    
}

6. Spark Job 的划分

由于 Spark 的懒执行, 在驱动程序调用一个action之前, Spark 应用不会做任何事情.

  • application
    应用. 创建一个SparkContext可以认为创建了一个Application.

  • job
    在一个application中, 每执行一次行动算子, 就会创建一个job

  • stage
    阶段. 默认会有一个stage, 再每碰到一个shuffle算子, 会产生一个新的stage
    一个job中,可以包含多个stage

  • task
    任务. 表示咱们阶段执行的时候的并行度.
    假设一个RDD 有100个分区. 处理的时候,每个分区的数据, 分配一个task来计算
    一个阶段会有多个task.
    分区, 是站数据的存储角度
    task, 是站计算的角度
    分区数和task是相等.

  • 总结:
    application
    多个job
    多个stage
    多个task

在这里插入图片描述

  • DAG(Directed Acyclic Graph) 有向无环图
    Spark 的顶层调度层使用 RDD 的依赖为每个 job 创建一个由 stages 组成的 DAG(有向无环图). 在 Spark API 中, 这被称作 DAG 调度器(DAG Scheduler).
    有些错误, 比如: 连接集群的错误, 配置参数错误, 启动一个 Spark job 的错误, 这些错误必须处理, 并且都表现为 DAG Scheduler 错误. 这是因为一个 Spark job 的执行是被 DAG 来处理.
    DAG 为每个 job 构建一个 stages 组成的图表, 从而确定运行每个 task 的位置, 然后传递这些信息给 TaskSheduler. TaskSheduler 负责在集群中运行任务.

7.持久化和设置检查点

7.1 RDD 数据的持久化

每碰到一个 Action 就会产生一个 job, 每个 job 开始计算的时候总是从这个 job 最开始的 RDD 开始计算.
在有些情况下是没有必要,可以持久化数据集在内存中. 当我们持久化一个 RDD 时, 每个节点都会存储他在内存中计算的那些分区, 然后在其他的 action 中可以重用这些数据. 这个特性会让将来的 action 计算起来更快(通常块 10 倍). 对于迭代算法和快速交互式查询来说, 缓存(Caching)是一个关键工具.

  1. 使用方式
    rdd.persist(存储级别)
    rdd.cache
  2. 不会重新起job来专门的持久化. 而是使用上一个job的结果来进行持久化
  3. 血缘还在. rdd2 的血缘关系还在. 一旦持久化的出现问题, 可以通过血缘关系重建rdd
import org.apache.spark.{SparkConf, SparkContext}

/**
 * Author jaffe
 * Date 2020/5/8 14:54
 */
object CacheDemo {
    def main(args: Array[String]): Unit = {
        val conf: SparkConf = new SparkConf().setAppName("CacheDemo").setMaster("local[2]")
        val sc: SparkContext = new SparkContext(conf)
        val list1 = List("hello world", "hello")
        val rdd1 = sc.parallelize(list1, 2)
        
        val rdd2 = rdd1
            .flatMap(x => {
                println("flatMap ..." + x) // 2次
                x.split(" ")
            })
            .map(x => {
                println("map ..." + x) // 3次
                (x, 1)
            })
        // 对那些需要重复使用RDD做持久化:
        //        rdd2.persist()  // rdd2经过第一个行动算子之后, 会刚刚计算的结果默认缓存到内存
        // 一般只持久化到内存!!!
        //        rdd2.persist(StorageLevel.DISK_ONLY)
        rdd2.cache() // 缓存到内存
        rdd2.collect
        
        println("-------------")
        rdd2.collect
        println("-------------")
        rdd2.countByKey()
        
        Thread.sleep(100000)
        sc.stop()
        
    }
}

7.2 设置检查点(checkepoint)

检查点(本质是通过将RDD写入Disk做检查点)是为了通过 Lineage(血缘) 做容错的辅助
Lineage 过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的 RDD 开始重做 Lineage,就会减少开销。
检查点通过将数据写入到 HDFS 文件系统实现了 RDD 的检查点功能。
为当前 RDD 设置检查点。该函数将会创建一个二进制的文件,并存储到 checkpoint 目录中,该目录是用 SparkContext.setCheckpointDir()设置的。在 checkpoint 的过程中,该RDD 的所有依赖于父 RDD中 的信息将全部被移除。
对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发, 在触发的时候需要对这个 RDD 重新计算.

  1. 使用方式
    sc.setCheckpointDir("./ck1")
    rdd.checkpoint()
  2. checkpoint的时候, 会重新启动一个新的job来专门的做checkpoint
  3. checkpoint会切断 rdd 的血缘关系.
import org.apache.spark.{SparkConf, SparkContext}

/**
 * Author atguigu
 * Date 2020/5/8 15:36
 */
object CheckPointDemo1 {
    def main(args: Array[String]): Unit = {
        val conf: SparkConf = new SparkConf().setAppName("CacheDemo").setMaster("local[2]")
        val sc: SparkContext = new SparkContext(conf)
        sc.setCheckpointDir("./ck1")
        val list1 = List("hello world", "hello")
        val rdd1 = sc.parallelize(list1, 2)
    
        val rdd2 = rdd1
            .flatMap(x => {
                println("flatMap ..." + x) // 2次
                x.split(" ")
            })
            .map(x => {
                println("map ..." + x) // 3次
                (x, 1)
            })
        
        rdd2.checkpoint()  // 仅仅是一个计划
        rdd2.cache()
        rdd2.collect
        println("-------------")
        rdd2.count()
        println("-------------")
        rdd2.collect
    
        Thread.sleep(100000)
        sc.stop()
    }
}
/*
checkpoint:
    如果rdd做checkpoint, 不会使用上个job结果.
    spark会自动启动一个新的job, 专门的去checkpoint
 */

7.3 持久化和checkpoint的区别

  1. 持久化只是将数据保存在 BlockManager 中,而 RDD 的 Lineage 是不变的。但是checkpoint 执行完后,RDD 已经没有之前所谓的依赖 RDD 了,而只有一个强行为其设置的checkpointRDD,RDD 的 Lineage 改变了。
  2. 持久化的数据丢失可能性更大,磁盘、内存都可能会存在数据丢失的情况。但是 checkpoint 的数据通常是存储在如 HDFS 等容错、高可用的文件系统,数据丢失可能性较小。
  3. 注意: 默认情况下,如果某个 RDD 没有持久化,但是设置了checkpoint,会存在问题. 本来这个 job 都执行结束了,但是由于中间 RDD 没有持久化,checkpoint job 想要将 RDD 的数据写入外部文件系统的话,需要全部重新计算一次,再将计算出来的 RDD 数据 checkpoint到外部文件系统。 所以,建议对 checkpoint()的 RDD 使用持久化, 这样 RDD 只需要计算一次就可以了.

8.Key-Value 类型 RDD 的数据分区器

分区器是用来在rdd shuffle之后那些kv值应该进入哪个分区来服务的
对于只存储 value的 RDD, 不需要分区器.
只有存储Key-Value类型的才会需要分区器.
Spark 目前支持 Hash 分区和 Range 分区,用户也可以自定义分区.
Hash 分区为当前的默认分区
Spark 中分区器直接决定了 RDD 中分区的个数、RDD 中每条数据经过 Shuffle 过程后属于哪个分区和 Reduce 的个数.

8.1 HashPartitioner

HashPartitioner分区的原理:对于给定的key,计算其hashCode,并除以分区的个数取余
分区弊端: 可能导致每个分区中数据量的不均匀,极端情况下会导致某些分区拥有 RDD 的全部数据

(10, v) 20.. 30 10 50 100
new HashPartioner(2)
全部进入0分区,严重的数据倾斜

8.2 RangePartitioner

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

  • 第一步:先从整个 RDD 中抽取出样本数据,将样本数据排序,计算出每个分区的最大 key 值,形成一个Array[KEY]类型的数组变量 rangeBounds;(边界数组).
  • 第二步:判断key在rangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标;该分区器要求 RDD 中的 KEY 类型必须是可以排序的.
    比如[1,100,200,300,400],然后对比传进来的key,返回对应的分区id

8.3 自定义分区器

要实现自定义的分区器,需要继承 org.apache.spark.Partitioner, 并且需要实现下面的方法:

  1. numPartitions
    该方法需要返回分区数, 必须要大于0.
  2. getPartition(key)
    返回指定键的分区编号(0到numPartitions-1)。
  3. equals
    Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个 RDD 的分区方式是否相同
  4. hashCode
    如果你覆写了equals, 则也应该覆写这个方法
import org.apache.spark.{Partitioner, SparkConf, SparkContext}

/**
 * Author jaffe
 * Date 2020/5/8 16:21
 */
object MyPartitionerDemo {
    def main(args: Array[String]): Unit = {
        val conf: SparkConf = new SparkConf().setAppName("MyPartitionerDemo").setMaster("local[2]")
        val sc: SparkContext = new SparkContext(conf)
        val list1 = List(30, 50, 7, 60, 1, 20, null, null)
        val rdd1 = sc.parallelize(list1, 4).map((_, 1))
        val rdd2 = rdd1.partitionBy(new MyPartitioner(2))
        
        val rdd3 = rdd2.reduceByKey(new MyPartitioner(2), _ + _)
       
        rdd3.glom().map(_.toList).collect.foreach(println)
        
        Thread.sleep(100000000)
        sc.stop()
        
    }
}

class MyPartitioner(val num: Int) extends Partitioner {
    // 返回分完区之后的分区数
    override def numPartitions: Int = num
    
    // 写一个hash分区器
    override def getPartition(key: Any): Int = {
        key match {
            case null => 0
            case _ => key.hashCode().abs % numPartitions
        }
    }
    
   override def hashCode(): Int = num
    
    override def equals(obj: Any): Boolean = {
        obj match {
            case null => false
            case o: MyPartitioner => num == o.num
            case _ => false
        }
    }
}

9.读写文件

从文件中读取数据是创建 RDD 的一种方式.
把数据保存到文件中的操作是一种 Action.
Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。
文件格式分为:Text文件、Json文件、Csv文件、Sequence文件以及Object文件;
文件系统分为:本地文件系统、HDFS、Hbase 以及 数据库。
平时用的比较多的就是: 从 HDFS 读取和保存 Text 文件.

9.1 读写 Text 文件

// 读取本地文件
scala> val rdd1 = sc.textFile("./words.txt")
rdd1: org.apache.spark.rdd.RDD[String] = ./words.txt MapPartitionsRDD[5] at textFile at <console>:24

scala> val rdd2 = rdd1.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ +_)
rdd2: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[8] at reduceByKey at <console>:26
// 保存数据到 hdfs 上.        
scala> rdd2.saveAsTextFile("hdfs://hadoop103:9000/words_output")

9.2 读取 Json 文件

如果 JSON 文件中每一行就是一个 JSON 记录,那么可以通过将 JSON 文件当做文本文件来读取,然后利用相关的 JSON 库对每一条数据进行 JSON 解析。
注意:使用 RDD 读取 JSON 文件处理很复杂,SparkSQL 集成了很好的处理 JSON 文件的方式,所以实际应用中多是采用SparkSQL处理JSON文件。

// 读取 json 数据的文件, 每行是一个 json 对象
scala> val rdd1 = sc.textFile("/opt/module/spark-local/examples/src/main/resources/people.json")
rdd1: org.apache.spark.rdd.RDD[String] = /opt/module/spark-local/examples/src/main/resources/people.json MapPartitionsRDD[11] at textFile at <console>:24

// 导入 scala 提供的可以解析 json 的工具类
scala> import scala.util.parsing.json.JSON
import scala.util.parsing.json.JSON

// 使用 map 来解析 Json, 需要传入 JSON.parseFull
scala> val rdd2 = rdd1.map(JSON.parseFull)
rdd2: org.apache.spark.rdd.RDD[Option[Any]] = MapPartitionsRDD[12] at map at <console>:27

// 解析到的结果其实就是 Option 组成的数组, Option 存储的就是 Map 对象
scala> rdd2.collect
res2: Array[Option[Any]] = Array(Some(Map(name -> Michael)), Some(Map(name -> Andy, age -> 30.0)), Some(Map(name -> Justin, age -> 19.0)))

9.3 读写 SequenceFile 文件

SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件(Flat File)。

Spark 有专门用来读取 SequenceFile 的接口。在 SparkContext 中,可以调用 sequenceFile [ keyClass, valueClass](path)
注意:SequenceFile 文件只针对 PairRDD

  1. 先保存一个 SequenceFile 文件
scala> val rdd1 = sc.parallelize(Array(("a", 1),("b", 2),("c", 3)))
rdd1: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[13] at parallelize at <console>:25

scala> rdd1.saveAsSequenceFile("hdfs://hadoop103:9000/seqFiles")
  1. 读取 SequenceFile 文件
scala> val rdd1 = sc.sequenceFile[String, Int]("hdfs://hadoop103:9000/seqFiles")
rdd1: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[18] at sequenceFile at <console>:25

scala> rdd1.collect
res4: Array[(String, Int)] = Array((a,1), (b,2), (c,3))
注意: 需要指定k和v泛型的类型 sc.sequenceFile[String, Int]

9.4 读写 objectFile 文件

对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。
可以通过objectFile[k,v](path) 函数接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调用saveAsObjectFile() 实现对对象文件的输出

  1. 把 RDD 保存为objectFile
scala> val rdd1 = sc.parallelize(Array(("a", 1),("b", 2),("c", 3)))
rdd1: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[19] at parallelize at <console>:25

scala> rdd1.saveAsObjectFile("hdfs://hadoop103:9000/obj_file")
  1. 读取 objectFile
scala> val rdd1 = sc.objectFile[(String, Int)]("hdfs://hadoop103:9000/obj_file")
rdd1: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[25] at objectFile at <console>:25

scala> rdd1.collect
res8: Array[(String, Int)] = Array((a,1), (b,2), (c,3))

9.5 从 HDFS 读写文件

Spark 的整个生态系统与 Hadoop 完全兼容的,所以对于 Hadoop 所支持的文件类型或者数据库类型,Spark 也同样支持.

另外,由于 Hadoop 的 API 有新旧两个版本,所以 Spark 为了能够兼容 Hadoop 所有的版本,也提供了两套创建操作接口.

对于外部存储创建操作而言,HadoopRDD 和 newHadoopRDD 是最为抽象的两个函数接口,主要包含以下四个参数.

  1. 输入格式(InputFormat): 制定数据输入的类型,如 TextInputFormat 等,新旧两个版本所引用的版本分别是 org.apache.hadoop.mapred.InputFormat 和org.apache.hadoop.mapreduce.InputFormat(NewInputFormat)
  2. 键类型: 指定[K,V]键值对中K的类型
  3. 值类型: 指定[K,V]键值对中V的类型
  4. 分区值: 指定由外部存储生成的RDD的partition数量的最小值,如果没有指定,系统会使用默认值defaultMinSplits

注意:其他创建操作的API接口都是为了方便最终的Spark程序开发者而设置的,是这两个接口的高效实现版本.例如,对于textFile而言,只有path这个指定文件路径的参数,其他参数在系统内部指定了默认值。

  1. 在Hadoop中以压缩形式存储的数据,不需要指定解压方式就能够进行读取,因为Hadoop本身有一个解压器会根据压缩文件的后缀推断解压算法进行解压.
  2. 如果用Spark从Hadoop中读取某种类型的数据不知道怎么读取的时候,上网查找一个使用map-reduce的时候是怎么读取这种这种数据的,然后再将对应的读取方式改写成上面的hadoopRDD和newAPIHadoopRDD两个类就行了

9.6 从 Mysql 数据读写文件

  1. 引入 Mysql 依赖:
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.27</version>
</dependency>
  1. 从 Mysql 读取数据
import java.sql.DriverManager

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

object JDBCDemo {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
        val sc = new SparkContext(conf)
        //定义连接mysql的参数
        val driver = "com.mysql.jdbc.Driver"
        val url = "jdbc:mysql://hadoop103:3306/rdd"
        val userName = "root"
        val passWd = "aaa"

        val rdd = new JdbcRDD(
            sc,
            () => {
                Class.forName(driver)
                DriverManager.getConnection(url, userName, passWd)
            },
            "select id, name from user where id >= ? and id <= ?",
            1,
            20,
            2,
            result => (result.getInt(1), result.getString(2))
        )
        rdd.collect.foreach(println)

    }
}
  1. 向 Mysql 写入数据
import java.sql.{Connection, DriverManager, PreparedStatement}

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

object JDBCDemo2 {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
        val sc = new SparkContext(conf)
        //定义连接mysql的参数
        val driver = "com.mysql.jdbc.Driver"
        val url = "jdbc:mysql://hadoop103:3306/rdd"
        val userName = "root"
        val passWd = "aaa"

        val rdd: RDD[(Int, String)] = sc.parallelize(Array((110, "police"), (119, "fire")))
        // 对每个分区执行 参数函数
        rdd.foreachPartition(it => {
            Class.forName(driver)
            val conn: Connection = DriverManager.getConnection(url, userName, passWd)
            it.foreach(x => {
                val statement: PreparedStatement = conn.prepareStatement("insert into user values(?, ?)")
                statement.setInt(1, x._1)
                statement.setString(2, x._2)
                statement.executeUpdate()
            })
        })
    }
}

9.7 从 Hbase 读写文件

由于 org.apache.hadoop.hbase.mapreduce.TableInputFormat 类的实现,Spark 可以通过Hadoop输入格式访问 HBase。
这个输入格式会返回键值对数据,其中键的类型为
org. apache.hadoop.hbase.io.ImmutableBytesWritable,而值的类型为org.apache.hadoop.hbase.client.Result。

  1. 导入依赖
<dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-server</artifactId>
    <version>1.3.1</version>
</dependency>

<dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-client</artifactId>
    <version>1.3.1</version>
</dependency>
  1. Hbase依赖排除
<dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-server</artifactId>
            <version>1.3.1</version>
            <exclusions>
                <exclusion>
                    <groupId>org.mortbay.jetty</groupId>
                    <artifactId>servlet-api-2.5</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>javax.servlet</groupId>
                    <artifactId>servlet-api</artifactId>
                </exclusion>
            </exclusions>

</dependency>
  1. 从 HBase 读取数据
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.client.Result
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.util.Bytes
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

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

        val hbaseConf: Configuration = HBaseConfiguration.create()
        hbaseConf.set("hbase.zookeeper.quorum", "hadoop102,hadoop103,hadoop104")
        hbaseConf.set(TableInputFormat.INPUT_TABLE, "student")

        val rdd: RDD[(ImmutableBytesWritable, Result)] = sc.newAPIHadoopRDD(
            hbaseConf,
            classOf[TableInputFormat],
            classOf[ImmutableBytesWritable],
            classOf[Result])

        val rdd2: RDD[String] = rdd.map {
            case (_, result) => Bytes.toString(result.getRow)
        }
        rdd2.collect.foreach(println)
        sc.stop()
    }
}
  1. 向 HBase 写入数据
import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.client.Put
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.TableOutputFormat
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.mapreduce.Job
import org.apache.spark.{SparkConf, SparkContext}

object HBaseDemo2 {


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

        val hbaseConf = HBaseConfiguration.create()
        hbaseConf.set("hbase.zookeeper.quorum", "hadoop102,hadoop103,hadoop104")
        hbaseConf.set(TableOutputFormat.OUTPUT_TABLE, "student")
        // 通过job来设置输出的格式的类
        val job = Job.getInstance(hbaseConf)
        job.setOutputFormatClass(classOf[TableOutputFormat[ImmutableBytesWritable]])
        job.setOutputKeyClass(classOf[ImmutableBytesWritable])
        job.setOutputValueClass(classOf[Put])

        val initialRDD = sc.parallelize(List(("100", "apple", "11"), ("200", "banana", "12"), ("300", "pear", "13")))
        val hbaseRDD = initialRDD.map(x => {
            val put = new Put(Bytes.toBytes(x._1))
            put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes(x._2))
            put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("weight"), Bytes.toBytes(x._3))
            (new ImmutableBytesWritable(), put)
        })
        hbaseRDD.saveAsNewAPIHadoopDataset(job.getConfiguration)
    }
}

10.RDD 编程进阶

10.1共享变量问题

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

object AccDemo1 {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
        val sc = new SparkContext(conf)
        val p1 = Person(10)
        // 将来会把对象序列化之后传递到每个节点上
        val rdd1 = sc.parallelize(Array(p1))
        val rdd2: RDD[Person] = rdd1.map(p => {p.age = 100; p})

        rdd2.count()
        // 仍然是 10
        println(p1.age)
    }
}

case class Person(var age:Int)

正常情况下, 传递给 Spark 算子(比如: map, reduce 等)的函数都是在远程的集群节点上执行, 函数中用到的所有变量都是独立的拷贝.
这些变量被拷贝到集群上的每个节点上, 这些变量的更改不会传递回驱动程序.
支持跨 task 之间共享变量通常是低效的, 但是 Spark 对共享变量也提供了两种支持:

  1. 累加器
  2. 广播变量

10.2累加器(Accumulator)

  • 累加器解决的是共享变量的写的问题(修改)

累加器用来对信息进行聚合,通常在向 Spark 传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,所以更新这些副本的值不会影响驱动器中的对应变量。
如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果。
累加器是一种变量, 仅仅支持“add”, 支持并发. 累加器用于去实现计数器或者求和. Spark 内部已经支持数字类型的累加器, 开发者可以添加其他类型的支持.

  • 内置累加器
    需求:计算文件中空行的数量
import org.apache.spark.rdd.RDD
import org.apache.spark.util.LongAccumulator
import org.apache.spark.{SparkConf, SparkContext}

object AccDemo1 {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
        val sc = new SparkContext(conf)
        val rdd: RDD[String] = sc.textFile("file://" + ClassLoader.getSystemResource("words.txt").getPath)
        // 得到一个 Long 类型的累加器.  将从 0 开始累加
        val emptyLineCount: LongAccumulator = sc.longAccumulator
        rdd.foreach(s => if (s.trim.length == 0) emptyLineCount.add(1))
        println(emptyLineCount.value)
    }
}

说明:

  1. 在驱动程序中通过sc.longAccumulator得到Long类型的累加器, 还有Double类型的
  2. 可以通过value来访问累加器的值.(与sum等价). avg得到平均值
  3. 只能通过add来添加值.累加器的更新操作最好放在action中, Spark 可以保证每个 task 只执行一次. 如果放在 transformations 操作中则不能保证只更新一次.有可能会被重复执行。

自定义累加器
通过继承类AccumulatorV2来自定义累加器.

下面这个累加器可以用于在程序运行过程中收集一些文本类信息,最终以List[String]的形式返回。

import java.util
import java.util.{ArrayList, Collections}

import org.apache.spark.util.AccumulatorV2

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

    }
}

class MyAcc extends AccumulatorV2[String, java.util.List[String]] {
    private val _list: java.util.List[String] = Collections.synchronizedList(new ArrayList[String]())
    override def isZero: Boolean = _list.isEmpty

    override def copy(): AccumulatorV2[String, util.List[String]] = {
        val newAcc = new MyAcc
        _list.synchronized {
            newAcc._list.addAll(_list)
        }
        newAcc
    }

    override def reset(): Unit = _list.clear()

    override def add(v: String): Unit = _list.add(v)

    override def merge(other: AccumulatorV2[String, util.List[String]]): Unit =other match {
        case o: MyAcc => _list.addAll(o.value)
        case _ => throw new UnsupportedOperationException(
            s"Cannot merge ${this.getClass.getName} with ${other.getClass.getName}")
    }

    override def value: util.List[String] = java.util.Collections.unmodifiableList(new util.ArrayList[String](_list))
}

测试:

object MyAccDemo {
    def main(args: Array[String]): Unit = {
        val pattern = """^\d+$"""
        val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
        val sc = new SparkContext(conf)
        // 统计出来非纯数字, 并计算纯数字元素的和
        val rdd1 = sc.parallelize(Array("abc", "a30b", "aaabb2", "60", "20"))

        val acc = new MyAcc
        sc.register(acc)
        val rdd2: RDD[Int] = rdd1.filter(x => {
            val flag: Boolean = x.matches(pattern)
            if (!flag) acc.add(x)
            flag
        }).map(_.toInt)
        println(rdd2.reduce(_ + _))
        println(acc.value)
    }
}

注意:
在使用自定义累加器的不要忘记注册sc.register(acc)

在这里插入图片描述

10.3广播变量

  • 广播变量解决的是遍历读的问题
  • 大变量
  • 对广播变量, 不要去改

广播变量在每个节点上保存一个只读的变量的缓存, 而不用给每个 task 来传送一个 copy.
例如, 给每个节点一个比较大的输入数据集是一个比较高效的方法. Spark 也会用该对象的广播逻辑去分发广播变量来降低通讯的成本.
广播变量通过调用SparkContext.broadcast(v)来创建. 广播变量是对v的包装, 通过调用广播变量的 value方法可以访问.

scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)

scala> broadcastVar.value
res0: Array[Int] = Array(1, 2, 3)

说明:
1.通过对一个类型T的对象调用SparkContext.broadcast创建出一个Broadcast[T]对象。任何可序列化的类型都可以这么实现。
2.通过value属性访问该对象的值(在Java中为value()方法)。
3.变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值