Spark运行架构
运行架构
Spark框架的核心是一个计算引擎,整体来说,它采用了标准 master-slave 的结构。如下图所示,它展示了一个 Spark执行时的基本结构。图形中的Driver表示master,负责管理整个集群中的作业任务调度。图形中的Executor 则是 slave,负责实际执行任务。
核心组件
Driver
Spark驱动器节点,用于执行Spark任务中的main方法,负责实际代码的执行工作。Driver在Spark作业执行时主要负责:
- 将用户程序转化为作业(job)
- 在Executor之间调度任务(task)
- 跟踪Executor的执行情况
- 通过UI展示查询运行情况
实际上,我们无法准确地描述Driver的定义,因为在整个的编程过程中没有看到任何有关Driver的字眼。所以简单理解,所谓的Driver就是驱使整个应用运行起来的程序,也称之为Driver类。
Executor
Spark Executor是集群中工作节点(Worker)中的一个JVM进程,负责在 Spark 作业中运行具体任务(Task),任务彼此之间相互独立。Spark 应用启动时,Executor节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有Executor节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他Executor节点上继续运行。
Executor有两个核心功能 :
- 负责运行组成Spark应用的任务,并将结果返回给驱动器进程
- 它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD 是直接缓存在Executor进程内的,因此任务可以在运行时充分利用缓存数据加速运算。
Master & Worker
- Spark集群的独立部署环境中,不需要依赖其他的资源调度框架,自身就实现了资源调度的功能,所以环境中还有其他两个核心组件:Master和Worker,这里的Master是一个进程,主要负责资源的调度和分配,并进行集群的监控等职责,类似于Yarn环境中的RM, 而Worker呢,也是进程,一个Worker运行在集群中的一台服务器上,由Master分配资源对数据进行并行的处理和计算,类似于Yarn环境中NM。
ApplicationMaster
- Hadoop用户向YARN集群提交应用程序时,提交程序中应该包含ApplicationMaster,用于向资源调度器申请执行任务的资源容器Container,运行用户自己的程序任务job,监控整个任务的执行,跟踪整个任务的状态,处理任务失败等异常情况。
- 简单来讲,ResourceManager(资源)和Driver(计算)之间的解耦合靠的就是ApplicationMaster。
核心概念
Executor与Core
Spark Executor是集群中运行在工作节点(Worker)中的一个JVM进程,是整个集群中的专门用于计算的节点。在提交应用中,可以提供参数指定计算节点的个数,以及对应的资源。这里的资源一般指的是工作节点Executor的内存大小和使用的虚拟CPU核(Core)数量。
应用程序相关启动参数如下:
并行度 (Parallelism)
在分布式计算框架中一般都是多个任务同时执行,由于任务分布在不同的计算节点进行计算,所以能够真正地实现多任务并行执行,记住,这里是并行,而不是并发。这里我们将整个集群并行执行任务的数量称之为并行度。那么一个作业到底并行度是多少呢?这个取决于框架的默认配置。应用程序也可以在运行过程中动态修改。
有向无环图 (DAG)
大数据计算引擎框架我们根据使用方式的不同一般会分为四类,其中第一类就是Hadoop所承载的MapReduce,它将计算分为两个阶段,分别为 Map阶段 和 Reduce阶段。对于上层应用来说,就不得不想方设法去拆分算法,甚至于不得不在上层应用实现多个 Job 的串联,以完成一个完整的算法,例如迭代计算。 由于这样的弊端,催生了支持 DAG 框架的产生。因此,支持 DAG 的框架被划分为第二代计算引擎。如 Tez 以及更上层的 Oozie。这里我们不去细究各种 DAG 实现之间的区别,不过对于当时的 Tez 和 Oozie 来说,大多还是批处理的任务。接下来就是以 Spark 为代表的第三代的计算引擎。第三代计算引擎的特点主要是 Job 内部的 DAG 支持(不跨越 Job),以及实时计算。
这里所谓的有向无环图,并不是真正意义的图形,而是由Spark程序直接映射成的数据流的高级抽象模型。简单理解就是将整个程序计算的执行过程用图形表示出来,这样更直观,更便于理解,可以用于表示程序的拓扑结构。
DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。
提交流程
所谓的提交流程,其实就是我们开发人员根据需求写的应用程序通过Spark客户端提交给Spark运行环境执行计算的流程。在不同的部署环境中,这个提交过程基本相同,但是又有细微的区别,我们这里不进行详细的比较,但是因为国内工作中,将Spark引用部署到Yarn环境中会更多一些,所以本课程中的提交流程是基于Yarn环境的。
Spark应用程序提交到Yarn环境中执行的时候,一般会有两种部署执行的方式:Client和Cluster。两种模式,主要区别在于:Driver程序的运行节点。
Yarn Client模式
Client模式将用于监控和调度的Driver模块在客户端执行,而不是Yarn中,所以一般用于测试。
- Driver在任务提交的本地机器上运行
- Driver启动后会和ResourceManager通讯申请启动ApplicationMaster
- ResourceManager分配container,在合适的NodeManager上启动ApplicationMaster,负责向ResourceManager申请Executor内存
- ResourceManager接到ApplicationMaster的资源申请后会分配container,然后ApplicationMaster在资源分配指定的NodeManager上启动Executor进程
- Executor进程启动后会向Driver反向注册,Executor全部注册完成后Driver开始执行main函数
- 之后执行到Action算子时,触发一个Job,并根据宽依赖开始划分stage,每个stage生成对应的TaskSet,之后将task分发到各个Executor上执行。
Yarn Cluster模式
Cluster模式将用于监控和调度的Driver模块启动在Yarn集群资源中执行。一般应用于实际生产环境。 - 在YARN Cluster模式下,任务提交后会和ResourceManager通讯申请启动ApplicationMaster,
- 随后ResourceManager分配container,在合适的NodeManager上启动ApplicationMaster,此时的ApplicationMaster就是Driver。
- Driver启动后向ResourceManager申请Executor内存,ResourceManager接到ApplicationMaster的资源申请后会分配container,然后在合适的NodeManager上启动Executor进程
- Executor进程启动后会向Driver反向注册,Executor全部注册完成后Driver开始执行main函数,
- 之后执行到Action算子时,触发一个Job,并根据宽依赖开始划分stage,每个stage生成对应的TaskSet,之后将task分发到各个Executor上执行。
Spark核心编程
Spark计算框架为了能够进行高并发和高吞吐的数据处理,封装了三大数据结构,用于处理不同的应用场景。三大数据结构分别是:
- RDD : 弹性分布式数据集
- 累加器:分布式共享只写变量
- 广播变量:分布式共享只读变量
RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。
- 弹性
- 存储的弹性:内存与磁盘的自动切换;
- 容错的弹性:数据丢失可以自动恢复;
- 计算的弹性:计算出错重试机制;
- 分片的弹性:可根据需要重新分片。
- 分布式:数据存储在大数据集群不同节点上
- 数据集:RDD封装了计算逻辑,并不保存数据
- 数据抽象:RDD是一个抽象类,需要子类具体实现
- 不可变:RDD封装了计算逻辑,是不可以改变的,想要改变,只能产生新的RDD,在新的RDD里面封装计算逻辑
- 可分区、并行计算
核心属性
分区列表
RDD数据结构中存在分区列表,用于执行任务时并行计算,是实现分布式计算的重要属性。
分区计算函数
Spark在计算时,是使用分区函数对每一个分区进行计算
RDD之间的依赖关系
RDD是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个RDD建立依赖关系
分区器(可选)
当数据为KV类型数据时,可以通过设定分区器自定义数据的分区
首选位置(可选)
计算数据时,可以根据计算节点的状态选择不同的节点位置进行计算
执行原理
从计算的角度来讲,数据处理过程中需要计算资源(内存 & CPU)和计算模型(逻辑)。执行时,需要将计算资源和计算模型进行协调和整合。
Spark框架在执行时,先申请资源,然后将应用程序的数据处理逻辑分解成一个一个的计算任务。然后将任务发到已经分配资源的计算节点上, 按照指定的计算模型进行数据计算。最后得到计算结果。
RDD是Spark框架中用于数据处理的核心模型,接下来我们看看,在Yarn环境中,RDD的工作原理:
- 启动Yarn集群环境
- Spark通过申请资源创建调度节点和计算节点
- Spark框架根据需求将计算逻辑根据分区划分成不同的任务
- 调度节点将任务根据计算节点状态发送到对应的计算节点进行计算
RDD在整个流程中主要用于将逻辑进行封装,并生成Task发送给Executor节点执行计算
基础编程
RDD创建
从集合(内存)中创建RDD
val sparkConf: SparkConf = new SparkConf().setMaster("local").setAppName("wordCount")
val sc = new SparkContext(sparkConf)
// 从内存中创建RDD - makeRDD参数解读
// 第一个参数 : 数据源
// 第二个参数 : 并行度 , 等同于RDD分区
// 并行度首先会从spark配置信息中获取spark.default.parallelism值
// 如果获取不到指定参数, 则会采用默认值totalCores(机器的总核数)
// 机器的总核数取决于当前环境中可用的核数
// local → 单核(单线程)
// local[4] → 四核(4线程)
// local[*] → 最大核数(取决于物理机核数)
// 从内存中创建RDD
// 方式一 :
val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4))
println(rdd1.collect().mkString(", "))
// 方式二 : 底层就是调用的parallelize方法, 原理一样
val rdd2: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
println(rdd2.collect().mkString(", "))
// 将RDD的处理后的数据保存到分区文件中
rdd.saveAsTextFile("output")
sc.stop()
从外部存储(文件)创建RDD
val sparkConf: SparkConf = new SparkConf().setMaster("local").setAppName("wordCount")
val sc = new SparkContext(sparkConf)
// 从磁盘创建RDD
val fileRdd: RDD[String] = sc.textFile("input")
println(fileRdd.
flatMap(line => line.split(" ")).
groupBy(word => word).
map(kv => (kv._1, kv._2.size)).
collect().
mkString(", "))
// 默认使用Hadoop读取文件的规则, 一行一行读取
// 读取指定文件、文件目录、读取HDFS, 支持通配符
sc.stop()
从其他RDD创建
主要是通过一个RDD运算完后,再产生新的RDD
直接创建RDD(new)
使用new的方式直接构造RDD,一般由Spark框架自身使用
RDD并行度与分区
内存(集合)数据分区规则
// =============== RDD并行度 与 分区 ==================]
// 并行度 : 能够并行计算的任务数量
// 由于一个分区对应一个任务, 所以, 在数值上 并行度 = 分区数
// 该数值可以在创建RDD时设定
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark - Parallelism")
val sc = new SparkContext(sparkConf)
// makeRDD 第一个参数 : 数据源
// makeRDD 第二个参数 : 默认并行度(分区数)
// 数据分区规则(内存) :
// 首先采用创建RDD时的传值numSlices
// 否则默认会从spark配置信息中获取spark.default.parallelism
// 如果获取不到指定参数, 则会采用默认值totalCores(总核数)
// totalCores取决于当前可用的总核数
// setMaster("local") → 表示单核
// setMaster("local[2]") → 表示双核
// setMaster("local[*]") → 表示最大核数(取决于物理机核数)
// ① 首先将内存中的数据按照平均分的方式进行分区处理
val rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6), 3)
// (seq.length = 6, numSlices = 3)
// → 分区数 : 3
// → 分区数据 : 6 / 3 = 2
// → 分区编号(0, 1, 2)
// List(1, 2, 3, 4, 5, 6) => Array(1, 2, 3 ,4, 5, 6)
// Array.slice => 切分数组 => (start until end)
// val start = ((i * length) / numSlices).toInt // i指分区号
// val end = (((i + 1) * length) / numSlices).toInt
// → 0 → [0, 2) → 1, 2
// → 1 → [2, 4) → 3, 4
// → 2 → [4, 6) → 5, 6
// ② 当内存中的数据不能均分时, 会将剩余的数据按照一个算法实现分配
val rdd2: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5), 3)
// (seq.length = 5, numSlices = 3)
// → 分区数 : 3
// → 分区数据 : 5 / 3 = 1...2
// → 分区编号(0, 1, 2)
// List(1, 2, 3, 4, 5, 6) => Array(1, 2, 3 ,4, 5, 6)
// Array.slice => 切分数组 => (start(from), end(until))
// val start = ((i * length) / numSlices).toInt // i指分区号
// val end = (((i + 1) * length) / numSlices).toInt
// → 0 → [0, 1) → 1
// → 1 → [1, 3) → 2, 3
// → 2 → [3, 5) → 4, 5
// rdd.collect().foreach(println)
// 将RDD的处理后的数据保存到分区文件中
rdd1.saveAsTextFile("output")
rdd2.saveAsTextFile("output")
sc.stop()
磁盘(单文件)数据分区规则
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark - Parallelism")
val sc = new SparkContext(sparkConf)
// textFile 第一个参数 : 数据源路径
// textFile 第二个参数 : 最小分区数
// 默认值为 : math.min(defaultParallelism, 2) => defaultParallelism : 总核数(totalCore)
// minPartitions => numSplits
// 数据分区规则(磁盘/单个文件) :
// 读取文件数据时, 是按照Hadoop文件读取的规则进行切片分区, 而切片规则和数据读取的规则有些差异
// 文件切片规则 : 按字节切片
// ① 文件字节数(totalSize) = 10byte, 预计切片数量(numSplits) = 2(默认值)
// goalSize = totalSize / (numSplits == 0 ? 1 : numSplits) = 10 / 2 = 5 => 2个分区
// 数据分区(读取)规则 : 按行读取
// splitSize = Math.max(minSize, Math.min(goalSize, blockSize))
// minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize)
val rdd1: RDD[String] = sc.textFile("input/word1.txt")
rdd1.saveAsTextFile("output1") // 12, 34
// 所谓的最小分区数, 取决于总的字节数是否能整除分区数并且剩余的字节达到一个比率(1.1)
// 实际产生的分区数量可能大于最小分区数
// ② 文件字节数(totalSize) = 10byte, 预计切片数量(numSplits) = 4(minPartitions)
// 10 / 4 = 2byte...2byte => 5个分区 (由于剩余数据 2 / 4 > 1.1, 则增加一个分区)
// 分区号 : (0, 1, 2, 3, 4)
// > 文件切片规则 : (按字节切片)
// [start to end)
// 0 => [0, 2]
// 1 => [2, 4]
// 2 => [4, 6]
// 3 => [6, 8]
// 4 => [8, 10]
// > 数据分区规则 : (按行读取 + 偏移量)
// 数据按行读取时, 还会考虑偏移量(数据的offset)的设置
// data offset
// 1\n => 012
// 2\n => 345
// 3\n => 678
// 4 => 9
// > 最终数据分区规则 : 文件切片规则 + 按行读取 + 偏移量
// 0 => [0, 2] => (012) => 1\n
// 1 => [2, 4] => (345) => 2\n
// 2 => [4, 6] => (678) => 3\n
// 3 => [6, 8] => 空
// 4 => [8, 10] => (9) => 4
// 小结 : 按行读取(不允许一行数据被切开)的基础上, 考虑偏移量, 文件切片, 分别读取到每个分区。
val rdd2: RDD[String] = sc.textFile("input/word1.txt", 4)
rdd2.saveAsTextFile("output2") // 1, 2, 3, 空, 4
// 例题 :
// 分区数 : 10 / 3 = 3..1 => 4个分区 (1 / 3 > 1.1)
// 分区号 : (0, 1, 2, 3)
// > 文件切片 : 按字节
// 0 => [0, 3]
// 1 => [3, 6]
// 2 => [6, 9]
// 3 => [9, 10]
// > 数据分区 : 按行读取 + 偏移量
// data offset
// 1\n => 012
// 2\n => 345
// 3\n => 678
// 4 => 9
// > 最终数据分区 : 文件切片规则 + 按行读取 + 偏移量
// 0 => [0, 3] => (012345) => 1\n2\n
// 1 => [3, 6] => (678) => 3\n
// 2 => [6, 9] => (9) => 4
// 3 => [9, 10] => 空
val rdd3: RDD[String] = sc.textFile("input/word1.txt", 3)
rdd3.saveAsTextFile("output3") // 12, 3, 4, 空
sc.stop()
// word1.txt数据(10byte)(\n : 换行)
// 1\n
// 2\n
// 3\n
// 4
磁盘(多文件)数据分区规则
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark - Parallelism")
val sc = new SparkContext(sparkConf)
// 数据分区规则(磁盘/多个文件) :
// 分区数 : 12 / 2 = 6 => 2个分区
// [0, 6] => 1\n234
// [0, 6] => 5\n678
val rdd1: RDD[String] = sc.textFile("input", 2)
rdd1.saveAsTextFile("output1") // 1\n234 , 5\n678
// 最终数据分区规则 : 文件划分规则 + 文件切片规则 + 按行读取 + 偏移量
// Hadoop分区是以文件为单位进行划分的, 读取数据不能跨越文件
// 分区数 : 12 / 3 = 4 => 4个分区
// word1.txt => [0, 4] => 1\n234
// => [4, 6] => 空
// word2.txt => [0, 4] => 5\n678
// => [4, 6] => 空
val rdd2: RDD[String] = sc.textFile("input", 3)
rdd2.saveAsTextFile("output2") // 1\n234 , 5\n678
sc.stop()
// word1.txt数据(6byte)(\n : 换行)
// 1\n
// 234
// word2.txt数据(6byte)(\n : 换行)
// 5\n
// 678
RDD转换算子
Value类型
map
// =================== RDD转换算子 ================
// 问题(初始) → 问题(处理中) → 问题(完成)
// operation operation
// 算子 算子
// 所谓的RDD的转换算子, 就是将旧的RDD通过方法的调用转换成新的RDD
// 分类 : Value类型 、双Value类型 、Key-Value类型
// ============== Value类型 - map ================
// 函数签名 : TODO def map[U: ClassTag](f: T => U): RDD[U]
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// 转换算子 → 不会触发作业的执行, 只是功能的封装
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
// 实现功能 : 旧RDD → map算子 → 新RDD
// 分区规则 : RDD中有分区列表, 默认分区数量不变, 区内数据转换后分别输出
// 执行顺序 : 区内数据按照算子的顺序执行, 一条数据执行完所有逻辑, 再操作下一条数据
// 区间数据执行没有顺序, 且无需等待, 互不相干
val newRdd: RDD[Int] = rdd.map(_ * 2)
newRdd.saveAsTextFile("output1")
// collect()方法不会转换RDD, 而是会触发作业的执行, 将该类方法称之为<行动算子(action)>
// newRdd.collect()
sc.stop()
// 需求 : 从服务器日志数据apache.log中获取用户请求URL资源路径
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("function_1")
val sc = new SparkContext(sparkConf)
val fileRDD: RDD[String] = sc.textFile("input/apache.log")
val urlRDD: RDD[String] = fileRDD.map(line => {
val datas: Array[String] = line.split(" ")
datas(6)
})
urlRDD.collect().foreach(println)
sc.stop()
mapPartitions
// ============== Value类型 - mapPartitions ================
/* 函数签名 :
TODO def mapPartitions[U: ClassTag](
f: Iterator[T] => Iterator[U],
preservesPartitioning: Boolean = false): RDD[U]
*/
// map算子 : 对数据以条为单位进行处理; 全量数据操作, 不能丢失数据
// mapPartitions算子 : 对数据以区为单位进行处理; 一次性获取分区的所有数据, 可以执行迭代器集合的所有操作
// 后者类似批处理, 如果一个分区的数据没有完全处理完, 那么当前分区的所有数据都不会释放, 有内存溢出的风险
// 当内存空间足够大时, 为了提高效率, 推荐使用mapPartitions算子
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
val dataRDD: RDD[Int] = rdd.mapPartitions(iter => {
iter.foreach(println)
iter.map(_ * 2)
// iter.filter(_ % 2 == 2)
})
println(dataRDD.collect().mkString(", ")) // 2, 4, 6, 8
sc.stop()
// 需求 : 获取每个分区的最大值
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
val rdd: RDD[Int] = sc.makeRDD(List(4, 7, 2, 9, 4, 0), 3)
val dataRDD: RDD[Int] = rdd.mapPartitions(iter => {
List(iter.max).iterator
})
println(dataRDD.collect().mkString(", ")) // 7, 9, 4
sc.stop()
mapPartitionsWithIndex
// ============== Value类型 - mapPartitionsWithIndex ================
/* 函数签名 :
TODO def mapPartitionsWithIndex[U: ClassTag](
f: (Int, Iterator[T]) => Iterator[U],
preservesPartitioning: Boolean = false): RDD[U]
*/
// 相较于mapPartitions算子返回datas
// 该算子返回的是Tuple(index, datas) index => 分区号
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
val dataRDD: RDD[Int] = rdd.mapPartitionsWithIndex((index, datas) => {
datas.map(index * _)
})
println(dataRDD.collect().mkString(", ")) // 0, 0, 3, 4
sc.stop()
// 需求 : 获取第二个数据分区的数据
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
val dataRDD: RDD[Int] = rdd.mapPartitionsWithIndex((index, datas) => {
if (index == 1) {
datas
} else {
Nil.iterator
}
})
println(dataRDD.collect().mkString(", ")) // 3, 4
sc.stop()
flatMap
// ============== Value类型 - flatMap ================
// 函数签名 : TODO def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// 将数据扁平化后再进行映射处理, 称之为扁平映射算子
val rdd: RDD[List[Int]] = sc.makeRDD(List(List(1, 2), List(3, 4)))
val newRDD: RDD[Int] = rdd.flatMap(list => list)
println(newRDD.collect().mkString(", ")) // 1, 2, 3, 4
sc.stop()
// 需求 : 将List(List(1,2), 3, (List(4,5))进行扁平化处理
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// 将数据扁平化后再进行映射处理, 称之为扁平映射算子
val rdd: RDD[Any] = sc.makeRDD(List(List(1, 2), 3, (List(4, 5))))
val newRDD: RDD[Int] = rdd.flatMap {
datas => {
datas match {
case list: List[Int] => list
case data: Int => List(data)
}
}
}
println(newRDD.collect().mkString(", ")) // 1, 2, 3, 4, 5
sc.stop()
glom
// ============== Value类型 - glom ================
// 函数签名 : TODO def glom(): RDD[Array[T]]
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// 将同一个分区的数据直接转换为相同类型的内存数组进行处理, 分区不变
// 即 : data : Any => Array[Any]
val rdd: RDD[Any] = sc.makeRDD(List(1, "2", 3, 4), 2)
val newRDD: RDD[Array[Any]] = rdd.glom()
newRDD.collect().foreach(_.foreach(println)) // 1, 2, 3, 4
sc.stop()
// 需求 : 计算所有分区最大值求和(分区内取最大值,分区间最大值求和)
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
val rdd: RDD[Int] = sc.makeRDD(List(5, 4, 7, 9, 2, 0), 3) /// 16
// 将每个分区的数据转换为数组
val newRDD1: RDD[Array[Int]] = rdd.glom()
// 将数组中的最大值取出
// val newRDD2: RDD[Int] = newRDD1.map(arr => arr.toList.max)
val newRDD2: RDD[Int] = newRDD1.map(arr => arr.max) // 自动转换
// 将取出的最大值求和
val array: Array[Int] = newRDD2.collect()
println(array.sum) // 1, 2, 3, 4
sc.stop()
groupBy
// ============== Value类型 - groupBy ================
// 函数签名 : TODO def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// 将数据根据指定的规则进行分组, 分区数默认不变,但是数据会被打乱重新组合,我们将这样的操作称之为shuffle。
// 极限情况下,数据可能被分在同一个分区中
// 该算子可能会导致, 下游RDD分区的数据倾斜, 此时可以改变下游RDD的数据分区数
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6), 3)
val newRDD: RDD[(Int, Iterable[Int])] = rdd.groupBy(
(num: Int) => {
num % 2
}, 2 // 设置shuffle后的下游分区数
)
newRDD.foreach(println)
sc.stop()
// 需求 : 将List("Hello", "hive", "hbase", "Hadoop")根据单词首写字母进行分组
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
val rdd: RDD[String] = sc.makeRDD(List("Hello", "hive", "hbase", "Hadoop"), 2)
val newRDD: RDD[(Char, Iterable[String])] = rdd.groupBy(word => {
// word.subSequence(0, 1)
// word.charAt(0)
// 隐式转换 : String(0) => StringOps
word(0)
})
newRDD.foreach(println)
sc.stop()
// 需求 : 从服务器日志数据apache.log中获取每个时间段访问量
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
val rdd: RDD[String] = sc.textFile("input/apache.log")
val newRDD1: RDD[String] = rdd.map(
log => {
val datas: Array[String] = log.split(" ")
datas(3)
}
)
val newRDD2: RDD[(String, Iterable[String])] = newRDD1.groupBy(
time => {
time.substring(11, 13)
}
)
newRDD2.foreach(println)
sc.stop()
// 需求 : WordCount
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
val rdd: RDD[String] = sc.makeRDD(List("Hello Scala", "Hello Spark", "Spark Scala"))
val newRDD1: RDD[String] = rdd.flatMap(data => data.split(" "))
val newRDD2: RDD[(String, Iterable[String])] = newRDD1.groupBy(word => word)
val result: RDD[(String, Int)] = newRDD2.map(kv => (kv._1, kv._2.size))
result.foreach(println)
sc.stop()
filter
// ============== Value类型 - filter ================
// 函数签名 : TODO def filter(f: T => Boolean): RDD[T]
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// filter : 过滤, 将数据按照指定的规则进行筛选过滤, 符合规则的数据将保留, 不符合规则的数据丢弃
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 6, 4, 5, 3), 3)
val result: RDD[Int] = rdd.filter(word => word % 2 == 0)
// 当数据筛选过滤后, 分区不变, 但是分区内的数据可能不均衡, 在生产环境下, 可能会出现数据倾斜
result.foreach(println)
sc.stop()
// 需求 : 从服务器日志数据apache.log中获取2015年5月17日的请求路径
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
val rdd: RDD[String] = sc.textFile("input/apache.log")
val newRDD1: RDD[String] = rdd.map(
log => {
val datas: Array[String] = log.split(" ")
datas(3)
}
)
val result: RDD[String] = newRDD1.filter(time => time.substring(0, 10) == "17/05/2015")
result.foreach(println)
sc.stop()
sample
// ============== Value类型 - sample ================
/* 函数签名 :
TODO
def sample(
withReplacement: Boolean,
fraction: Double,
seed: Long = Utils.random.nextLong): RDD[T]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// sample : 根据指定的规则从数据集中抽取数据
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6), 3)
// 抽取数据不放回(伯努利算法)
// 伯努利算法 : 又叫0、1分布。例如扔硬币, 要么正面, 要么反面。
// 具体实现 : 根据种子和随机算法算出一个数和第二个参数设置几率较小, 小于第二个参数要, 大于不要
// 第一个参数 : 抽取的数据是否放回, false : 不放回
// 第二个参数 : 每条数据抽取的几率, [0, 1]
// 第三个参数 : 随机数种子
val result1: RDD[Int] = rdd.sample(false, 0.5, 1)
// 抽取数据后放回(泊松算法)
// 第一个参数 : 抽取的数据是否放回, true : 放回
// 第二个参数 : 每一条数据出现次数的期望(λ)。 [0, ∞), 表示每一条数据被期望抽到的次数
// 第三个参数 : 随机数种子
val result2: RDD[Int] = rdd.sample(true, 2, 1)
result1.foreach(println)
result2.foreach(println)
// 应用 - 取样
// 比如 : 如果在开发中, 出现了数据倾斜的情况, 那么可以从数据倾斜的分区中抽取一部分数据进行分析
sc.stop()
distinct
// ============== Value类型 - distinct ================
/* 函数签名 :
TODO
def distinct()(implicit ord: Ordering[T] = null): RDD[T]
def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// distinct : 将数据集中的数据进行去重
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 1, 2, 4), 3)
val result1: RDD[Int] = rdd.distinct()
// distinct的第二个参数 : 去重后的分区数量
val result2: RDD[Int] = rdd.distinct(2)
result1.saveAsTextFile("output1")
result2.saveAsTextFile("output2")
sc.stop()
coalesce
// ============== Value类型 - coalesce ================
/* 函数签名 :
TODO
def coalesce(numPartitions: Int, shuffle: Boolean = false,
partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
(implicit ord: Ordering[T] = null)
: RDD[T]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// coalesce : 缩减分区, 用于大数据集过滤后(或分区不合理时), 减少分区的个数, 降低任务调度成本
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 1, 2, 4), 3)
// 减小分区
// 第一个参数表示缩减分区后的分区数量
// coalesce默认无法扩大分区, 在缩减分区时数据不会打乱重组, 即coalesce默认没有shuffle过程
val result1: RDD[Int] = rdd.coalesce(2)
// 扩大分区 : 常用repartition代替
// 第二个参数表示是否启用shuffle。不开启shuffle, 无法扩大分区
val result2: RDD[Int] = rdd.coalesce(4, true) // 默认为false
result1.foreach(println)
result2.foreach(println)
sc.stop()
repartition
// ============== Value类型 - repartition ================
/* 函数签名 :
TODO
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// repartition : 改变分区数, 底层执行的coalesce操作
// 默认开启shuffle过程, 且无法更改。因此缩减分区时不推荐使用
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 1, 2, 4), 3)
val result: RDD[Int] = rdd.repartition(4)
result.foreach(println)
sc.stop()
sortBy
// ============== Value类型 - sortBy ================
/* 函数签名 :
TODO
def sortBy[K](
f: (T) => K,
ascending: Boolean = true,
numPartitions: Int = this.partitions.length)
(implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// sortBy : 按照规则对数据进行排序, 默认正序, 不改变分区数
// 可以通过传递第二个参数改变排序的方式
// 可以设定第三个参数改变排序后的分区数
val rdd: RDD[Int] = sc.makeRDD(List(4, 5, 3, 1, 2), 3)
// 默认升序
val result1: RDD[Int] = rdd.sortBy(word => word)
// 降序, 排序后分区设为2
val result2: RDD[Int] = rdd.sortBy(word => word, false ,2)
result1.foreach(println)
result2.foreach(println)
sc.stop()
pipe
函数签名 : def pipe(command: String): RDD[String]
管道,针对每个分区,都调用一次shell脚本,返回输出的RDD。
注意:shell脚本需要放在计算节点可以访问到的位置
- 编写一个脚本,并增加执行权限
[root@linux1 data]# vim pipe.sh
#!/bin/sh
echo "Start"
while read LINE; do
echo ">>>"${LINE}
done
[root@linux1 data]# chmod 777 pipe.sh
- 命令行工具中创建一个只有一个分区的RDD
scala> val rdd = sc.makeRDD(List("hi","Hello","how","are","you"), 1)
- 将脚本作用该RDD并打印
scala> rdd.pipe("/opt/module/spark/pipe.sh").collect()
res18: Array[String] = Array(Start, >>>hi, >>>Hello, >>>how, >>>are, >>>you)
双Value类型
// ============== 双Value类型 ================
/* 函数签名 :
TODO
def intersection(other: RDD[T]): RDD[T]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// 当两个RDD的数据类型不一致时
// 对于并集/交集/差集 : 编译不通过(泛型限制)
// 对于拉链 : 正常拉链
val rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 3)
val rdd2: RDD[Int] = sc.makeRDD(List(3, 4, 5, 6), 4)
// intersection : 对源RDD和参数RDD求 <交集> 对应Scala中的intersect
// 保留最大分区数, 数据被打乱重组(shuffle)
val newRDD1: RDD[Int] = rdd1.intersection(rdd2)
newRDD1.saveAsTextFile("output1")
// union : 对源RDD和参数RDD求 <并集>
// 数据合并, 分区也合并(numSlices1 + numSlices2)
val newRDD2: RDD[Int] = rdd1.union(rdd2)
newRDD2.saveAsTextFile("output2")
// subtract : 对源RDD和参数RDD求 <差集> 对应Scala中的diff
// 保留rdd1的分区数, 数据被打乱重组(shuffle)
val newRDD3: RDD[Int] = rdd1.subtract(rdd2) // 1, 2
newRDD3.saveAsTextFile("output3")
// zip : 将两个RDD中的数据, 以键值对形式进行合并。其中Key为rdd1中的元素, Value为rdd2中的元素
// 两个RDD的分区数一致, 但总数据量不一致 时
// 对部分数据拉链 并 报异常 : Can only zip RDDs with same number of elements in each partition
// 两个RDD的分区数不一致, 总数据量一致 时
// 报异常 : Can't zip RDDs with unequal numbers of partitions: List(4, 3)
val newRDD4: RDD[(Int, Int)] = rdd1.zip(rdd2)
newRDD4.saveAsTextFile("output4")
sc.stop()
Key-Value类型
partitionBy
object RDD29_operator18_partitionBy {
def main(args: Array[String]): Unit = {
// =================== RDD转换算子 ================
// ============== Key - Value类型 - partitionBy ================
// Spark中很多方法是基于Key进行操作, 所以数据的理想格式为键值对或对偶元组
// 当RDD中数据类型为K-V类型时, Spark会自动补充很多算子(功能扩展) ==> 隐式转换
// partitionBy方法来自于PairRDDFunction类
// RDD的伴生对象中提供了隐式转换可以将PairRDDFunction中的功能扩展到RDD[K, V]类中
/* 函数签名 :
TODO
def partitionBy(partitioner: Partitioner): RDD[(K, V)]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3), ("d", 4)))
// partitionBy : 将数据按照指定分区规则进行重分区。
// 参数 : 分区器对象。HashPartitioner 或 RangePartitioner, Spark默认的分区器为HashPartitioner
// HashPartitioner的分区规则 : 将当前数据的key进行取余操作
val result1: RDD[(String, Int)] = rdd.partitionBy(new HashPartitioner(2))
result1.saveAsTextFile("output1")
// RangePartitioner的分区规则 : 按照key的大小匹配分区的设定范围。前提是key可比较大小
// sortBy使用了RangePartitioner
// 如果重分区的分区器和当前RDD的分区数一样, 那么不进行任何处理
// ============ 自定义分区器 ==================
val result2: RDD[(String, Int)] = rdd.partitionBy(MyPartitioner(3))
result2.mapPartitionsWithIndex(
(index, datas) => {
datas.map(
data => (index, data)
)
}
).foreach(println)
sc.stop()
}
// 自定义分区器
// 继承Partitioner类并重写方法
case class MyPartitioner(num: Int) extends Partitioner {
// 获取分区的数量
override def numPartitions: Int = num
// 根据数据的key来决定数据在哪个分区中进行处理
// 方法的返回值为分区号(index)
override def getPartition(key: Any): Int = {
key match {
case "a" => 0
case "b" => 1
case _ => 2
}
}
}
}
reduceByKey
// ============== Key - Value类型 - reduceByKey ================
/* 函数签名 :
TODO
def reduceByKey(func: (V, V) => V): RDD[(K, V)]
def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("a", 3), ("b", 4)))
// reduceByKey : 将数据按照相同的key对value进行聚合
// 第一个参数表示 : 相同key的value的聚合方式
// 第二个参数表示 : 聚合后的分区数
val result: RDD[(String, Int)] = rdd.reduceByKey(_ + _, 2)
result.foreach(println)
sc.stop()
groupByKey
// ============== Key - Value类型 - groupByKey ================
/* 函数签名 :
TODO
def groupByKey(): RDD[(K, Iterable[V])]
def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])]
def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("a", 3), ("b", 4)))
// groupByKey : 将分区的数据直接转换为相同类型的内存数组进行后续处理
// 返回的数据类型为元组
// 元组的第一个元素表示用于分组的key
// 元组的第二个元素表示分组后, 相同key的value的集合
val result: RDD[(String, Iterable[Int])] = rdd.groupByKey()
result.foreach(println)
// reduceByKey 与 groupByKey 的区别
// 两个算子在实现相同的业务功能时, reduceByKey存在预聚合功能, 性能较高, 推荐使用
// 当数据的处理不限于聚合操作时, 可采用groupByKey算子
sc.stop()
aggregateByKey
// ============== Key - Value类型 - aggregateByKey ================
/* 函数签名 :
TODO
def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U,
combOp: (U, U) => U): RDD[(K, U)]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// reduceByKey : 分区内和分区间计算规则相同
// aggregateByKey : 分区间和分区间计算规则不同
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("a", 2), ("c", 3), ("b", 4), ("c", 5), ("c", 6)), 2)
// aggregateByKey : 将数据根据不同的规则进行区内计算和区间计算
// 第一个参数列表 zeroValue : 计算的初始值, 用于在分区内计算时, 当作初始值
// 第二个参数列表 seqOp : 分区内计算规则, 相同key的value的计算
// combOp : 分区间计算规则, 相同key的value的计算
val result: RDD[(String, Int)] = rdd.aggregateByKey(0)(
(x, y) => math.max(x, y), // 区内相同key的value取最大值
(x, y) => x + y // 区间相同key的value求和
)
// 如果分区内计算规则和分区间的计算规则相同且都是求和, 那么可以计算WordCount
// val result1: RDD[(String, Int)] = rdd.aggregateByKey(0)(
// (x, y) => x + y,
// (x, y) => x + y
// )
// 简化后
// val result1: RDD[(String, Int)] = rdd.aggregateByKey(0)(_ + _, _ + _)
// 或使用foldByKey()
val result1: RDD[(String, Int)] = rdd.foldByKey(0)(_ + _)
result.foreach(println)
result1.foreach(println)
// 对比Scala
// List().reduce(_ + _)
// List().fold(0)(_ + _)
// Spark
// rdd.reduceByKey(_ + _)
// rdd.foldByKey(0)(_ + _)
// reduceByKey 与 groupByKey 的区别
// 两个算子在实现相同的业务功能时, reduceByKey存在预聚合功能, 性能较高, 推荐使用
// 当数据的处理不限于聚合操作时, 可采用groupByKey算子
sc.stop()
combineByKey
// ============== Key - Value类型 - combineByKey ================
/* 函数签名 :
TODO
def combineByKey[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C): RDD[(K, C)]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// combineByKey : 最通用的对key-value型rdd进行聚集操作的聚集函数(aggregation function)。
// 类似于aggregate(),combineByKey()允许用户返回值的类型与输入不一致
// 第一个参数表示 : 将计算的第一个key的value值转换结构
// 第二个参数表示 : 分区内的计算规则
// 第三个参数表示 : 分区间的计算规则
// 需求 : 将数据List(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98))求每个key的value的平均值
// 0 => [("a", 88), ("b", 95), ("a", 91)]
// 1 => [("b", 93), ("a", 95), ("b", 98)]
// 88 => (88, 1) + 91 => (179, 2) => (274, 3)
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98)), 2)
val result: RDD[(String, (Int, Int))] = rdd.combineByKey(
v => (v, 1),
(x: (Int, Int), y) => (x._1 + y, x._2 + 1), // 区内计算 : 数量叠加 ; 次数+1
(x: (Int, Int), y: (Int, Int)) => (x._1 + y._1, x._2 + y._2) // 区间计算 : 数量相加 ; 次数相加
)
result.map{
case (k, (total, cnt)) => (k, total / cnt)
}.collect().foreach(println)
sc.stop()
sortByKey
def main(args: Array[String]): Unit = {
// =================== RDD转换算子 ================
// ============== Key - Value类型 - sortByKey ================
/* 函数签名 :
TODO
def sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length)
: RDD[(K, V)]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// sortByKey : 在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的value
// 第一个参数 : true 升序 ; false 降序
// 第二个参数 : 排序后的分区数
// 需求 : 设置key为自定义User
val rdd: RDD[(User, Int)] = sc.makeRDD(
List(
(new User(), 3),
(new User(), 2),
(new User(), 4)
)
)
val result: RDD[(User, Int)] = rdd.sortByKey(true, 2)
result.collect().foreach(println)
sc.stop()
}
// 继承Ordered, 混入Serializable
class User extends Ordered[User] with Serializable {
override def compare(that: User): Int = {
1
}
}
join
def main(args: Array[String]): Unit = {
// =================== RDD转换算子 ================
// ============== Key - Value类型 - join ================
/* 函数签名 :
TODO
def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// join : 在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素连接在一起的(K,(V,W))的RDD
// 由于join过程存在shuffle过程和笛卡尔积现象, 故性能较低
val rdd1: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (2, "b"), (3, "c")))
val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (3, 6)))
val result: RDD[(Int, (String, Int))] = rdd1.join(rdd2)
result.collect().foreach(println)
sc.stop()
}
leftOuterJoin
def main(args: Array[String]): Unit = {
// =================== RDD转换算子 ================
// ============== Key - Value类型 - leftOuterJoin ================
/* 函数签名 :
TODO
def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// leftOuterJoin : 类似于SQL语句的左外连接
val rdd1: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (2, "b"), (3, "c")))
val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (3, 6)))
val result: RDD[(Int, (String, Option[Int]))] = rdd1.leftOuterJoin(rdd2)
result.collect().foreach(println)
sc.stop()
}
cogroup
def main(args: Array[String]): Unit = {
// =================== RDD转换算子 ================
// ============== Key - Value类型 - cogroup ================
/* 函数签名 :
TODO
def cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))]
*/
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// cogroup : 在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable<V>,Iterable<W>))类型的RDD
val rdd1: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (2, "b"), (3, "c")))
val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (3, 6)))
val result: RDD[(Int, (Iterable[String], Iterable[Int]))] = rdd1.cogroup(rdd2)
result.collect().foreach(println)
sc.stop()
}
RDD行动算子(Action)
def main(args: Array[String]): Unit = {
// =================== RDD行动算子(Action) ================
// 转换算子不会触发作业的执行, 只是功能的扩展和包装
// 所谓的行动算子, 不会再产生新的RDD, 而是触发作业的执行
// 执行后, 会获取到作业的执行结果
// Spark的行动算子执行时, 会产生Job对象, 然后提交这个Job对象
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Operation")
val sc = new SparkContext(sparkConf)
// ============== reduce ================
/* 函数签名 :
TODO
def reduce(f: (T, T) => T): T
*/
// reduce : 简化规约。
// 聚集RDD中的所有元素,先聚合分区内数据,再聚合分区间数据
// 聚集 : 由多变少
val rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
val result1: Int = rdd1.reduce(_ + _)
println(result1) // 10
// ============== collect ================
/* 函数签名 :
TODO
def collect(): Array[T]
*/
// collect : 采集数据。
// 在驱动程序中,以数组Array的形式返回数据集的所有元素
val result2: Array[Int] = rdd1.collect()
println(result2.mkString(", ")) // 1, 2, 3, 4
// ============== count ================
/* 函数签名 :
TODO
def count(): Long
*/
// count : 返回RDD中元素的个数
val result3: Long = rdd1.count()
println(result3) // 4
// ============== first ================
/* 函数签名 :
TODO
def first(): T
*/
// first : 返回RDD中的第一个元素
val result4: Int = rdd1.first()
println(result4) // 1
// ============== take ================
/* 函数签名 :
TODO
def take(num: Int): Array[T]
*/
// take : 返回一个由RDD的前n个元素组成的数组
val result5: Array[Int] = rdd1.take(2)
println(result5.mkString(", ")) // 1, 2
// ============== takeOrdered ================
/* 函数签名 :
TODO
def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T]
*/
// takeOrdered : 返回该RDD排序后的前n个元素组成的数组
val rdd2: RDD[Int] = sc.makeRDD(List(3, 2, 4, 1))
val result6: Array[Int] = rdd2.takeOrdered(3)
println(result6.mkString(", ")) // 1, 2, 3
// ============== aggregate ================
/* 函数签名 :
TODO
def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U
*/
// aggregate : 分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合
// aggregateByKey : 初始值只参与分区内的计算
// aggregate : 初始值参与分区内计算和分区间计算
val rdd3: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
val result7: Int = rdd3.aggregate(0)(_ + _, _ + _)
val result8: Int = rdd3.aggregate(10)(_ + _, _ + _)
println(result7) // 10
println(result8) // 40
// ============== fold ================
/* 函数签名 :
TODO
def fold(zeroValue: T)(op: (T, T) => T): T
*/
// fold : 折叠操作,aggregate的简化版操作
val result9: Int = rdd3.fold(0)(_ + _)
val result10: Int = rdd3.fold(10)(_ + _)
println(result9) // 10
println(result10) // 40
// ============== countByKey ================
/* 函数签名 :
TODO
def countByKey(): Map[K, Long]
*/
// countByKey : 统计相同key的value的个数
val rdd4: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 1), ("a", 2)))
val result11: collection.Map[String, Long] = rdd4.countByKey()
println(result11.mkString(", ")) // a -> 2, b -> 1
// ============== countByValue ================
/* 函数签名 :
TODO
def countByValue()(implicit ord: Ordering[T] = null): Map[T, Long]
*/
// countByValue : 统计RDD中元素的个数
val rdd5: RDD[String] = sc.makeRDD(List("a", "b", "a"))
val result12: collection.Map[(String, Int), Long] = rdd4.countByValue()
val result13: collection.Map[String, Long] = rdd5.countByValue()
println(result12.mkString(", ")) // (a,1) -> 1, (b,1) -> 1, (a,2) -> 1
println(result13.mkString(", ")) // a -> 2, b -> 1
// ============== save相关 ================
/* 函数签名 :
TODO
def saveAsTextFile(path: String): Unit
def saveAsObjectFile(path: String): Unit
def saveAsSequenceFile(
path: String,
codec: Option[Class[_ <: CompressionCodec]] = None): Unit
*/
// saveAsTextFile : 保存为Text文件
// saveAsObjectFile : 序列化成对象保存到文件
// saveAsSequenceFile : 保存成Sequencefile文件
rdd4.saveAsTextFile("output1")
rdd4.saveAsObjectFile("output2")
rdd4.saveAsSequenceFile("output3")
// ============== foreach ================
/* 函数签名 :
TODO
def foreach(f: T => Unit): Unit = withScope {
val cleanF = sc.clean(f)
sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}
*/
// foreach : 分布式遍历RDD中的每一个元素, 调用指定函数
// RDD中的方法称之为算子, 该foreach非普通的方法, 是一种分布式的算子, 结果区内有序, 区间无序
// 算子的逻辑代码时在分布式计算节点Executor执行的
// 方法的逻辑代码时在Driver端执行的
rdd3.collect().foreach(println) // 1, 2, 3, 4 // 此时为方法 : 结果有序
rdd3.foreach(println) // 3, 4, 1, 2 // 此时为算子 : 结果无序
sc.stop()
}
RDD序列化
def main(args: Array[String]): Unit = {
// ======= 闭包检测 =======
// 从计算的角度, 算子以外的代码都是在Driver端执行, 算子里面的代码都是在Executor端执行。
// 那么在Scala的函数式编程中, 就会导致算子内经常会用到算子外的数据, 这样就形成了闭包效果,
// 如果使用的算子外的数据无法序列化, 就意味着无法传值给Executor端执行, 就会发生错误,
// 所以需要在执行任务计算前, 检测闭包内的对象是否可以进行序列化, 这个操作称之为闭包检测。
// ======= 序列化方法和属性 ========
// 从计算的角度, 算子以外的代码都是在Driver端执行, 算子里面的代码都是在Executor端执行
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Serializable")
val sc = new SparkContext(sparkConf)
val rdd: RDD[String] = sc.makeRDD(Array("hello world", "hello spark", "hive", "hello scala"))
val search = new Search("hello")
// search.getMatch1(rdd).collect().foreach(println)
search.getMatch2(rdd).collect().foreach(println)
sc.stop()
}
class Search(query: String) extends Serializable {
def isMatch(s: String): Boolean = {
s.contains(query)
}
// 函数序列化案例
def getMatch1(rdd: RDD[String]): RDD[String] = {
rdd.filter(isMatch)
}
// 属性序列化案例
def getMatch2(rdd: RDD[String]): RDD[String] = {
rdd.filter(x => x.contains(query))
// val q = query
// rdd.filter(x => x.contains(q)) // 可以不序列化
}
}
def main(args: Array[String]): Unit = {
// ========== Kryo序列化框架 ===========
// Java的序列化的序列化能够序列化任何的类。但是比较重(字节多), 序列化后, 对象的提交也比较大。
// Spark处于对性能的考虑, Spar2.0开始支持另外一种Kryo序列化机制。
// Kryo速度是Serializable的10倍。当RDD在Shuffle数据时,
// 简单数据类型、数组和字符串类型已经在Spark内部使用Kryo来序列化。
val sparkConf: SparkConf = new SparkConf()
.setAppName("SerDemo")
.setMaster("local[*]")
// 替换默认的序列化机制
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
// 注册需要使用 kryo 序列化的自定义类
.registerKryoClasses(Array(classOf[Searcher]))
val sc = new SparkContext(sparkConf)
val rdd: RDD[String] = sc.makeRDD(Array("hello world", "hello scala", "scala", "spark"), 2)
val searcher = new Searcher("hello")
val result: RDD[String] = searcher.getMatch1(rdd)
result.collect().foreach(println)
sc.stop()
}
case class Searcher(query: String) {
def isMatch(s: String): Boolean = {
s.contains(query)
}
// 函数序列化
def getMatch1(rdd: RDD[String]): RDD[String] = {
rdd.filter(isMatch)
}
// 属性序列化
def getMatch2(rdd: RDD[String]): RDD[String] = {
rdd.filter(x => x.contains(query))
// val q = query
// rdd.filter(x => x.contains(q)) // 可以不序列化
}
}
RDD依赖关系
def main(args: Array[String]): Unit = {
// ======= RDD血缘关系 =========
// RDD只支持粗粒度转换, 即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,
// 以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,
// 当该RDD的部分分区数据丢失时, 它可以根据这些信息来重新运算和恢复丢失的数据分区。
// ======= RDD依赖关系 ======
// 所谓的依赖关系, 就是RDD之间的关系
// RDD 窄依赖 : 表示每一个父RDD的Partition最多被子RDD的一个Partition使用 (多对一)
// NarrowDependency
// RDD 宽依赖 : 表示同一个父RDD的Partition被多个子RDD的Partition依赖, 会引起Shuffle (多对多)
// Dependency
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Dependency")
val sc = new SparkContext(sparkConf)
val rdd: RDD[String] = sc.makeRDD(List("Hello Scala", "Hello Spark", "Spark Scala"), 2)
println("------- 血缘关系 --------")
println(rdd.toDebugString)
println("------- 依赖关系 --------")
println(rdd.dependencies)
val newRDD1: RDD[String] = rdd.flatMap(_.split(" "))
println("------- 血缘关系 --------")
println(newRDD1.toDebugString)
println("------- 依赖关系 --------")
println(newRDD1.dependencies)
val newRDD2: RDD[(String, Iterable[String])] = newRDD1.groupBy(word => word)
println("------- 血缘关系 --------")
println(newRDD2.toDebugString)
println("------- 依赖关系 --------")
println(newRDD2.dependencies)
val result: RDD[(String, Int)] = newRDD2.map(kv => (kv._1, kv._2.size))
println("------- 血缘关系 --------")
println(result.toDebugString)
println("------- 依赖关系 --------")
println(result.dependencies)
println("------- 结果 --------")
result.collect().foreach(println)
sc.stop()
}
RDD阶段划分
DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。例如,DAG记录了RDD的转换过程和任务的阶段。
阶段的个数取决于数据落盘/shuffle的次数。阶段数 = shuffle依赖数 + 1
RDD任务划分
RDD任务切分中间分为:Application、Job、Stage和Task
Application:初始化一个SparkContext即生成一个Application;
Job:一个Action算子就会生成一个Job;
Stage:Stage等于宽依赖(ShuffleDependency)的个数加1;
Task:一个Stage阶段中,最后一个RDD的分区个数就是Task的个数。
Application->Job->Stage->Task每一层都是1对n的关系。
RDD持久化
RDD Cache缓存
def main(args: Array[String]): Unit = {
// ======= RDD持久化 ========
// 由于RDD中是不保存数据的, 如果多个RDD需要共享其中的一个RDD的数据, 那么必须从头执行, 效率很低,
// 所以如果将一些重复性比较高, 比较耗时的操作的结果缓存起来, 这样就可以大大的提高效率。
// ======= RDD Cache缓存 =======
// RDD通过Cache或者Persist方法将前面的计算结果缓存,默认情况下会把数据以序列化的形式缓存在JVM的堆内存中。
// 但是并不是这两个方法被调用时立即缓存,而是触发后面的action算子时,该RDD将会被缓存在计算节点的内存中,并供后面重用
// > 默认的缓存是存储在Executor端的内存中, Cache底层其实是调用的persist方法
// > persist方法在持久化数据时会采用不同的存储级别对数据进行持久化操作
// > cache缓存的默认操作就是将数据缓存到内存MEMORY_ONLY
// > cache存储的数据在内存中, 如果内存不够用, executor可以将内存的数据进行整理并丢弃部分数据
// 如果由于executor端整理内存导致缓存的数据丢失, 那么数据操作依然要从头执行
// 如果cache后的数据从头执行数据操作的话, 那么必须要遵循血缘关系, 所以cache操作不能删除血缘关系
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Cache")
val sc = new SparkContext(sparkConf)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
rdd.cache()
// cache操作在行动算子执行后, 会在血缘关系中增加和缓存相关的依赖
// cache操作不会切断血缘, 一旦发生错误, 可以重新执行
val cacheRDD = rdd.cache()
println(cacheRDD.toDebugString)
println(cacheRDD.collect().mkString(", "))
println(cacheRDD.toDebugString)
sc.stop()
}
RDD CheckPoint
def main(args: Array[String]): Unit = {
// ======= RDD CheckPoint =======
// 所谓的检查点其实就是通过将RDD中间结果写入磁盘
//由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,
// 如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销。
//对RDD进行checkpoint操作并不会马上被执行,必须执行Action操作才能触发。
// 检查点操作会切断血缘关系, 一旦数据丢失不会从头读取数据
// 检查点可以将数据保存到分布式存储系统中, 数据相对来说比较安全, 不易丢失
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Cache")
val sc = new SparkContext(sparkConf)
// 设置检查点路径
sc.setCheckpointDir("./checkpoint1")
// 创建RDD
val rdd: RDD[String] = sc.textFile("input/word1.txt")
// 业务逻辑
val newRDD1: RDD[String] = rdd.flatMap(_.split(" "))
val newRDD2: RDD[(String, Long)] = newRDD1.map((_, System.currentTimeMillis()))
// 增加缓存
newRDD2.cache()
// 数据检查点, 针对newRDD2做检查点计算
newRDD2.checkpoint()
// 触发执行逻辑
newRDD2.collect().foreach(println)
sc.stop()
}
缓存和检查点区别
1)Cache缓存只是将数据保存起来,不切断血缘依赖。Checkpoint检查点切断血缘依赖。
2)Cache缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint的数据通常存储在HDFS等容错、高可用的文件系统,可靠性高。
3)建议对checkpoint()的RDD使用Cache缓存,这样checkpoint的job只需从Cache缓存中读取数据即可,否则需要再从头计算一次RDD。
RDD文件读取与保存
def main(args: Array[String]): Unit = {
// ======= RDD文件读取与保存 =======
// Spark的数据读取及数据保存可以从两个维度来区分 : 文件格式 & 文件系统
// 文件格式 : text文件、csv文件、sequence文件、Object文件
// 文件系统 : 本地文件系统、HDFS、HBASE、数据库
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Cache")
val sc = new SparkContext(sparkConf)
// text文件
// 读取
val rdd1: RDD[String] = sc.textFile("input/word1.txt")
// 保存
rdd1.saveAsTextFile("output1")
// sequence文件
// SequenceFile文件是Hadoop用来存储二进制形式的key-value对而设计的一种平面文件(Flat File)。
// 在SparkContext中,可以调用sequenceFile[keyClass, valueClass](path)
val rdd2: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3)))
// 保存数据为sequenceFile
rdd2.saveAsSequenceFile("output2")
// 读取sequenceFile
sc.sequenceFile[String, Int]("output2")
// object对象文件
// 对象文件是将对象序列化后保存的文件,采用Java的序列化机制。
// 可以通过objectFile[T: ClassTag](path)函数接收一个路径,读取对象文件,
// 返回对应的RDD,也可以通过调用saveAsObjectFile()实现对对象文件的输出。
// 因为是序列化所以要指定类型
// 保存数据
rdd1.saveAsObjectFile("output3")
// 读取数据
sc.objectFile[String]("output3")
sc.stop()
}
累加器
def main(args: Array[String]): Unit = {
// ======= 累加器 Accumulator =======
// 累加器用来把Executor端变量信息聚合到Driver端。在Driver程序中定义的变量,
// 在Executor端的每个Task都会得到这个变量的一份新的副本,每个task更新这些副本的值后,
// 传回Driver端进行merge
// ====== 系统累加器 =======
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Cache")
val sc = new SparkContext(sparkConf)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
// 声明累加器
val sum: LongAccumulator = sc.longAccumulator("sum")
rdd.foreach(
num => {
// 使用累加器
sum.add(num)
}
)
// 获取累加器的值
println("sum = " + sum.value)
// ======= 自定义累加器 ========
val rdd2: RDD[String] = sc.makeRDD(List("hello scala", "hello spark"))
// 1. 创建累加器
val acc = new MyAccumulator()
// 2. 注册累加器
sc.register(acc)
// 3. 使用累加器
rdd2.flatMap(_.split(" ")).foreach(acc.add)
// 4. 获取累加器的返回值
println(acc.value)
sc.stop()
}
// 自定义累加器
// 继承AccumulatorV2
// 定义泛型 [In, Out]
// In : 输入值的类型
// Out : 返回值的类型
// 重写方法
class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Int]] {
// 存储WordCount的集合
var wordCountMap: mutable.Map[String, Int] = mutable.Map[String, Int]()
// 判断累加器是否为初始状态
override def isZero: Boolean = {wordCountMap.isEmpty}
// 赋值累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {new MyAccumulator}
// 重置累加器
override def reset(): Unit = {wordCountMap.clear()}
// 逻辑
override def add(word: String): Unit = {
// wordCountMap(word) = wordCountMap.getOrElse(word, 0) + 1
wordCountMap.update(word, wordCountMap.getOrElse(word, 0) + 1)
}
// 合并累加器
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
val map1 = wordCountMap
val map2 = other.value
// 两个Map的合并
wordCountMap = map1.foldLeft(map2)(
( innerMap, kv ) => {
innerMap(kv._1) = innerMap.getOrElse(kv._1, 0) + kv._2
innerMap
}
)
}
// 返回累加器的值(Out)
override def value: mutable.Map[String, Int] = wordCountMap
}
广播变量
def main(args: Array[String]): Unit = {
// ======== 广播变量 - broadcast =======
// 广播变量 : 分布式共享只读变量
// 当Executor中有多个Task, 且拥有相同的数据, 那么会造成Executor中数据的冗余, 性能降低
// 此时可以采用广播变量的方式, 将共有的数据保存到Executor的缓存区中
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("BroadCast")
val sc = new SparkContext(sparkConf)
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3), ("d", 4)), 4)
val list = List(("a", 4), ("b", 5), ("c", 6), ("d", 7))
// 声明广播变量
val bcList: Broadcast[List[(String, Int)]] = sc.broadcast(list)
rdd.map{
case (key, num) => {
var num2 = 0
// 使用广播变量
for((k, v) <- bcList.value){
if(k == key) {
num2 = v
}
}
(key, (num, num2))
}
}.collect().foreach(println)
sc.stop()
}