一 spark简介
Spark是一种快速、通用、可扩展的大数据分析引擎,2009年诞生于加州大学伯克利分校AMPLab,2010年开源,2013年6月成为Apache孵化项目,2014年2月成为Apache的顶级项目,2014年5月发布spark1.0,2016年7月发布spark2.0,2020年6月18日发布spark3.0.0
1.spark特点
- Ease of Use:简洁易用
- Spark支持 Java、Scala、Python和R等编程语言编写应用程序,大大降低了使用者的门槛。自带了80多个高等级操作算子,并且允许在Scala,Python,R 的使用命令进行交互式运行,可以非常方便的在Spark Shell中地编写spark程序。
- Generality:通用、全栈式数据处理
- Spark提供了统一的大数据处理解决方案,非常具有吸引力,毕竟任何公司都想用统一的平台去处理遇到的问题,减少开发和维护的人力成本和部署平台的物力成本。 同时Spark还支持SQL,大大降低了大数据开发者的使用门槛,同时提供了SparkStream和Structed Streaming可以处理实时流数据;MLlib机器学习库,提供机器学习相关的统计、分类、回归等领域的多种算法实现。其高度封装的API 接口大大降低了用户的学习成本;Spark GraghX提供分布式图计算处理能力;PySpark支持Python编写Spark程序;SparkR支持R语言编写Spark程序。
- Runs Everywhere:可以运行在各种资源调度框架和读写多种数据源
- Spark支持的多种部署方案:Standalone是Spark自带的资源调度模式;Spark可以运行在Hadoop的YARN上面;Spark 可以运行在Mesos上(Mesos是一个类似于YARN的资源调度框架);Spark还可以Kubernetes实现容器化的资源调度
- 丰富的数据源支持。Spark除了可以访问操作系统自身的本地文件系统和HDFS之外,还可以访问 Cassandra、HBase、Hive、Alluxio(Tachyon)以及任何 Hadoop兼容的数据源。这极大地方便了已经 的大数据系统进行顺利迁移到Spark。
2.Spark与MapReduce的对比
MapReduce和Spark的本质区别
- MR只能做离线计算,如果实现复杂计算逻辑,一个MR搞不定,就需要将多个MR按照先后顺序连成一串,一个MR计算完成后会将计算结果写入到HDFS中,下一个MR将上一个MR的输出作为输入,这样就要频繁读写HDFS,网络IO和磁盘IO会成为性能瓶颈。从而导致效率低下。
- 既可以做离线计算,有可以做实时计算,提供了抽象的数据集(RDD、Dataset、DataFrame、DStream)有高度封装的API,算子丰富,并且使用了更先进的DAG有向无环图调度思想,可以对执行计划优化后在执行,并且可以数据可以cache到内存中进行复用。
- 注意:MR和Spark在Shuffle时数据都落本地磁盘
3.spark架构体系
-
三种模式
- standalone client模式
- standalone cluster模式
- Spark On YARN cluster模式
-
Spark执行流程简介
-
Job:RDD每一个行动操作都会生成一个或者多个调度阶段 调度阶段(Stage):每个Job都会根据依赖关系,以Shuffle过程作为划分,分为Shuffle Map Stage和Result Stage。每个Stage对应一个TaskSet,一个Task中包含多Task,TaskSet的数量与该阶段最后一个RDD的分区数相同。
-
Task:分发到Executor上的工作任务,是Spark的最小执行单元
-
DAGScheduler:DAGScheduler是将DAG根据宽依赖将切分Stage,负责划分调度阶段并Stage转成TaskSet提交给TaskScheduler
-
TaskScheduler:TaskScheduler是将Task调度到Worker下的Exexcutor进程,然后丢入到Executor的线程池的中进行执行
-
-
Spark中重要角色
- Master :是一个Java进程,接收Worker的注册信息和心跳、移除异常超时的Worker、接收客户端提交的任务、负责资源调度、命令Worker启动Executor。
- Worker :是一个Java进程,负责管理当前节点的资源管理,向Master注册并定期发送心跳,负责启动Executor、并监控Executor的状态。
- SparkSubmit :是一个Java进程,负责向Master提交任务。
- Driver :是很多类的统称,可以认为SparkContext就是Driver,client模式Driver运行在SparkSubmit进程中,cluster模式单独运行在一个进程中,负责将用户编写的代码转成Tasks,然后调度到Executor中执行,并监控Task的状态和执行进度。
- Executor :是一个Java进程,负责执行Driver端生成的Task,将Task放入线程中运行。
-
Spark和Yarn角色对比(Spark StandAlone的Client模式对比YARN)
- Master => ResourceManager
- Worker => NodeManager
- Executor => YarnChild
- SparkSubmit(Driver) => ApplicationMaster
二 集群搭建及启动
1.下载与安装
-
下载spark,通过国内的镜像网站进行下载(如清华镜像站等)
-
传输到集群中的一台并解压
-
修改conf下的spark-env.sh文件
export JAVA_HOME=/opt/apps/jdk1.8.0_141
export SPARK_MASTER_HOST=linux01 -
修改conf下的slaves,加入worker的设备
2.spark-shell启动
/opt/apps/spark-3.0.1-bin-hadoop3.2/bin/spark-shell --master spark://linux01:7077 --executor-memory 2g --total-executor-cores 4
- 设置的内存是每个worker占用的内存大小
- 设置的核心数是整个集群的全部核心数量
三 spark编程入门
1. scala入门程序
使用Scala编写Spark的WorkCount
-
创建一个maven项目
-
导入依赖等信息
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn._51doit</groupId> <artifactId>spark-in-action</artifactId> <version>1.0-SNAPSHOT</version> <!-- 定义了一些常量 --> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <scala.version>2.12.12</scala.version> <spark.version>3.0.1</spark.version> <encoding>UTF-8</encoding> </properties> <dependencies> <!-- 导入scala的依赖 --> <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>${scala.version}</version> </dependency> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_2.12</artifactId> <version>${spark.version}</version> </dependency> </dependencies> <build> <pluginManagement> <plugins> <!-- 编译scala的插件 --> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <version>3.2.2</version> </plugin> <!-- 编译java的插件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> </plugin> </plugins> </pluginManagement> <plugins> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <executions> <execution> <id>scala-compile-first</id> <phase>process-resources</phase> <goals> <goal>add-source</goal> <goal>compile</goal> </goals> </execution> <execution> <id>scala-test-compile</id> <phase>process-test-resources</phase> <goals> <goal>testCompile</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <executions> <execution> <phase>compile</phase> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> <!-- 打jar插件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.4.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
-
编写main方法
package cn.doit.day01.demo01 import org.apache.spark.rdd.RDD import org.apache.spark.{SparkConf, SparkContext} object WordCount { def main(args: Array[String]): Unit = { //创建SparkContext,只有使用SparkContext才可以向集群申请资源,才可以创建RDD val conf = new SparkConf().setAppName("WordCount") val sc = new SparkContext(conf) //1 创建RDD:指定[以后]从HDFS中读取数据 val lines: RDD[String] = sc.textFile(args(0)) //2 对数据进行切分压平 val words = lines.flatMap(_.split(" ")) //3 将单词和一组合 val wordAndOne = words.map((_, 1)) //4 单词聚合,先局部聚合,再全局聚合,使用reduceByKey val wordAndNums = wordAndOne.reduceByKey(_ + _) //5 输出结果并保存 wordAndNums.saveAsTextFile(args(1)) } }
-
打包
可以使用provided 指定依赖是否打入包中
<dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>${scala.version}</version> <scope>provided</scope> </dependency>
使用 指定main方法
<!-- 指定maven(main)方法 --> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>cn.doit.day01.demo01.WordCount</mainClass> </transformer> </transformers>
-
将jar包导入集群中执行
-
如果不指定main方法,需要指定class,指定类名
–class cn.doit.day01.demo01.WordCount
./bin/spark-submit --master spark://linux01:7077 【--class cn.doit.day01.demo01.WordCount】 /root/spark_learn.jar hdfs://linux01:8020/anli/wordCount/input hdfs://linux01:8020/anli/wordCount/output02
-
2.使用java编写
java编写,并在本地运行
代码如下:
public class WordCount {
public static void main(String[] args) {
//创建连接,设置进程名()
SparkConf conf = new SparkConf().setAppName("JavaWordCount");
//如果在本地运行,设置Master所调用的线程资源数,一般使用local[*],调用全部资源(不能设置为1)
conf.setMaster("local[*]");
//javaSparkContext是对SparkContext的包装类
JavaSparkContext context = new JavaSparkContext(conf);
//设置文件输入路径
JavaRDD<String> rdd = context.textFile(args[0]);
//切分压平
JavaRDD<String> words = rdd.flatMap(new FlatMapFunction<String, String>() {
@Override
public Iterator<String> call(String s) throws Exception {
return Arrays.stream(s.split(" ")).iterator();
}
});
//单词和一结合
JavaPairRDD<String, Integer> wordAndOne = words.mapToPair(new PairFunction<String, String, Integer>() {
@Override
public Tuple2<String, Integer> call(String s) throws Exception {
return Tuple2.apply(s, 1);
}
});
//聚合
JavaPairRDD<String, Integer> wordAndNums = wordAndOne.reduceByKey(new Function2<Integer, Integer, Integer>() {
@Override
public Integer call(Integer integer, Integer integer2) throws Exception {
return integer+integer2;
}
});
//交换顺序
JavaPairRDD<Integer, String> numsAndWord = wordAndNums.mapToPair(new PairFunction<Tuple2<String, Integer>, Integer, String>() {
@Override
public Tuple2<Integer, String> call(Tuple2<String, Integer> stringIntegerTuple2) throws Exception {
return Tuple2.apply(stringIntegerTuple2._2, stringIntegerTuple2._1);
}
});
//排序
JavaPairRDD<Integer, String> sortedNumsAndWord = numsAndWord.sortByKey(false);
//再交换顺序
JavaPairRDD<String, Integer> sortedWordAndNums = sortedNumsAndWord.mapToPair(new PairFunction<Tuple2<Integer, String>, String, Integer>() {
@Override
public Tuple2<String, Integer> call(Tuple2<Integer, String> integerStringTuple2) throws Exception {
return Tuple2.apply(integerStringTuple2._2, integerStringTuple2._1);
}
});
//保存
sortedWordAndNums.saveAsTextFile(args[1]);
//关闭资源
context.stop();
}
}
3.使用Lambda表达式
尝试在本地运行,读写hadoop上的文件
在IDEA设置(传参数):
-
Configurations — Program arguments:
hdfs://linux01:8020/anli/wordCount/input hdfs://linux01:8020/anli/wordCount/output03
public class LambdaWordCount {
public static void main(String[] args) {
//在idea上执行,hadoop上的文件,并输出到hadoop中
//因为windows的权限不够,需要修改调用者名称
System.setProperty("HADOOP_USER_NAME","root");
//结果:权限不足,hadoop权限调整为777,可以运行;或者使用相同方式在scala代码上,可以运行
SparkConf conf = new SparkConf().setAppName("LambdaWordCount").setMaster("local[*]");
JavaSparkContext context = new JavaSparkContext(conf);
JavaRDD<String> lines = context.textFile(args[0]);
JavaRDD<String> words = lines.flatMap(e -> Arrays.stream(e.split(" ")).iterator());
JavaPairRDD<String, Integer> wordAndNum = words.mapToPair(e -> Tuple2.apply(e, 1));
JavaPairRDD<String, Integer> wordAndNums = wordAndNum.reduceByKey(Integer::sum);
JavaPairRDD<Integer, String> numsAndWord = wordAndNums.mapToPair(e -> Tuple2.apply(e._2, e._1));
JavaPairRDD<Integer, String> sortedNumsAndWord = numsAndWord.sortByKey(false);
JavaPairRDD<String, Integer> sortedWordAndNums = sortedNumsAndWord.mapToPair(e -> Tuple2.apply(e._2, e._1));
sortedWordAndNums.saveAsTextFile(args[1]);
context.stop();
}
}
四 RDD详解
RDD:Resilient Distributed Dataset—有弹性的分布式的数据集合;里面没有真正的数据,是一个抽象的、不可变得、被分区的集合,集合内的元素可以被并行的操作(多个Task)。
五大特点:
- 有多个分区,分区数量决定任务并行数
- 函数作用在分区的迭代器中,决定了计算逻辑
- 众RDD之间存在依赖关系,可以根据依赖关系恢复失败的任务和划分stage
- 如果要发生shuffle,要使用分区器,默认使用HashPartitioner
- 最优位置,即将Executor调度到数据所在的节点上,要求Worker和DataNode部署在同一节点或OnYarn,通过访问Namenode获取数据块位置信息
深层理解RDD:
- RDD是一个抽象的数据集,保存的是元数据信息,和对应的运算逻辑
- 即RDD记录了:数据从哪里来、所属Task的分区、对数据作何处理
- 一个Task是由多个RDD相连组成的,而多个RDD之间是由各自的迭代器连接,形成迭代器链,将RDD的对应Task分区连接起来的
- RDD是由,多个各自分区的迭代器(迭代器中记录了数据来源),及其Task的Function(运算逻辑:compute方法),组成的一个大集合
1.Spark中的基础概念
-
Application
- 一个Application只能有一个Executor
- 一个Application可以 一次提交多个Job
-
DAG
- 有向无环图
- 是对多个RDD转换过程和依赖关系的描述
- 触发Action就会形成一个完整的DAG,一个DAG就是一个Job
-
Job
- Driver向Executor提交的作业
- 一个完整的DAG会产生一个Job,不Action就不会有Job
- 一个Job中有一个到多个Stage,一个Stage对应多个TaskSet
-
Stage
- 执行任务的阶段
- 一个stage对应一个TaskSet
- 一个stage中有一到多个Task,Task的数量取决于Stage中最后一个RDD分区的数量
- Stage分为两种:
- ShuffleMapStage:会进行shuffleWrite,为下一个stage做准备,里面的Task叫ShuffleMapTask
- ResultStage:数据源不确定,但是一定会Action会输出结果,里面对应的Task叫ResultTask
- stage结束的标志就是shuffle或者result,由于shuffle会写磁盘,在同一个Application中,如果执行重复的stage,会直接在shuffle到磁盘的数据上直接拉取,不再重新执行重复的stage
-
TaskSet
- 保存相同处理逻辑的、不同数据目标的多个Task
-
Task
-
是spark中最小的任务执行单元
-
Task有属性(从哪里读数据)、有方法(执行逻辑)
-
是一个类的实例,但是Task的执行逻辑是根据代码动态生成的
org.apache.spark.scheduler中的一个抽象类
-
有两个继承类:
- ShuffleMapTask
- ResultTask
-
Task的数量决定了并行度,Task如果过多也不会阻塞,线程会在多个task中切换并行
-
-
dependency:
- 依赖关系,指的是父RDD和子RDD之间的依赖关系,分为宽依赖和窄依赖
- 窄依赖:是指父RDD的每个分区只被子RDD的一个分区所使用,子RDD一般对应父RDD的一个或者多个分区。(与数据规模无关)不会产生shuffle。
- 宽依赖:指父RDD的多个分区可能被子RDD的一个分区所使用,子RDD分区通常对应所有的父RDD分区(与数据规模有关),会产生shuffle
-
pipeline
- 迭代器链
1.1 shuffle
- shuffle,中文字义为洗牌
- 每个分区的数据按照一定的规则,分区写到本地磁盘中;
- 下一个stage的各个分区的Task,到上个stage的磁盘中,拉取属于自己分区的数据,这个过程,为shuffle
- 注意事项:
- shuffle过程中,一定是一个分区的数据,有分散向多个分区的趋势;
- 也就是说,如果在已有数据中,刚好一个分区的数据全部到另一个分区中,不一定没有shuffle;有分区规则,但是数据最终都到了一个分区中(下游Stage只有一个分区),也不算shuffle
1.2 补充知识点
-
classOf[T]
方法- 获取类型T的Class对象,返回在运行过程中的类型。
-
创建连接
- 在创建对象或连接时,不要在Driver中创建,可能会导致Task无法序列化
- 关闭资源时,可以判断迭代器没有下一条数据时,进行关闭
-
分区器类型相同,分区数量相同,不再shuffle,也就没有新的stage
-
Partitioner分区器
- 一个父类,需要重写两个方法,有三个实现类
-
查看Executor的打印台
- 在work目录—job文件夹的stdout中
2.分区
- RDD分区方式
- 直接设置最小分区数,通过调用parallelize方法(仅限scala数组使用)
- 通过设置textFile方法中的minpartitions参数,调节分区数
- spark读取hdfs文件时,分区数算法详述
- 在获取RDD时,sc调用textFile等方法,输入path,并可选输入minPartitions
- minPartitions会进行判断,取输入值和2之间的最小值
- textFile类:调用hadoopFile,HadoopFile会返回一个KV类型的RDD(指针位置和行内容),textFile使用map方法抽取value,返回一个String类型的RDD
- 在hadoopFile中,调入了一个类:TextInputFormat,其父类FileInputFormat中,包含了RDD的分区方法:getSplits
- 遍历所有文件,获取总的文件长度totalSize,再用总长度除numSplits(前面的minPartitions,默认为2)(numSplits == 0 ? 1 : numSplits),获取目标大小goalSize
- 遍历所有文件,首先调用computeSplitSize方法,取goalSize和128M的最小值,返回为splitSize
- 进入for循环,判断文件的实际长度是否大于splitSize的1.1倍,如果大于,调用makeSplit进行切分,切掉的大小为splitSize
- 而后该文件剩余部分继续循环,直到小于splitSize的1.1倍为止
3.Transformation算子
算子是对RDD进行操作
对RDD中的每个分区的迭代器进行函数处理
Transformation的算子都是Lazy的,不会触发Action
3.1 不产生shuffle
不会读取数据
3.1.1 原理分析
map方法详解
- map方法传入自定义的函数返回一个RDD
- 在该方法中调用了一个构造方法,创建了一个RDD对象:MapPartitionsRDD
- MapPartitionsRDD继承RDD,利用多态,构造方法可以返回一个RDD对象
- 函数有三个参数(TaskContext,Index,Iterater),对Iterater进行map操作
- 在构造方法中,传入了调用map方法的RDD,和一个函数:
(_, _, iter) => iter.map(cleanF)
- 函数中的cleanF是经过处理后的传入的function,iter是一个读取上一个RDD信息的迭代器
- 可以发现,在构造方法的函数中,对迭代器读取的信息,直接调用了map方法进行了处理,并返回了一个新的迭代器,与其他方法组合,成为一个RDD(MapPartitionsRDD)
3.1.2 map类算子
- map(函数)
- 做映射,对RDD内的数据一一进行处理
- mapValues(函数)
- 是对RDD中对偶元组的value进行处理,不用管key,将value应用的传入的函数处理后在跟key放到对偶元组中
- mapPartitions(函数)
- 对Partition进行map操作
- 适用于关联外部的维度数据,例如查询数据库、外部的接口,套实现创建一下连接对象,一个分区的多条数据可以复用事先创建的对象
- mapPartitionsWithIndex(函数)
- 对Partition进行map操作,与此同时可以将分区编号获取到
- 输入的是迭代器,返回的数据也是迭代器
- 函数输入值:(index,iter)
- 底层调用了mapPartitionsRDD,函数有三个参数(TaskContext,Index,Iterater)用到了分区编号
- flatMap(函数)
- 相当于先map在flatten,rdd中没有flatten方法
- 返回值必须是一个或多个数组或集合
- flatMapValues
- 当RDD时KV类型,先执行mapValues,执行的结果应该是数组、集合或者迭代器
- 然后将集合炸开取每个元素,与key结合成,多个新的元组进行返回
3.1.3 过滤型算子
- filter(函数)
- 过滤,也是一条一条的过滤,返回true的就保留
- 底层new的是MapPartitionsRDD,对Iterater进行filter,传入你指定的的过滤函数
- keys
- Rdd中的数据类型为对偶元组,可以获取对偶元组中的第一个
- 用到了隐式转换,将RDD[(K, V)]类型,即RDD中的数据类型为对偶元组,进行包装之后转成了PairRDDFunction
- values
- 同上,返回第二个
3.1.4 其他算子
- union(++)
- 只能相同类型RDD进行union
- union不会去重,没有shuffle,其实就是将原来的两个RDD包装了一层
- 会把所有的元素汇总为新的RDD
- 相当于把多个RDD的多个分区sum到一个新的RDD中
3.2 产生shuffle
在分布式计算中,将数据按照一定的计算逻辑(分区器)
3.2.1 原理分析
reduceByKey方法详解
-
调用了combineByKeyWithClassTag方法,传入4个参数:一个reduceByKey的默认函数,二和三都是传入的自定义函数,四是一个分区器
-
在方法中,调用的构造函数是:ShuffledRDD(绝大部分带shuffle的Transformation都是该RDD)
-
构造函数传入调用reduceByKey的RDD,和一个分区器,该构造方法构造出来的RDD只会执行一个shuffle,不会执行shuffle前后的聚合等方法
-
需要通过set:Serializer(序列化方法)、Aggregator(传入上面那3个函数)、MapSideCombine(shuffle前是否聚合)等信息对ShuffledRDD进行完善
-
可以自行创建ShuffledRDD,来替代reduceByKey方法:
//手动编写reduceByKey方法 //函数1:聚合时,首个元素传入时对value的处理方法 val f1 = (e:Int) => e //函数2:聚合value的方法 val f2 = (e1:Int ,e2: Int) => e1+ e2 //在reduceByKey中,shuffle前后的聚合方法是一样的 val f3 = f2 //三个泛型:Key类型,原Value类型,聚合后的Value类型 //分区器创建一个新的HashPartitioner,分区数量设置为源RDD的分区数量 val shuffledRDD: ShuffledRDD[String, Int, Int] = new ShuffledRDD[String, Int, Int](wordAndOne, new HashPartitioner(wordAndOne.partitions.length)) //创建Aggregator,传入函数 var agg = new Aggregator[String, Int, Int](f1,f2,f3) shuffledRDD.setMapSideCombine(true) shuffledRDD.setAggregator(agg)
cogroup方法详解
- 协分组、联合分组,是对多个RDD进行分组
- RDD必须是KeyValue类型的,并且两个RDD的key类型一样才可以进行cogroup
- 生成的RDD中,数据格式为最外层是KV类型的元组,value是由两个迭代器组成的元组构成的
- cogroup是多个算子的实际执行方法,如join、leftOuterJoin、RightOuterJoin等
- 其中,创建的RDD是一个独特的RDD:CoGroupedRDD
3.2.2 聚合型算子
-
groupBy(分组原则)
-
groupByKey
- 底层调用的是combineByKeyWithClassTag,在该方法中new的ShuffleRDD,mapSideCombine = true
-
reudceByKey(函数)
-
可以先局部聚合,在全局聚合,底层调用的是combineByKeyWithClassTag,并在方法中中new的ShuffleRDD, ,mapSideCombine = true
-
foldByKey(初始值)(函数)
- 可以指定初始值的reduceByKey
-
combinByKey(函数1,函数2,函数3)
- 将reduceByKey分解,可以单独设置:初始化函数、聚合前的value合并函数、聚合的合并函数
- 底层调用的是combineByKeyWithClassTag, 并在方法中中new的ShuffleRDD
- mapSideCombine(shuffle前是否聚合)参数默认为true
-
aggregateByKey(初始值)(函数1,函数2)
- 与reduceByKey相同,更为灵活
- 可以将shuffle前后的聚合方法分别编写
- 局部聚合应用初始值,全局聚合不用初始值
-
distinct
- 去重
- 先调用map将数据变成K,V类型,Value是null,然后调用reduceByKey,先在每个分区进行局部去重,然后在全局去重
3.2.3 分区型算子
- partitionBy(分区器)
- 重新分区
- 只分区,不聚合、不分组
- 与new shuffledRDD什么也不设置是一样的
- repartition(分区数)
- 重新分区(分区方法还是HashPartitioner)
- 使用场景:提高并行度、更改分区器
- coalesce(分区数,是否shuffle)
- 也是重新分区,可以设置是否shuffle
- 如果不shuffle,分区数只能减少,不能增加(因为如果分区增加,一定要一个分区的数据发送到多个分区,必须要shuffle)
- 先将数据调用mapPartitionerWithIndexInternal,根据分区的编号和分区的数量生成随机数最为key,然后在new shuffledRDD进行重新分区
- repartition底层调用的就是coalesce,shuffle为true
3.2.4 联合型算子
-
join(RDD)
-
类似于SQL中的inner
-
底层调用的也是cogroup,每个RDD对应的迭代器都不为空就join上了
-
然后调用flatMapValues,使用for循环和yield实现类似java的双层for循环,将返回的结果在压平
-
使用cogroup实现join
val rdd2: RDD[(String, (Iterable[Int], Iterable[Int]))] = rdd11.cogroup(rdd12) val out = rdd2.flatMapValues(e => for (x <- e._1; y <- e._2) yield (x, y))
-
-
leftOuterJoin
- 左外连接
- 手写实现leftOuterJoin(rightOuterJoin同理)
val rdd2: RDD[(String, (Iterable[Int], Iterable[Int]))] = rdd11.cogroup(rdd12) val out = rdd2.flatMapValues(pair=> if (pair._2.isEmpty) pair._1.map((_,None)) else for ( x<- pair._1 ; y <- pair._2 ) yield (x, Some(y))) //else pair._1.flatMap(e=> pair._2.map(y=> (e,Some(y)))))
-
fullOuterJoin
- 空值连接(左外连接和右外连接的结合,没有数据填null)
- 手写实现
val rdd2: RDD[(String, (Iterable[Int], Iterable[Int]))] = rdd11.cogroup(rdd12) val out = rdd2.flatMapValues { case (Seq(), iter) => iter.map(e => (None, Some(e))) case (iter, Seq()) => iter.map(e => (Some(e), None)) case (it1, it2) => it1.flatMap(x => it2.map(y => (Some(x), Some(y)))) }
-
intersection 交集--------------------------------------------------------------------------------------------
- 底层调用的cogroup
- 使用map将两个RDD的数据本身当成key,null当成value,组成KV类型的RDD
- 然后调用cogroup,再进行过滤,过滤的条数是,两个迭代器都不为空迭代器(类似join的选择结果,把join的结果的value丢掉),然后调用keys取出key
-
subtract(RDD)
- 求差集,rdd1-rdd2
- 调用了SubtractedRDD
- 底层中,将第一个RDD的数据放到一个Map集合中,作为Key,出现的次数(会创建一个ArrayBuffer数组,出现一次,添加一个null,null的个数就是出现的此时)作为Value;
- 再遍历第二个集合的数据,对其中的元素遍历,将所有的元素在Map集合中进行remove(这样不论value中的数组有多少个null),都会直接移除
- 剩下的数据使用flatMapValue和keys,恢复成原始的数据格式,就求出了差集
3.2.5 排序型算子
-
sortBy(排序关键字,是否正序)
- 先分桶排序,再全局聚合
- 会进行采样(sample),collect结果,构建分区规则,划分桶的范围,将数据shuffle到不同范围的桶中
- 对各个桶内进行排序,按照桶的分区号(序号)
-
repartitionAndSortWithinPartitions(分区器)
- 重新分区且排序
- 可以delimited排序器(Ordering)来自定义排序规则
4.action算子
所有的action算子都会调用:sc.runJob
调用sc.runJob方法,根据最后一个RDD从后往前推,触发Action就会生成DAG,切分Stage,生成TaskSet
4.1 收集型算子
- collect
- 收集数据到一个数组中
- 将每个分区的数据通过网络收集到Driver端,然后方法到Driver端的内存中,然后的数据按照分区的先后存放在数组中,如果Driver端的内存不足,只会收集部分数据
- saveAsTextFile
- 保存为Text类型文件
- 该方法内部会调用mapPartitions,将数据转成NullWirtable和Text类型,然后使用TextOutputFormat格式写入到HDFS中
4.2 遍历型算子
- foreach
- 遍历每一条数据
- 底层调用的是迭代器的next(),用完就没了,返回Unit
- foreachPartition
- 与foreach相似,处理分区
- 如果要将数据写入到数据库中,一个分区一个连接,效率更高
- foreachPartitionAsync
- 与foreachPartition相似,异步进行
4.3 聚合型算子
- count
- 先在每个分区内进行计数,使用的是迭代器遍历一条数量加1
- 然后将每个分区计数的结果返回到Driver端进行全局的聚合
- sum
- 底层调用了fold(0)(函数)
- 先局部求和,再全局求和
- fold(初始值)(函数)
- 带初始值执行函数
- reduce(函数)
- 不带初始值的聚合
- 将所有数据聚合再一起,局部聚合和全局聚合都是这一个函数
- aggregate(初始值)(局部聚合函数,全局聚合函数)
- 更为灵活地聚合,类似transformation中的aggregateByKey,区别在于返回值可以为任意类型而不是RDD;初始值在局部聚合和全局聚合都会应用
- 各个分区进行聚合时, 是没有顺序的,谁快谁来
4.4 取值型算子
- take(数量)
- 取n条数据,可能触发多次Action
- 底层使用while循环,使用hasnext和next
- 先从0号分区取数据,如果不够就到下一个分区接着取,直到取完全部分区
- first
- 返回第一个元素,调用的是take(1)
- 不同之处:take返回数组,first返回元素
- min/max
- 最大值,最小值
- 在每个分区内,使用冒泡的方式求出最大(最小)值,再将结果汇总到Driver端进行比较
4.5 排序型算子
- top(n)
- 取前n个,返回数组
- 在每个分区内使用有界优先队列对N个数据进行排序,移除不符合的数据
- 再在Driver端使用有界优先队列进行全局排序
- 有界优先队列:
- 有界优先队列类似一个TreeSet,但是不去重;
- 如果取n个数据,则会创建一个长度为n的有界优先队列
- 利用红黑树的高效排序,每存储一个数据,集合内的数据达到n+1条时,对集合内最小(最大)的数据进行remove
- 使集合内始终保留最大(最小)的n条数据,这样就可以求出每个分区的前n条
- 两个长度为n的队列结合(++=),同样会保留n条数据
- 只在Driver端生成进行Map的操作,不需要shuffle
- takeOrdered(获取数量)(排序规则)
- 与top相反,获取最小的n条数据(top底层调用的就是takeOrdered)
- 可以自定义排序规则
五 高级功能
spark提供了一些在实用场景下,为提高运行效率,添加的一些功能
1.cache/persist
将数据缓存到Executor的内存中,当RDD调用时,可以直接在内存中调用并且复用缓存的数据,相应速度快
-
适用场景
- 当需要多次调用action时,可以使用
-
注意事项
- 数据较大时占用内存,应先将数据初步处理后再cache
- cache大量文件时,如果内存不足,会自行判断存储一部分文件(文件不会拆分)
- cache(persist)不是算子,不会返回新的RDD
-
使用优化
- 常规设置cache序列化方法为java对象进行序列化,数据冗余度较高,占用空间大;
- 使用spark提供的专用序列化方式:
//设置序列化方式为kryo sc.getConf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer") //使用persist进行缓存设置,以便选择存储位置 //cache底层调用了persist rdd.persist(StorageLevel.MEMORY_ONLY_SER) //删除缓存数据(是否同步) rdd.unpersist(true)
- 通过SrotageLevel可以选择多种存储方式,磁盘储存、内存储存、备份等
-
使用建议
- 生产环境中,建议使用kryo的序列化方式
- 使用MEMORY_AND_DISK_BOTH_SER--------------------------
2.checkpoint
对于一些经过复杂计算得到的,重要的中间结果数据,为了避免数据丢失,可以将中间结果保存到hdfs中,以后在Application中可以反复使用(适合复杂计算)
-
使用checkpoint
-
在调用rdd的checkpoint前,一定要指定保存的目录:
sc.setCheckPointDir
-
在数据action之前,进行调用
rdd.checkpoint
-
-
执行机制
- 执行过程中,调用checkpoint后的第一次触发action时,才会做checkpoint
- 由于checkpoint会将数据写到hdfs中,所以会触发一次job
- 如果RDD做了checkpoint,这个RDD以前的依赖关系就不再使用了
- 如果同时设置了cache和checkpoint,RDD的迭代器在拉取数据时,会进行判断,按照cache、checkpoint、父迭代器的优先顺序进行选择
-
注意事项
- checkpoint不是Transformation,只是标记
- 如果设置了checkpoint后,hdfs中的数据丢失了,再次调用RDD时,会报错
-
使用建议
- 由于使用checkpoint会触发至少两个action,建议在checkpoint前,对RDD进行cache,可以避免数据重复计算
3.广播变量
广播变量通常是为了实现mapSideJoin,减少网络传输,可以将小的数据广播到属于该application的Executor中,然后通过Driver广播变量返回的引用,获取实现广播到Executor的数据
-
注意事项
- 要广播的数据不宜太大,要根据服务器的内存和分配的executor-memory决定
- 广播变量是readOnly的,一旦广播出去,只读不能改
- 但是在Driver端可以调用unpersist将广播变量释放,如果以后在Executor中用到该变量,需要重新广播
-
使用方法
//将Driver中的数据广播到Executor中,这个是个阻塞方法 val broadcastRef = sc.broadcast(数据集合) //会返回一个广播变量的引用 //RDD可以通过该引用获取到已经广播到Executor中的数据 broadcastRef.value
-
使用演示:
- 使用广播变量进行ip地址查询
package cn.doit.day06 import cn.doit.day06.IpTest.{binary2Decimal, ip2City} import org.apache.spark.rdd.RDD import utils.SparkUtils object IpTestEasy { def main(args: Array[String]): Unit = { //创建连接 val sc = SparkUtils.createContext() //读取ip地址的匹配规则数据 val ipRulerLines = sc.textFile(args(0)) val idAndAddr: Array[(Long, Long, String, String)] = ipRulerLines.map(e => { val arr1: Array[String] = e.split("\\|") val startIp = arr1(2).toLong val endIp = arr1(3).toLong val province = arr1(6) val city = arr1(7) (startIp, endIp, province, city) }).collect() //使用broadcast进行广播 val ip2AddrRuler = sc.broadcast(idAndAddr) val lines = sc.textFile(args(1)) //一会用 mappartitions尝试,一个分区读取一次广播的变量是否会加快速度 val addrAndOne = lines.map(e => { //获取广播变量 val ipRulerArr: Array[(Long, Long, String, String)] = ip2AddrRuler.value //获取二进制地址 val binaryIp: String = e.split("\\|")(1) //将二进制地址转为十进制IP地址 val decimalIp = binary2Decimal(binaryIp) //使用二分法查找IP地址对应的实际地址 val addr = ip2City(ipRulerArr, decimalIp) (addr, 1) }) //聚合 val result: RDD[((String, String), Int)] = addrAndOne.reduceByKey(_ + _) println(result.collect().toBuffer) //ArrayBuffer(((云南,昆明),126), ((北京,北京),1535), ((河北,石家庄),383), ((未知省份,未知城市),1), ((陕西,西安),1824), ((重庆,重庆),868)) } /** * 封装二进制IP地址转换为十进制的IP地址的方法 * 输入:二进制的IP地址 --- 字符串 * 输出:十进制的IP地址 --- Long类型 */ def binary2Decimal(s: String): Long = { val arr: Array[String] = s.split("\\.") //判断 if (arr.length != 4) return 0L var decimalIp: Long = 0 for ( i <- 0 to 3 ) decimalIp += (arr(3-i).toLong * Math.pow(256, i).toLong) decimalIp } /** * 封装通过二进制地址查找实际地址的方法 * 使用二分查找法 * 输入值为:规则数组,十进制IP地址 * 返回值为:元组(省份,城市) */ def ip2City(arr: Array[(Long, Long, String, String)], decimalIp: Long): Tuple2[String, String]={ //定义一个中间索引 var mid = arr.length / 2 //中间元组 val midValue = arr(mid) if (decimalIp>= midValue._1){ if (decimalIp> arr.last._2) return ("未知省份","未知城市") else if (decimalIp> midValue._2) return ip2City(arr.takeRight(arr.length - mid -1), decimalIp) else return (midValue._3, midValue._4) } else if (decimalIp< arr(0)._1) return ("未知省份","未知城市") else return ip2City(arr.take(mid) , decimalIp) } }