Spark基础和算子
- 一、Spark基础架构
- 二、RDD设计和运行原理
- 三、基础编程
- 3.1 RDD的创建
- 3.2 RDD并行度与分区
- 3.3 Transformation算子
- map算子
- flatMap算子
- reduceByKey算子
- aggregateByKey算子
- foldByKey算子
- combineByKey算子
- mapValues算子
- cogroup算子
- groupBy算子
- filter算子
- sampple算子
- distinct算子
- join算子
- leftOuterJoin算子
- intersection&union&subtract&zip
- glom算子
- groupByKey算子
- sortBy算子
- sortByKey算子
- mapPartitions分区操作算子
- mapPartitionsWithIndex操作指定分区算子
- partitionBy分区操作算子
- repartition分区操作算子
- coalease分操作算子
- 3.4 Action算子
- 四.重要理解
一、Spark基础架构
Apache Spark是用于大规模数据(large-scala data)处理的统一分析引擎。
1.Spark是一款分布式内存计算的统一分析引擎。
2.其特点就是对任意类型的数据进行自定义计算。
3.Spark可以计算:结构化、半结构化、非结构化等各种类型的数据结构,同时也支持使用Python、Java、Scala、R以及SQL语言去开发应用程序计算数据。
4.Spark的适用面非常广泛,所以,被称之为 统一的(适用面广)的分析引擎(数据处理)
1.1 Spark VS Hadoop
Hadoop | Spark | |
---|---|---|
类型 | 基础平台(计算,存储,调度) | 纯计算工具(分布式) |
场景 | 海量数据批处理(磁盘迭代) | 海量数据的批处理(内存迭代计算、交互式计算)、海量数据流计算(流计算方面逐渐被Flink代替) |
成本 | 低 | 对内存有要求 |
编程范式 | Map+Reduce, API 较为底层, 算法适应性差 | RDD组成DAG有向无环图, API 较为顶层, 方便使用 |
存储结构 | MapReduce中间计算结果在HDFS磁盘上, 延迟大 | RDD中间运算结果在内存中 , 延迟小 |
运行方式 | Task以进程方式维护, 任务启动慢 | Task以线程方式维护, 任务启动快,可批量创建提高并行能力 |
Hadoop的基于进程的计算和Spark基于线程方式优缺点?
Hadoop中的MR中每个map/reduce task都是一个java进程方式运行好处在于进程之间是互相独立的,每个task独享进程资源,没有互相干扰,监控方便,但是问题在于task之间不方便共享数据,执行效率比较低。
比如多个map task读取不同数据源文件需要将数据源加载到每个map task中,造成重复加载和浪费内存。而基于线程的方式计算是为了数据共享和提高执行效率,Spark采用了线程的最小的执行单位,但缺点是线程之间会有资源竞争。
1.2 Spark运行模式
Spark提供多种运行模式,包括:
本地模式(单机)常用的Windows环境就是本地模式
本地模式就是以一个独立的进程,通过其内部的多个线程来模拟整个Spark运行时环境
Standalone模式(集群)
Spark中的各个角色以独立进程的形式存在,并组成Spark集群环境
Hadoop YARN模式(集群)
Spark中的各个角色运行在YARN的容器内部,并组成Spark集群环境
- Kubernetes模式(容器集群)
Spark中的各个角色运行在Kubernetes的容器内部,并组成Spark集群环境
- 云服务模式(运行在云平台上)
1.3 Spark框架设计
Spark运行架构包括集群资源管理器(Cluster Manager)、运行作业任务的工作节点(Worker Node)、每个应用的任务控制节点(Driver Program,或简称为Driver)和每个工作节点上负责具体任务的执行进程(Executor)。其中,集群资源管理器可以是 Spark 自带的资源管理器,也可以是YARN或Mesos等资源管理框架。可以看出,就系统架构而言,Spark采用“主从架构”,包含一个Master(即Driver)和若干个Worker。
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 应用的任务,并将结果返回给驱动器进程(Driver)
➢ 它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD 是直接缓存在 Executor 进程内的,因此任务可以在运行时充分利用缓存数据加速运算。
Executor中有一个BlockManager存储模块,会将内存和磁盘共同作为存储设备(默认使用内存,当内存不够时,会写到磁盘),当需要多轮迭代计算时,可以将中间结果存储到这个存储模块里,下次需要时,就可以直接读取该存储模块里的数据,而不需要读取HDFS等文件系统的数据,因而有效减少了I/O开销,或者在交互式查询场景下,预先将表缓存到该存储系统上,从而可以提高读写I/O性能。
总体而言,在Spark中,一个应用(Application)由一个任务控制节点(Driver)和若干个作业(Job)构成,一个作业由多个阶段(Stage)构成,一个阶段由多个任务(Task)组成。当执行一个应用时,任务控制节点Driver会向集群管理器(Cluster Manager)申请资源,启动Executor,并向Executor发送应用程序代码和文件,然后在Executor上执行任务,运行结束后,执行结果会返回给任务控制节点Driver,写到HDFS或者其他数据库中。
有向无环图(DAG)
大数据计算引擎框架我们根据使用方式的不同一般会分为四类,其中第一类就是Hadoop 所承载的 MapReduce,它将计算分为两个阶段,分别为 Map 阶段 和 Reduce 阶段。对于上层应用来说,就不得不想方设法去拆分算法,甚至于不得不在上层应用实现多个 Job 的串联,以完成一个完整的算法,例如迭代计算。 由于这样的弊端,催生了支持 DAG 框架的产生。因此,支持 DAG 的框架被划分为第二代计算引擎。如 Tez 以及更上层的Oozie。这里我们不去细究各种 DAG 实现之间的区别,不过对于当时的 Tez 和 Oozie 来说,大多还是批处理的任务。接下来就是以 Spark 为代表的第三代的计算引擎。第三代计算引擎的特点主要是 Job 内部的 DAG 支持(不跨越 Job),以及实时计算。这里所谓的有向无环图,并不是真正意义的图形,而是由 Spark 程序直接映射成的数据流的高级抽象模型。简单理解就是将整个程序计算的执行过程用图形表示出来,这样更直观,更便于理解,可以用于表示程序的拓扑结构。
DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。
1.4 Spark运行基本流程
下图为一个Spark程序读取HDFS上的数据进行操作后再次写入HDFS的示例图:
工作流程大致可以分为:
(1)创建RDD对象;
(2)SparkContext负责计算RDD之间的依赖关系,构建DAG;
(3)DAGScheduler 负责把 DAG 图分解成多个阶段,每个阶段中包含了多个任务,每个任务会被任务调度器分发给各个工作节点(Worker Node)上的Executor去执行。
下边介绍Spark不同搭建模式下的工作原理
SparkStandalone
(1)当一个Spark应用被提交时,首先需要为这个应用构建起基本的运行环境,即由任务控制节点(Driver)创建一个SparkContext对象,由SparkContext负责和资源管理器(Cluster Manager)的通信以及进行资源的申请、任务的分配和监控等,SparkContext 会向资源管理器注册并申请运行Executor的资源,SparkContext可以看成是应用程序连接集群的通道。
(2)资源管理器为Executor分配资源,并启动Executor进程,Executor运行情况将随着“心跳”发送到资源管理器上。
(3)SparkContext根据RDD的依赖关系构建DAG图,DAG图提交给DAG调度器(DAGScheduler)进行解析,将DAG图分解成多个“阶段”(每个阶段都是一个任务集),并且计算出各个阶段之间的依赖关系,然后把一个个“任务集”提交给底层的任务调度器(TaskScheduler)进行处理;Executor向SparkContext申请任务,任务调度器将任务分发给Executor运行,同时,SparkContext将应用程序代码发放给Executor。
(4)任务在Executor上运行,把执行结果反馈给任务调度器,然后反馈给DAG调度器,运行完毕后写入数据并释放所有资源。
总体而言,Spark运行架构具有以下几个特点。
(1)每个应用都有自己专属的Executor进程,并且该进程在应用运行期间一直驻留。Executor进程以多线程的方式运行任务,减少了多进程任务频繁的启动开销,使得任务执行变得非常高效和可靠。(2)Spark运行过程与资源管理器无关,只要能够获取Executor进程并保持通信即可。
(3)Executor上有一个BlockManager存储模块,类似于键值存储系统(把内存和磁盘共同作为存储设备),在处理迭代计算任务时,不需要把中间结果写入到HDFS等文件系统,而是直接放在这个存储系统上,后续有需要时就可以直接读取;在交互式查询场景下,也可以把表提前缓存到这个存储系统上,提高读写I/O性能。
(4)任务采用了数据本地性和推测执行等优化机制。数据本地性是尽量将计算移到数据所在的节点上进行,即“计算向数据靠拢”,因为移动计算比移动数据所占的网络资源要少得多。而且,Spark采用了延时调度机制,可以在更大的程度上实现执行过程优化。比如,拥有数据的节点当前正被其他的任务占用,那么,在这种情况下是否需要将数据移动到其他的空闲节点呢?答案是不一定。因为,如果经过预测发现当前节点结束当前任务的时间要比移动
Spark on Yarn
从计算的角度来讲,数据处理过程中需要计算资源(内存 & CPU)和计算模型(逻辑)。执行时,需要将计算资源和计算模型进行协调和整合。Spark 框架在执行时,先申请资源,然后将应用程序的数据处理逻辑分解成一个一个的计算任务。然后将任务发到已经分配资源的计算节点上, 按照指定的计算模型进行数据计算。最后得到计算结果。RDD 是 Spark 框架中用于数据处理的核心模型,接下来我们看看,在 Yarn 环境中,一个程序的执行流程:
-
Yarn中的节点信息,此处ResourceManager=Master,NodeManager=Worker
-
用户提交程序后,便会在对应节点启动一个对应的driver进程,driver进程会向集群管理者ResourceMannager(Yarn)申请程序所需资源,也就是executor,然后集群管理者会根据spark应用所设置的参数在各个worker上分配一定数量的executor
-
driver进程会将我们编写的spark应用代码拆分成多个stage,每个stage执行一部分代码片段,并为每个stage创建一批tasks,然后将这些tasks分配到各个executor中执行(在driver端进行程序的阶段划分)。
-
executor进程宿主在worker节点上,一个worker可以有多个executor。每个executor持有一个线程池,每个线程可以执行一个task,executor执行完task以后将结果返回给driver,每个executor执行的task都属于同一个应用。
二、RDD设计和运行原理
Spark 计算框架为了能够进行高并发和高吞吐的数据处理,封装了三大数据结构,用于
处理不同的应用场景。三大数据结构分别是:
➢ RDD : 弹性分布式数据集 (SparkCore中常用)
➢ DataFrames : 数据框架 (SparkSql中常用)
➢ Datasets : 数据集 (SparkStreaming中常用)
2.1 什么是RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,一个RDD就是一个分布式对象集合,本质上是一个只读的分区记录集合,每个RDD可以分成多个分区,每个分区就是一个数据集片段,并且一个RDD的不同分区可以被保存到集群中不同的节点上,从而可以在集群中的不同节点上进行并行计算。RDD是 Spark 中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。
➢ 弹性
⚫ 存储的弹性:内存与磁盘的自动切换;
⚫ 容错的弹性:数据丢失可以自动恢复;
⚫ 计算的弹性:计算出错重试机制;
⚫ 分片的弹性:可根据需要重新分片。
➢ 分布式:数据存储在大数据集群不同节点上
➢ 数据集:RDD 封装了计算逻辑,并不保存数据
➢ 数据抽象:RDD 是一个抽象类,需要子类具体实现
➢ 不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的 RDD,在新的 RDD 里面封装计算逻辑
➢ 可分区、并行计算
2.2 RDD五大特性
1.RDD是有分区的,分区之间互不影响
2.RDD的计算方法会作用在其所有分区上
3.RDD之间具有相互依赖关系(血缘关系),
4.KeyValue型的RDD可以有分区器
5.RDD分区规划,会尽量靠近数据所在的服务器,在确保并行计算能力的前提下,尽量进行本地读取
值得一提的是,RDD采用了惰性调用,即在RDD的执行过程中(见下图),真正的计算发生在RDD的“行动”操作,对于“行动”之前的所有“转换”操作,Spark只是记录下“转换”操作应用的一些基础数据集以及RDD生成的轨迹,即相互之间的依赖关系,而不会触发真正的计算。
例如(见下图),从输入中逻辑上生成A和C两个RDD,经过一系列**“转换**”操作,逻辑上生成了F(也是一个RDD),之所以说是逻辑上,是因为这时候计算并没有发生,Spark 只是记录了RDD之间的生成和依赖关系,也就是得到DAG图。当F要进行计算输出时,也就是当遇到针对F的“行动”操作的时候,Spark才会生成一个作业,向DAG调度器提交作业,触发从起点开始的真正的计算。
上述这一系列处理称为一个“血缘关系(Lineage)”,即DAG拓扑排序的结果。
Tip:血缘关系是非常重要的一个概念
采用惰性调用机制以后,通过血缘关系连接起来的一系列RDD操作就可以实现管道化(pipeline),避免了多次转换操作之间数据同步的等待,而且不用担心有过多的中间数据,因为这些具有血缘关系的操作都管道化了,一个操作得到的结果不需要保存为中间数据,而是直接管道式地流入到下一个操作进行处理。同时,这种通过血缘关系把一系列操作进行管道化连接的设计方式,也使得管道中每次操作的计算变得相对简单,保证了每个操作在处理逻辑上的单一性
正是因为RDD的血缘关系,RDD具备了高效的容错性,即使数据出现错误丢失,只需重新计算就能避免整个系统回滚。
WordCount案例的图解:
RDD的数据只有在调用Action算子时,才会真正执行业务逻辑操作;
RDD调用的Transformation算子都只是在编写执行计划,血缘关系;
三、基础编程
3.1 RDD的创建
在 Spark 中创建 RDD 的创建方式可以分为四种:
1) 从集合(内存)中创建 RDD
从集合中创建 RDD,Spark 主要提供了两个方法:parallelize 和 makeRDD
2) 从外部存储(文件)创建 RDD
由外部存储系统的数据集创建 RDD 包括:本地的文件系统,所有 Hadoop 支持的数据集,比如 HDFS、HBase 等。
3) 从其他 RDD 创建
主要是通过一个 RDD 运算完后,再产生新的 RDD。
4) 直接创建 RDD(new)
使用 new 的方式直接构造 RDD,一般由 Spark 框架自身使用。
object create {
def main(args: Array[String]): Unit = {
// todo 准备环境
val conf: SparkConf = new SparkConf().setAppName("create").setMaster("local[*]")
val sc = new SparkContext(conf)
// todo 创建rdd-内存中创建
// parallize:并行
val seq = Seq[Int](1,2,3,4,5)
// val rdd: RDD[Int] = sc.parallelize(seq)
// makeRDD底层调用的还是parallize方法
val rdd: RDD[Int] = sc.makeRDD(seq)
rdd.collect().foreach(println)
//todo 创建rdd-使用文件创建
// path默认是当前环境的根路径,可以写绝对也可以写相对路径,还能够使用目录读取目录下所有文件
// val rdd2: RDD[String] = sc.textFile("resources/wordcount.txt")
val rdd2: RDD[(String, String)] = sc.wholeTextFiles("resources")
// textFile:以行为单位读取数据
// wholeTextFiles:以文件为单位读取数据
rdd2.collect().foreach(print)
//todo 关闭
sc.stop()
}
}
3.2 RDD并行度与分区
默认情况下,Spark 可以将一个作业切分多个任务后,发送给 Executor 节点并行计算,而能够并行计算的任务数量我们称之为并行度。这个数量可以在构建 RDD 时指定。记住,这里的并行执行的任务数量,并不是指的切分任务的数量,不要混淆了。
object partitions {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
// 第二个参数能够设置数据分区
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
rdd.saveAsTextFile("resources/output")
}
}
⚫ 读取内存数据时,数据可以按照并行度的设定进行数据的分区操作,数据分区规则的Spark 核心源码如下:
//length是数据长度 numSlices是分区数量
def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = {
(0 until numSlices).iterator.map { i =>
val start = ((i * length) / numSlices).toInt
val end = (((i + 1) * length) / numSlices).toInt
(start, end)
}
}
//默认数值,totalCores为当前运行环境最大可用核数
// scheduler.conf.getInt("spark.default.parallelism",totalCores)
//可以在SparkConf().set("spark.default.parallelism",num)设置分区数量
⚫ 读取文件数据时,数据是按照 Hadoop 文件读取的规则进行切片分区,而切片规则和数据读取的规则有些差异
RDD分区之间的执行是无序的,但是分区内的计算是有序的
3.3 Transformation算子
RDD 根据数据处理方式的不同将算子整体上分为 Value 类型、双 Value 类型和
Key-Value类型,转换算子的作用是对将旧RDD包装成新的RDD,对旧RDD 的功能的补充和封装。
map算子
➢ 函数签名
def map[U: ClassTag](f: T => U): RDD[U]
➢ 函数说明
将处理的数据逐条进行映射转换,这里的转换可以是类型的转换,也可以是值的转换。
def main(args: Array[String]): Unit = {
//创建环境
val conf: SparkConf = new SparkConf().setAppName("create").setMaster("local[*]")
val sc = new SparkContext(conf)
//构建RDD
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
//定义函数
def mapFunction(num:Int): Int ={
num* 2 //return num*2
}
val rdd2: RDD[Int] = rdd.map(mapFunction)
//使用函数柯里化
//val rdd2: RDD[Int] = rdd.map(_ * 2)
rdd2.foreach(print) 2468
sc.stop()
}
pyspark
[1,2,3,4,5,6,7,8,9] ->rdd.map(lambda x : x*10)->[10,20,30,40,50,60,70,80,90]
flatMap算子
➢ 函数签名
def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]
➢ 函数说明
将处理的数据进行扁平化后再进行映射处理,所以算子也称之为扁平映射
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("create").setMaster("local[*]")
val sc = new SparkContext(conf)
val rdd = sc.makeRDD(List(List(1,2),3,List(4,5)))
val rdd2: RDD[Any] = rdd.flatMap(data => {
data match {
case list: List[_] => list
case dat => List(dat)
}
})
rdd2.collect().foreach(print) #12345
}
Pyspark
对RDD执行map操作,然后执行解除嵌套操作
rdd =sc.parallelize( ["hadoop spark hadoop","SPARK HADOOP FLINK"])
rdd2 = rdd.flatMap(lambda x:x.split(" "))
["hadoop"," spark" ,"hadoop","SPARK ","HADOOP", "FLINK"]
reduceByKey算子
➢ 函数签名
def reduceByKey(func: (V, V) => V): RDD[(K, V)] def
reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]
➢ 函数说明
可以将数据按照相同的 Key 对 Value 按照设定的规则进行两两聚合
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("c",3),("a",2)))
val dataRDD2 = dataRDD1.reduceByKey(_+_)
# ("a",3),("b",2),("c",3)
val dataRDD3 = dataRDD1.reduceByKey(_+_, 2)
如果RDD中元素的key只有一个,是不会参与运算的
aggregateByKey算子
➢ 函数签名
def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) =>U, combOp: (U, U) => U): RDD[(K, U)]
➢ 函数说明 将数据根据不同的规则进行分区内计算和分区间计算
val dataRDD1 =sparkContext.makeRDD(List(("a",1),("b",2),("c",3)))
val dataRDD2 =dataRDD1.aggregateByKey(0)(_+_,_+_)
#结果:
(a,1)
(b,2)
(c,3)
取出每个分区内相同 key 的最大值然后分区间相加
// TODO : 取出每个分区内相同 key 的最大值然后分区间相加
// aggregateByKey 算子是函数柯里化,存在两个参数列表
// 1. 第一个参数列表中的参数表示初始值
// 2. 第二个参数列表中含有两个参数
// 2.1 第一个参数表示分区内的计算规则
// 2.2 第二个参数表示分区间的计算规则
val rdd =sc.makeRDD(List(
("a",1),("a",2),("c",3),
("b",4),("c",5),("c",6)
),2)
// 第一个参数10用于分区内计算
// 0号分区:("a",1),("a",2),("c",3) => (a,10)(c,10)
// 1号分区:("b",4),("c",5),("c",6) => (b,10)(c,10)
val resultRDD =
rdd.aggregateByKey(10)(
(x, y) => math.max(x,y),
(x, y) => x + y
)
resultRDD.collect().foreach(println)
#结果
(b,10)
(a,10)
(c,20)
foldByKey算子
➢ 函数签名
def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]
➢ 函数说明
当分区内计算规则和分区间计算规则相同时,aggregateByKey 就可以简化为 foldByKey
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("a",2),("c",3),("b",4),("c",5),("c",6)))
val dataRDD2 = dataRDD1.foldByKey(0)(_+_)
#结果
(a,3)
(b,4)
(c,14)
combineByKey算子
➢ 函数签名
def combineByKey[C]( createCombiner: V => C,mergeValue: (C, V) => C,mergeCombiners: (C, C) => C): RDD[(K, C)]
➢ 函数说明
最通用的对 key-value 型 rdd 进行聚集操作的聚集函数(aggregation function)。类似于aggregate(),combineByKey()允许用户返回值的类型与输入不一致。
combineByKey
第一个参数:将相同的key的第一个数据进行结构转换
第二个参数:分区内的计算规则
第三个参数:分区间的计算规则
mapValues算子
针对二元元组RDD,对其内部的二元元组的Value执行map操作
cogroup算子
➢ 函数签名
def cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))]
➢ 函数说明
在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD
val dataRDD1 = sc.makeRDD(List(("a",1),("a",2),("c",3)))
val dataRDD2 = sc.makeRDD(List(("a",1),("c",2),("c",3)))
val rdd: RDD[(String, (Iterable[Int], Iterable[Int]))] = dataRDD1.cogroup(dataRDD2)
rdd.collect().foreach(println)
#结果
(a,(CompactBuffer(1, 2),CompactBuffer(1)))
(c,(CompactBuffer(3),CompactBuffer(2, 3)))
cogroup: connect+group
对RDD中的数据分组然后连接
groupBy算子
➢ 函数签名
def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]
➢ 函数说明
将数据根据指定的规则进行分组, 分区默认不变,但是数据会被打乱重新组合,我们将这样的操作称之为 shuffle。
极限情况下,数据可能被分在同一个分区中,一个组的数据在一个分区中,但是并不是说一个分区中只有一个组
//根据传入的参数对数据进行分组
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
def groupByFunction(num:Int): Int ={
num % 2 //每个元素对2取余,结果一样的数据存放到一个分组
}
val rdd2: RDD[(Int,Iterable[Int])] = rdd.groupBy(groupByFunction)
//group by 遍历每个元素,根据指定的规则对元素进行分组
rdd2.collect().foreach(println)
结果:
(0,CompactBuffer(2, 4))
(1,CompactBuffer(1, 3))
Pyspark
filter算子
➢ 函数签名
def filter(f: T => Boolean): RDD[T]
➢ 函数说明
将数据根据指定的规则进行筛选过滤,符合规则的数据保留,不符合规则的数据丢弃。当数据进行筛选过滤后,分区不变,但是分区内的数据可能不均衡,生产环境下,可能会出现数据倾斜。
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("create").setMaster("local[*]")
val sc = new SparkContext(conf)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6))
val rdd2: RDD[Int] = rdd.filter(_ % 2 == 0)
rdd2.collect().foreach(println)
}
结果:
2
4
6
sampple算子
➢ 函数签名 def sample(
withReplacement: Boolean,
fraction: Double, seed: Long = Utils.random.nextLong): RDD[T]
➢ 函数说明 根据指定的规则从数据集中抽取数据,设置随机数种子是,无论执行几次结果都一样
val dataRDD = sparkContext.makeRDD(List(1,2,3,4),1)
// 第一个参数:抽取的数据是否放回,false:不放回
// 第二个参数:抽取的几率,范围在[0,1]之间,0:全不取;1:全取;
// 第三个参数:随机数种子
val dataRDD1 = dataRDD.sample(false, 0.5)
// 抽取数据放回(泊松算法)
// 第一个参数:抽取的数据是否放回,true:放回;false:不放回
// 第二个参数:重复数据的几率,范围大于等于 0.表示每一个元素被期望抽取到的次数
// 第三个参数:随机数种子
val dataRDD2 = dataRDD.sample(true, 2)
distinct算子
➢ 函数签名
def distinct()(implicit ord: Ordering[T] = null): RDD[T]
def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
➢ 函数说明 将数据集中重复的数据去重
val dataRDD = sparkContext.makeRDD(List(1,2,3,4,1,2),1)
val dataRDD1 = dataRDD.distinct()
val dataRDD2 = dataRDD.distinct(2)
底层实现:
map(x=>(x,null)).reduceByKey((x,_)=>x,numPartitions).map(_._1)
join算子
➢ 函数签名
def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]
➢ 函数说明
在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素连接在一起的(K,(V,W))的 RDD
val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (2, "b"), (3, "c"),(1,"aimyon")))
val rdd1: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (3, 6)))
rdd.join(rdd1).collect().foreach(println)
#结果:
(1,(a,4))
(1,(aimyon,4))
(2,(b,5))
(3,(c,6))
该算子类似inner join 如果两个数据源中存在多个相同的key,会依次匹配,产生笛卡尔积
leftOuterJoin算子
➢ 函数签名
def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]
➢ 函数说明
类似于 SQL 语句的左外连接,左表的数据需要全部进行加载显示,匹配不上的显示None
val dataRDD1 = sc.makeRDD(List(("a",1),("b",2),("c",3)))
val dataRDD2 = sc.makeRDD(List(("a",4),("b",5)))
val rdd: RDD[(String, (Int, Option[Int]))] = dataRDD1.leftOuterJoin(dataRDD2)
#结果
(a,(1,Some(4)))
(b,(2,Some(5)))
(c,(3,None))
intersection&union&subtract&zip
➢ 函数签名
def intersection(other: RDD[T]): RDD[T] #交集
def union(other: RDD[T]): RDD[T] #并集
➢ 函数说明
对源 RDD 和参数 RDD 求交集后返回一个新的 RDD
➢ 函数签名
def subtract(other: RDD[T]): RDD[T]#差集
➢ 函数说明
以一个 RDD 元素为主,去除两个 RDD 中重复元素,将其他元素保留下来。求差集
➢ 函数签名
def zip[U: ClassTag](other: RDD[U]): RDD[(T, U)] #拉链
➢ 函数说明
将两个 RDD中的元素,以键值对的形式进行合并。其中,键值对中的 Key 为第 1 个 RDD 中的元素,Value 为第 2 个 RDD中的相同位置的元素。
val dataRDD1 = sc.makeRDD(List(1,2,3,4))
val dataRDD2 = sc.makeRDD(List(3,4,5,6))
val intersectRDD = dataRDD1.intersection(dataRDD2) //交集
println(intersectRDD.collect().mkString(",")) #3,4
val unionRDD = dataRDD1.union(dataRDD2)//并集
println(unionRDD.collect().mkString(","))# 1,2,3,4,3,4,5,6
val subRDD: RDD[Int] = dataRDD1.subtract(dataRDD2)//差集
println(subRDD.collect().mkString(","))#1,2
val zipRDD: RDD[(Int, Int)] = dataRDD1.zip(dataRDD2)//拉链
println(zipRDD.collect().mkString(","))#(1,3),(2,4),(3,5),(4,6)
交集,差集,并集要求两个RDD的数据类型一致
拉链要求两个RDD分区数量相同并且每个分区中的元素类型相同
glom算子
➢ 函数签名
def glom(): RDD[Array[T]]
➢ 函数说明
将同一个分区的数据直接转换为相同类型的内存数组进行处理,分区不变
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
//一个list数组两个分区,每个分区中的数据是Int类型
val rdd2: RDD[Array[Int]] = rdd.glom()
//将同一个分区的数据转换为Array[Int]数组
rdd2.collect().foreach(data => println(data.mkString(",")))
//使用collect将excutor端数据拉取到driver,得到两个Arrary
//使用foreach算子对Arrary数据使用mkString(将iter中的元素根据分隔符连接成str)
结果:
1,2
3,4
groupByKey算子
➢ 函数签名
def groupByKey(): RDD[(K, Iterable[V])]
def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])]
def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])]
➢ 函数说明
将数据源的数据根据 key 对 value 进行分组
val rdd = sc.makeRDD(List(("a",2),("a",3),("a",1),("b",2),("b",1),("c",3)))
val rdd2: RDD[(String, Iterable[Int])] = rdd.groupByKey()
rdd2.collect().foreach(println)
#结果
(a,CompactBuffer(2, 3, 1))
(b,CompactBuffer(2, 1))
(c,CompactBuffer(3))
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 的分区数一致。中间存在 shuffle 的过程
val dataRDD = sparkContext.makeRDD(List(1,2,3,4,1,2),2)
val dataRDD1 = dataRDD.sortBy(num=>num, false, 4)
参数1:函数,表示按照数据的哪一个列进行排序
参数2:true表示升序,false表示降序
参数3:分区数(如果要全局有序,分区数设置为1)
sortByKey算子
mapPartitions分区操作算子
➢ 函数签名
def mapPartitions[U: ClassTag](f: Iterator[T] => Iterator[U], preservesPartitioning: Boolean = false): RDD[U]
➢ 函数说明
将待处理的数据以分区为单位发送到计算节点进行处理,这里的处理是指可以进行任意的处理,哪怕是过滤数据。
求出每个分区的最大值
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("create").setMaster("local[*]")
val sc = new SparkContext(conf)
val rdd = sc.makeRDD(List(1,2,3,4),2)
//
val rdd2: RDD[Int] = rdd.mapPartitions(iter => {
List(iter.max).iterator
})
rdd2.collect().foreach(println)
}
不同于map,以迭代器的形式传递一个分区的数据,能够以分区为单位进行数据转换操作,但是会将整个分区的数据加载到内存进行引用,即使处理完也不会释放掉资源,因为其存在对象的引用。
map算子的高配版,能减少网络IO次数;使用时需要担心一个分区的数据是否会超过一个executoe的内存。
mapPartitionsWithIndex操作指定分区算子
➢ 函数签名
def mapPartitionsWithIndex[U: ClassTag]( f: (Int, Iterator[T]) => Iterator[U], preservesPartitioning: Boolean = false): RDD[U]
➢ 函数说明
将待处理的数据以分区为单位发送到计算节点进行处理,这里的处理是指可以进行任意的处理,哪怕是过滤数据,在处理时同时可以获取当前分区索引。
//获取分区为1的数据
val rdd3 = rdd.mapPartitionsWithIndex(
(index,iter)=>{ //mapPartitionWithIndex获得的是(分区索引,分区数据迭代器)
if (index == 1){
iter
}else{
Nil.iterator //如果该分区不是第一分区,返回空迭代器
}
}
)
partitionBy分区操作算子
➢ 函数签名
def partitionBy(partitioner: Partitioner): RDD[(K, V)]
➢ 函数说明
将数据按照指定 Partitioner 重新进行分区。要求RDD中的数据类型为(K,V)。Spark 默认的分区器是 HashPartitioner
val rdd: RDD[(Int, String)] =sc.makeRDD(Array((1,"aaa"),(2,"bbb"),(3,"ccc")),3)
import org.apache.spark.HashPartitioner
val rdd2: RDD[(Int, String)] = rdd.partitionBy(new HashPartitioner(2))
HashPartitioner:根据RDD中元素的Hash值进行分区
如果重分区的分区器和当前RDD的分区器一样,会判断分区的数量是否一样,如果一样将会抛出异常。
repartition分区操作算子
➢ 函数签名
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
➢ 函数说明
该操作内部其实执行的是 coalesce 操作,参数 shuffle 的默认值为 true。无论是将分区数多的RDD 转换为分区数少的 RDD,还是将分区数少的 RDD 转换为分区数多的 RDD,repartition操作都可以完成,因为无论如何都会经 shuffle 过程。
val dataRDD = sparkContext.makeRDD(List(1,2,3,4,1,2),2)
val dataRDD1 = dataRDD.repartition(4)
coalease分操作算子
➢ 函数签名
def coalesce(numPartitions: Int, shuffle: Boolean = false, partitionCoalescer: Option[PartitionCoalescer] = Option.empty) (implicit ord: Ordering[T] = null) : RDD[T]
➢ 函数说明
根据数据量缩减分区,用于大数据集过滤后,提高小数据集的执行效率当 spark 程序中,存在过多的小任务的时候,可以通过 coalesce 方法,收缩合并分区,减少分区的个数,减小任务调度成本
val dataRDD = sparkContext.makeRDD(List(1,2,3,4,1,2),6)
val dataRDD1 = dataRDD.coalesce(2,true/false)//是否使用Shuffle
#coleasce算子默认情况下是不会将分区数据打乱重组的(不存在一个分区的数据被分配到两个分区中)
这种方式能够避免Shuffle的出现,但是有可能会造成数据倾斜
推荐使用coalease分区操作,其有shuffle安全机制,在减小分区时较安全,在增加分区是需要使用Shuffle
3.4 Action算子
Action算子会触发作业的执行,一个程序中拥有几个Action算子就会产生几个job。
aggregate算子
➢ 函数签名
def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U
➢ 函数说明
分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 4)
// 将该 RDD 所有元素相加得到结果
val result: Int = rdd.aggregate(10)(_ + _, _ + _) # 60
aggregateByKey:初始值只会参与分区内计算
aggregate:初始值参与分区内和分区间计算
countByKey&countByValue算子
➢ 函数签名
def countByKey(): Map[K, Long]
➢ 函数说明
统计每种 key 的个数
val rdd = sc.makeRDD(List(1, 2, 3, 4), 2)
val result: collection.Map[Int, Long] = rdd.countByValue()
val rdd2: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("a", 1), ("a", 1), ("b", 1), ("c", 1), ("b", 1)), 2)
val result2: collection.Map[String, Long] = rdd2.countByKey()
println(result) #Map(4 -> 1, 2 -> 1, 1 -> 1, 3 -> 1)
print(result2) #Map(b -> 2, a -> 3, c -> 1)
collect算子
➢ 函数签名
def collect(): Array[T]
➢ 函数说明
在驱动程序中,以数组 Array 的形式返回数据集的所有元素
collect()将不同分区的数据按照分区顺序采集到Driver端内存中,形成数组
ruduce算子
➢ 函数签名
def reduce(f: (T, T) => T): T
➢ 函数说明
聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据
fold算子
➢ 函数签名
def fold(zeroValue: T)(op: (T, T) => T): T
➢ 函数说明
折叠操作,aggregate 的简化版操作
count算子
➢ 函数签名
def count(): Long
➢ 函数说明
返回 RDD 中元素的个数
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 返回 RDD 中元素的个数
val countResult: Long = rdd.count()
println(countResult)# 4
takeSample算子
takeOrdered算子
➢ 函数签名
def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T]
➢ 函数说明
返回该 RDD 排序后的前 n 个元素组成的数组
val rdd: RDD[Int] = sc.makeRDD(List(1,3,2,4))
// 返回 RDD 中元素的个数
val result: Array[Int] = rdd.takeOrdered(2) # 1 2
foreach算子
def foreach(f: T => Unit): Unit = withScope {
val cleanF = sc.clean(f)
sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}
➢ 函数说明
分布式遍历 RDD 中的每一个元素,调用指定函数
rdd.foreach(println)是由executor直接输出的,没有经过driver的数据拉取。(数据的输出是无序的)
rdd.collect().foreach(println)通过collect将分区数据拉取到Driver端后进行foreach循环遍历(数据按分区输出)
foreachPartition分区操作算子
save相关算子
➢ 函数签名
def saveAsTextFile(path: String): Unit
def saveAsObjectFile(path: String): Unit
def saveAsSequenceFile(
path: String,
codec: Option[Class[_ <: CompressionCodec]] = None): Unit
➢ 函数说明
将数据保存到不同格式的文件中
// 保存成 Text 文件
rdd.saveAsTextFile("output")
// 序列化成对象保存到文件
rdd.saveAsObjectFile("output1")
// 保存成 Sequencefile 文件
rdd.map((_,1)).saveAsSequenceFile("output2")
四.重要理解
1.groupByKey和reduceByKey的区别
从 shuffle 的角度:reduceByKey 和 groupByKey 都存在 shuffle 的操作,但是 reduceByKey可以在 shuffle 前对分区内相同 key 的数据进行预聚合(combine)功能,这样会减少落盘的数据量,而 groupByKey 只是进行分组,不存在数据量减少的问题,reduceByKey 性能比较高。
从功能的角度:reduceByKey 其实包含分组和聚合的功能。GroupByKey 只能分组,不能聚合,所以在分组聚合的场合下,推荐使用 reduceByKey,如果仅仅是分组而不需要聚合。那么还是只能使用groupByKey
2. reduceByKey、foldByKey、aggregateByKey、combineByKey 的区别?
reduceByKey: 相同 key 的第一个数据不进行任何计算,分区内和分区间计算规则相同
foldByKey: 相同 key的第一个数据和初始值进行分区内计算,分区内和分区间计算规则相同
aggregateByKey:相同 key的第一个数据和初始值进行分区内计算,分区内和分区间计算规则可以不相同,当分区内和分区间的计算规则相同时可以使用foldByKey
combineByKey:当计算时,发现数据结构不满足要求时,可以让第一个数据转换结构。分区内和分区间计算规则不相同
3.map和mapPartitions的区别
➢ 数据处理角度
Map 算子是分区内一个数据一个数据的执行,类似于串行操作。而 mapPartitions 算子是以分区为单位进行批处理操
作。
➢ 功能的角度
Map 算子主要目的将数据源中的数据进行转换和改变。但是不会减少或增多数据。MapPartitions 算子需要传递一个迭代器,返回一个迭代器,没有要求的元素的个数保持不变,所以可以增加或减少数据
➢ 性能的角度
Map 算子因为类似于串行操作,所以性能比较低,而是 mapPartitions 算子类似于批处理,所以性能较高。但是 mapPartitions 算子会占用内存,那么这样会导致内存可能不够用,出现内存溢出的错误。所以在内存有限的情况下,不推荐使用。使用 map 操作。
4.groupBy和groupByKey的区别
groupBy能够根据自定义的规则进行聚合分组,groupByKey根据数据的key进行聚合;
val rdd: RDD[(String, Int)] = sc.makeRDD(List(
("hello", 1), ("world", 1)
))
//todo groupBy对数据的key和value类型都没有确定:
val rdd2: RDD[(String, Iterable[(String, Int)])] = rdd.groupBy(_._1)
//todo groupByKey已经确定了数据的Key类型:
val rdd3: RDD[(String, Iterable[Int])] = rdd.groupByKey()
5.闭包检测
算子以外的代码都是在Driver端执行,算子内部的代码都是在Executor端执行。
在Scala的函数式编程中,会导致算子内经常使用到算子外的数据,这就形成了闭包的效果。所以在执行程序时,如果算子外的数据无法序列化,就意味着不能通过网络IO无法在Executor端执行,因此在执行程序前会进行闭包内的对象是否可以进行序列化,这个操作称为闭包检测。
Scala中的case class 样例类提供了序列化,继承了序列化的特质,因此使用起来很方便。