SparkCore-基础算子

一、Spark基础架构

Apache Spark是用于大规模数据(large-scala data)处理的统一分析引擎。

1.Spark是一款分布式内存计算的统一分析引擎。
2.其特点就是对任意类型的数据进行自定义计算。
3.Spark可以计算:结构化、半结构化、非结构化等各种类型的数据结构,同时也支持使用Python、Java、Scala、R以及SQL语言去开发应用程序计算数据。
4.Spark的适用面非常广泛,所以,被称之为 统一的(适用面广)的分析引擎(数据处理)

1.1 Spark VS Hadoop

HadoopSpark
类型基础平台(计算,存储,调度)纯计算工具(分布式)
场景海量数据批处理(磁盘迭代)海量数据的批处理(内存迭代计算、交互式计算)、海量数据流计算(流计算方面逐渐被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 环境中,一个程序的执行流程:

  1. Yarn中的节点信息,此处ResourceManager=Master,NodeManager=Worker
    在这里插入图片描述

  2. 用户提交程序后,便会在对应节点启动一个对应的driver进程,driver进程会向集群管理者ResourceMannager(Yarn)申请程序所需资源,也就是executor,然后集群管理者会根据spark应用所设置的参数在各个worker上分配一定数量的executor
    在这里插入图片描述

  3. driver进程会将我们编写的spark应用代码拆分成多个stage,每个stage执行一部分代码片段,并为每个stage创建一批tasks,然后将这些tasks分配到各个executor中执行(在driver端进行程序的阶段划分)。
    在这里插入图片描述

  4. 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 样例类提供了序列化,继承了序列化的特质,因此使用起来很方便。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aimyon_36

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值