Spark学习笔记
一、Spark基本概念
1、架构初析
1-1、Spark的基本架构组成
- Spark应用程序由一个驱动器进程(driver)和一组执行器(worker)进程组成。
- 其中驱动器(driver)的作用是:维护Spark应用程序的相关信息;回应用户的程序或输入;分析任务并分发给各个执行器去处理。
- 执行器(worder)的作用是负责执行驱动器分配的任务,并将状态反馈给驱动器,其执行的进程叫做Executor。
在Spark应用程序运行的过程中,对于资源的分配由Cluster Manager集群管理器操控,其中集群管理器可以是以下三种:Spark的独立集群管理器、YARN、Mesos。也就是说Spark架构主要是:Driver(用户的应用程序分配) Worker(执行节点) Cluster Manager(资源管理器)
架构总结:一个应用(Application)由一个任务控制节点(Driver)和若干个作业(Job)构成,一个作业由多个阶段(Stage)构成,一个阶段由多个任务(Task)组成。当执行一个应用时,任务控制节点会向集群管理器(Cluster Manger)申请资源,启动Exector,并向Exector发送应用程序代码和文件,然后在Executor上执行任务,运行结束后,执行结果会返回给任务控制节点,或者写到HDFS或者其他数据库中。
架构特点:任务采用数据本地化和推测执行等优化机制。数据本地化是尽量将计算移到数据所在节点上进行,也就是计算靠近数据。推测执行的指的是,如果任务调度的节点正在被占用,不会立刻转到其他数据节点进行操作,而是计算移动的时间和等待的时间哪一个消耗的时间短,如果等待的时间短,就继续等待,也就是选择最优的方案。
1-2、Spark的运行基本流程
- 当一个Spark应用被提交时,首先为这个应用构建起基本的运行环境,即由任务控制节点(Driver)创建一个SparkContext,由SparkContext负责和资源管理器(Cluster Manager)进行资源的申请、任务的分配与监控等。
- 资源管理器(Cluster Manager)为Executor分配资源,并启动Executor进程,Executor运行情况随着心跳信息发送给资源管理器。
- SparkContext根据RDD的依赖关系,构建DAG图,并将其交给DAG调度器进行解析,将DAG图分解为多个"stage",每个stage都是一个任务集,并且计算各个stage之间的依赖关系,然后把每一个任务集交给底层的任务调度器(TaskScheduler)进行处理;Executor向SparkContext申请任务,任务调度器将任务分发给Exector去运行,同时SparkContext将应用程序代码分发给Executor
- 任务在Executor上运行,将运行结果反馈给任务调度器,然后反馈给DAG调度器,运行完毕后,写入数据并释放所有资源。
1-3、Spark与Hadoop相比的优点
- Spark把中间数据保存在内存中,而Hadoop是保存在磁盘上,节省IO,速度更快
- Spark支持DAG,也就是有向无环图,减少迭代次数,同时延迟加载提高效率
- Spark的核心计算是RDD,容错率高,因为是弹性数据集,一部分丢失,可以通过血统快速重建
- Spark更加通用,因为Hadoop只提供map和reduce两种操作,而Spark提供transformations和Actions两大类操作。同时Spark提供了一站式的解决方案,从离线到准实时、到机器学习。
- Spark的计算与MapReduce相比,采用的Executor优点在于:
- 采用多线程(MR是多进程)
- Executor中有一个BlockManager存储模块,将内存和磁盘共同作为存储,则多伦迭代时中间结果保存在内存中,极大减少了磁盘IO,提高了性能
2、SparkCore
Spark的核心计算Core,底层是RDD,也就是弹性分布式数据集,之后更高级的API本质上还是要转换成RDD进行操作。
2-1、RDD的相关概念
- RDD就是弹性分布式数据集,本质上就是只读的分区记录集合,可以分成多个分区,每个分区就是一个数据集片段,分区可以保存在不同节点上。
- RDD的操作可以分为转换和行动。转换是接受RDD然后产生新的RDD,而行动是接受RDD然后会返回一个值。
- RDD有“血缘关系”,也就是DAG拓扑排序关系,通过血缘关系连接起来,RDD是惰性的,也就是说,转换不会立刻发生,而是当有动作时才会触发,也就是管道化,这样中间结果不用保存了就可以直接使用,减少了数据的等待,性能更好。
2-2、RDD的特性
- 高效的容错性。因为RDD只读,不可修改,如果想要修改就要从父RDD生成子RDD,也就是说之间有血缘链路,这样的话如果RDD丢失或损坏,可以由父RDD直接产生,不用再从头开始。
- 中间结果持久化到内存。因为多个RDD之间进行传递,不需要落地磁盘,减少了不必要的磁盘IO
- 存放的数据可以是Java对象,减少了不必要的序列化和反序列化的开销
2-3、RDD之间的依赖关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cNFKWH7s-1598339867522)(C:\Users\20018824\AppData\Roaming\Typora\typora-user-images\image-20200812195148915.png)]
RDD的依赖关系可以分为:宽依赖,窄依赖
- 宽依赖:父RDD的分区可以对应子RDD的多个分区
- 窄依赖:父RDD的分区只能对应一个子RDD的分区
宽窄依赖使得RDD有天生的容错性,并且窄依赖的恢复效率更高。
2-4、RDD的运行过程
- 创建RDD
- SparkContext负责计算RDD之间的依赖关系,构建DAG
- DAGScheduler负责把DAG图分解成多个阶段,每个阶段包含多个任务,每个任务由任务调度器分配到不同的word上的executor区执行。
2-5、RDD的Java使用
RDD的使用主要分转换操作和动作操作,其中转换操作输入值是RDD,返回值是RDD,且其是惰性的,也就是说不会真的去操作,只有当动作操作到来时才会全部一次性去操作类似于链条一样。动作操作的输入值是RDD,输出值的值,也就是RDD操作的终结。
2-5-0、创建RDD
/*
*创建rdd的方式有多种
*从文件读取、从数据源获取、手动创建
*步骤都是:
* 1、创建sparkconf进行配置
* 2、创建JavaSparkContext
* 3、创建JavaRDD
*注意:SparkSession是用在SparkSQL、SparkStreaming上
*/
//1、手动创建
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张三");
list.add("李四");
list.add("王五");
list.add("王五");
JavaRDD<String> javaRDD = sc.parallelize(list);
//2、从文件读取
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
JavaRDD<String> javaRDD= sc.textFile("D://ab.txt");
//3、从数据源获取
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
JavaRDD<String> javaRDD= sc.textFile("HDFS的路径");
2-5-1、转换算子
- map操作
//针对普通的rdd
JavaRDD<String> maprdd = javaRDD.map(x -> {
return x + "哈哈";
});
loger.info(String.valueOf(maprdd.collect()));
结果:
[张三哈哈, 李四哈哈, 王五哈哈, 王五哈哈]
//针对键值对RDD,也就是pair RDD
//首先创建key:value的规则
PairFunction<String,String,Integer> keyData = (String x)->{return new Tuple2(x,1);};
//创建JavaPairRdd也就是键值对RDD
JavaPairRDD<String,Integer> javaPairRDD = javaRDD.mapToPair(keyData);
loger.info(String.valueOf(javaPairRDD.collect()));
结果:
[(张三,1), (李四,1), (王五,1), (王五,1)]
注意:
PairFunction<String,String,Integer> keyData = (String x)->{return new Tuple2(x,1);}; 使用了函数式编程
原生应该是这样:
PairFunction<String, String, Integer> keyData = new PairFunction<String, String, Integer>() {
public Tuple2<String, Integer> call(String x) {
return new Tuple2(x, 1);
}
};
//JavaPairRDD适用的算子是:各种聚合操作,比如reduceBykey,groupBykey,combineBykey,mergeBykey,sortBykey()等
- flatmap算子
/*
*flatmap算子的功能是对每一个值进行map然后再进行扁平化。说白了就是进行map,然后汇总一下
*比如map产生的是[[1,2],[3,4]]
*那么flatmap就会将其扁平化成[1,2,3,4],就是汇聚成一个整体
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
//map的结果是:
JavaRDD<String> javaRDD = sc.parallelize(list);
JavaRDD<List<String>> map = javaRDD.map(x -> {
return Arrays.asList(x.split("_"));
});
loger.info(String.valueOf(map.collect()));
[[张, 三], [李, 四], [王, 五], [王, 五]]
//进行flatmap的结果是:
JavaRDD<String> flatmap = javaRDD.flatMap(new FlatMapFunction<String, String>() {
@Override
public Iterator<String> call(String s) throws Exception {
return Arrays.asList(s.split("_")).iterator();
}
});
loger.info("flatmap结果如下----------------------------");
loger.info(String.valueOf(flatmap.collect()));
结果如下:
[张, 三, 李, 四, 王, 五, 王, 五]
- filter算子:
/*
*filter算子的作用就是过滤,只保留符合函数定义的条件
*/
JavaRDD<String> filtermap = javaRDD.filter(x -> {
return !x.equals("张_三");
});
loger.info(String.valueOf(filtermap.collect()));
结果:
[李_四, 王_五, 王_五]
- groupBykey(针对pair RDD)
/*
*groupBykey针对pair RDD,进行聚合
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
//首先创建key:value的规则
PairFunction<String,String,Integer> keyData = (String x)->{return new Tuple2(x,1);};
//创建JavaPairRdd也就是键值对RDD
JavaPairRDD<String,Integer> javaPairRDD = javaRDD.mapToPair(keyData);
//4、groupbyKey算子(针对pair RDD)
JavaPairRDD<String, Iterable<Integer>> stringIterableJavaPairRDD = javaPairRDD.groupByKey();
loger.info(String.valueOf(stringIterableJavaPairRDD.collect()));
结果:
[(张_三,[1]), (李_四,[1]), (王_五,[1, 1])]
- reduceBykey(针对pair RDD)
/*
*reduceBykey针对pair RDD,进行汇总
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
//首先创建key:value的规则
PairFunction<String,String,Integer> keyData = (String x)->{return new Tuple2(x,1);};
//创建JavaPairRdd也就是键值对RDD
JavaPairRDD<String,Integer> javaPairRDD = javaRDD.mapToPair(keyData);
//4、reduceBykey算子(针对pair RDD)
JavaPairRDD<String, Integer> reduceByKeyRdd = javaPairRDD.reduceByKey((x,y)->{return x+y;});
loger.info(String.valueOf(reduceByKeyRdd.collect()));
结果:
[(张_三,1), (李_四,1), (王_五,2)]
注意:
reduceBykey与groupBykey的区别在于reduceBykey最终每一个key的value是按照规则汇总的值,而groupBykey就只是单纯的聚合在一起
- 集合运算-union算子
/*
*union算子的作用就是将两个rdd合成一个rdd
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
ArrayList<String> list2 = new ArrayList<String>();
list2.add("张_三_2");
list2.add("李_四_2");
list2.add("王_五_2");
list2.add("王_五_2");
JavaRDD<String> javaRDD2 = sc.parallelize(list2);
JavaRDD<String> union = javaRDD.union(javaRDD2);
loger.info(String.valueOf(union.collect()));
结果:
[张_三, 李_四, 王_五, 王_五, 张_三_2, 李_四_2, 王_五_2, 王_五_2]
- 集合运算-subtract 算子
/*
*subtract 算子是 rdd1减去rdd2中rdd1有的 也就是 rdd1-(rdd1交rdd2) 比如rdd1 = [1,2,3] *rdd2=[2,3,4,5] 则rdd1.subtract(rdd2) 就是[1]
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
ArrayList<String> list2 = new ArrayList<String>();
list2.add("张_三_2");
list2.add("李_四");
list2.add("王_五_2");
list2.add("王_五_2");
JavaRDD<String> javaRDD2 = sc.parallelize(list2);
JavaRDD<String> subtract = javaRDD.subtract(javaRDD2);
loger.info(String.valueOf(subtract.collect()));
结果是:
[王_五, 王_五, 张_三]
- 集合运算-intersection算子
/*
*intersection算子就是取rdd1与rdd2的交集
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
ArrayList<String> list2 = new ArrayList<String>();
list2.add("张_三_2");
list2.add("李_四");
list2.add("王_五_2");
list2.add("王_五_2");
JavaRDD<String> javaRDD2 = sc.parallelize(list2);
JavaRDD<String> subtract = javaRDD.intersection(javaRDD2);
loger.info(String.valueOf(subtract.collect()));
结果:
[李_四]
- join操作(Pair RDD)
/*
*join的操作就是将两个键值对rdd进行连接,join就是nature join 当然还有*left join、right join、cartesian也就是笛卡尔积
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
//首先创建key:value的规则
PairFunction<String,String,Integer> keyData = (String x)->{return new Tuple2(x,1);};
//创建JavaPairRdd也就是键值对RDD
JavaPairRDD<String,Integer> javaPairRDD = javaRDD.mapToPair(keyData);
//创建规则2
PairFunction<String,String,Integer> keyData2 = (String x)->{return new Tuple2(x,2);};
//创建JavaPairRdd2也就是键值对RDD
JavaPairRDD<String,Integer> javaPairRDD2 = javaRDD.mapToPair(keyData2);
JavaPairRDD<String, Tuple2<Integer, Integer>> join = javaPairRDD.join(javaPairRDD2);
loger.info(String.valueOf(join.collect()));
结果:
[(张_三,(1,2)), (李_四,(1,2)), (王_五,(1,2)), (王_五,(1,2)), (王_五,(1,2)), (王_五,(1,2))]
过程就是
rdd1 rdd2
张_三 1 张_三 2
李_四 1 李_四 2
王_五 1 王_五 2
王_五 1 王_五 2
最后就是
(张_三,(1,2))
(李_四,(1,2))
(王五,(rdd1第一个王_五的1,rdd2第一个王_五的2))
(王五,(rdd1第一个王_五的1,rdd2第二个王_五的2))
(王五,(rdd1第二个王_五的1,rdd2第一个王_五的2))
(王五,(rdd1第二个王_五的1,rdd2第二个王_五的2))
2-5-2、动作算子
- collect算子:获取所有的值(以列表形式返回)
- take(n)算子:获取前n个内容(以列表形式返回)
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
//动作算子
//1、take(n)算子
List<String> take = javaRDD.take(2);
loger.info(String.valueOf(take));
结果:
[张_三, 李_四]
- count()算子:获取所有的个数
- reduce算子:
/*
*reduce算子的作用就是整体进行聚合,返回一个数值,与reduceBykey不同的是,*reduceBykey其实就是按key进行分组然后reduce。
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
//2、reduce算子
String reduce = javaRDD.reduce((x, y) -> {
return x + y+"|";
});
loger.info(reduce);
结果:
张_三李_四|王_五|王_五|
- foreach算子
/*
*相较于前者action算子的返回一个值、返回一个集合相比,foreach不会返回值,*作用就是对每一个元素进行相关操作
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
javaRDD.foreach(x->{
System.out.println(x);
});
结果:
就是对rdd的每一个值进行打印操作
张_三
李_四
王_五
王_五
- foreachpartition
/*
*foreachpartition方法的作用就是对每一个分区进行操作,在函数内部,通过迭
*代器的方式,对分区的每一个元素进行操作
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
javaRDD.foreachPartition(new VoidFunction<Iterator<String>>() {
@Override
public void call(Iterator<String> stringIterator) throws Exception {
while (stringIterator.hasNext()){
String x= stringIterator.next();
System.out.println(x);
}
}
});
结果如下:这个时候还没有划分分区,所以只有一个主分区
张_三
李_四
王_五
王_五
2-5-3、其他操作
除了上述的转换和动作算子之外,为了效率的提高,可以使用如下相关操作。
- 持久化操作:
持久化操作的目的是,为了减少计算次数,优化执行效率。因为spark的计算引擎,默认的是:每次进行一次rdd操作时,都会重新进行一次从源头到这个rdd的过程,再进行你要执行的操作,所以效率很低。持久化的作用就是,将你需要复用的rdd保存到内存或磁盘中,下次使用直接就可以使用了,无需再执行一遍获取此rdd的过程,极大的提高了效率。但是也有可能导致资源不足,具体见RDD优化策略。
- cache操作
/*
*cache的操作,就是将rdd持久化到内存中,供之后复用。但只有触发action时才会进行保存,所以建议count一下,保存一下数据
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
//1、cache操作
JavaRDD<String> filtermap = javaRDD.filter(x -> {
return !x.equals("张_三");
});
filtermap.cache().count();
作用:这个就是将filtermap保存到了内存中,以后再使用filtermap就不会根据链路重新执行一次获取filtermap的操作了
- peisist操作
/*
*persist操作,就是将数据持久化到指定的区域,cache其实是persist的一种,就是全部持久化到内存的一种
*persist的种类包括:MEMORY_ONLY(就是cache)、MEMORY_AND_DISK、*MEMORY_ONLY_SER、MEMORY_AND_DISK_SER、DISK_ONLY、*MEMORY_ONLY_2、MEMORY_AND_DISK_2
*同样需要触发action才能真的执行,所以建议count一下,持久化一下
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
//1、cache操作
JavaRDD<String> filtermap = javaRDD.filter(x -> {
return !x.equals("张_三");
});
filtermap.persist(StorageLevel.MEMORY_AND_DISK()).count();
结果:
持久化到了内存和磁盘中,也就是内存不够时,会持久化到磁盘。
- unpersist操作
/*
*unpersist功能就是移除持久化
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
//1、cache操作
JavaRDD<String> filtermap = javaRDD.filter(x -> {
return !x.equals("张_三");
});
filtermap.persist(StorageLevel.MEMORY_AND_DISK()).count();
//3、unpersist操作
filtermap.unpersist();
- 手动分区,在连接时,聚合时更快,可以获益的操作是:groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey()、reduceByKey()、 combineByKey() 以及 lookup()。
3、Spark部署模式
Spark三种部署方式:standalone、Spark on Mesos、Spark on YARN
- standalone:Spark原生的部署方式与MapReduce1.0框架类似,但是无法使用存储在HDFS上的数据。
- Spark on Mesos:运行在Mesos比Yarn更加灵活,官方推荐模式
- Spark on Yarn:可以与Hadoop统一部署,分布式存储则依赖HDFS
4、API介绍
- Spark的API:
- 低级API:
- RDD
- 分布式共享变量
- 高级API:
- Dataset:仅用于Java和Scala中,相当于集合一样类似于Java的ArrayList,而且Dataset是安全的也就是其对象的类型不变。
- DataFrame:表格型结构类型,数据+结构信息,可以用特定的语句进行操作,但是在实际过程中,一般都是利用SQL对其操作,将其以表和视图看待。
注意:DataFrame与Dataset的区别在于DataFrame是row类型的Dataset的集合(可以理解为Dataset是集合,DataFrame相当于二位数组或二维集合,酷似表一样)。Dataset只能在基于JVM的编程语言上使用,比如Java和Scala。还有一点的是,DataFrame是非类型化的而Dataset是类型化的,其实就是类似于泛型一样的概念。
DataFrame = Dataset[Row]
Row其实表示一行数据,比如Row=[“a”,12,123]
这样的话其实Dataset就类似与二维数组相当于DataFrame
- Schema:是DataFrame提供的详细的结构信息,帮助sparksql清除知道该数据集包含哪些列、每列的名称和类型是什么
- SQL和视图:SQL对应的就是表,视图对应的就是数据库中的视图概念
5、分布式共享变量
分布式共享变量有两种类型:广播变量和累加器。这两个变量可以在用户定义函数中使用,比如RDD或者DataFrame。下面是其优点与使用方法:
5-1、广播变量
广播变量可以在集群中共享,且是只读的不变量。在创建时,会保存在每一个节点的Executor内存中。好处就是:不用每次任务都要在工作节点上执行多次反序列化(因为普通的变量需要发送到每个工作节点上进行反序列化操作);其次,他创建的数据,不需要序列化,因为没有反序列化这个需要。
广播变量适合大型数据,比如吧一个大表放入广播变量,减少了来回数据的传输,但要注意OOM
简单的例子如下:
/*
*这里将数组放入广播变量中,使用的时候直接读取即可
*/
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
//共享变量
ArrayList<String> bro_list = new ArrayList<>();
bro_list.add("第一名");
bro_list.add("第二名");
bro_list.add("第三名");
bro_list.add("第四名");
final Broadcast<ArrayList<String>> broadcast = sc.broadcast(bro_list);
//使用broadcast
JavaRDD<String> broadcast_map = javaRDD.map(new Function<String, String>() {
int i = 0;
ArrayList<String> value = broadcast.value();
@Override
public String call(String s) throws Exception {
String result = s + value.get(i);
i += 1;
return result;
}
});
loger.info(String.valueOf(broadcast_map.collect()));
结果如下:
[张_三第一名, 李_四第二名, 王_五第三名, 王_五第四名]
5-2、累加器
所谓累加器,也就是可以发生改变的分布式共享变量,由Driver进行读取,Executor来修改,所以只要有一个Executor进行修改了,那么全局都会发生改变。(这里的Driver可以读取的意思是只能通过.value来获取其值,不能直接拿来使用)
累加器的使用场景:对某个事件进行计数。需要SparkSession来构建,因此适合SparkSQL、SparkStreaming这些场景。
累加器默认的三种类型是:LongAccumulator针对Long类型数据,DoubleAccumulator针对浮点型数据,CollectionAccumulator针对集合类型数据。当然,如果自带的累加器无法满足,就要使用自定义累加器,实现AccumulatorV2接口即可。
累加器是可以命名的,命名的累加器可以在SparkUI中看到。
简单的例子:
SparkSession spark = SparkSession.builder().master("local[*]").getOrCreate();
LongAccumulator longAccumulator = spark.sparkContext().longAccumulator();
longAccumulator.add(1);
System.out.println(longAccumulator.value());
结果
1
用在转换和动作算子上
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
SparkSession spark = SparkSession.builder().master("local[*]").getOrCreate();
LongAccumulator longAccumulator = spark.sparkContext().longAccumulator();
javaRDD.map(new Function<String, String>() {
@Override
public String call(String s) throws Exception {
longAccumulator.add(1);
return s+"累加";
}
});
loger.info("------------------------------------");
loger.info(String.valueOf(longAccumulator));
loger.info("------------------------------------");
mapRdd.count();
loger.info(String.valueOf(longAccumulator));
loger.info("------------------------------------");
mapRdd.count();
loger.info(String.valueOf(longAccumulator));
loger.info("------------------------------------");
结果是:
-------------------------
0
-------------------------
4
-------------------------
8
//这里说明累加器也是惰性的,如果放在转换操作后面,而且每一次相同rdd触发动作操作,因为会从头获取RDD,所以累加操作就会重新操作一遍,所以是0、4、8
//单纯的action操作就不会带来问题
针对转换算子带来的问题,解决方案如下:
将需要进行累加器的算子进行cache,也就是切断和之前的血缘链条,使用时不需要再重新根据链路获得
SparkConf conf = new SparkConf();
JavaSparkContext sc = new JavaSparkContext("local", "First Spark App", conf);
ArrayList<String> list = new ArrayList<String>();
list.add("张_三");
list.add("李_四");
list.add("王_五");
list.add("王_五");
loger.info("任务开始");
loger.info(String.valueOf(list));
JavaRDD<String> javaRDD = sc.parallelize(list);
SparkSession spark = SparkSession.builder().master("local[*]").getOrCreate();
LongAccumulator longAccumulator = spark.sparkContext().longAccumulator();
JavaRDD<String> mapRdd = javaRDD.map(new Function<String, String>() {
@Override
public String call(String s) throws Exception {
longAccumulator.add(1);
return s + "累加";
}
});
JavaRDD<String> cache = mapRdd.cache();
loger.info("------------------------------------");
loger.info(String.valueOf(longAccumulator));
loger.info("------------------------------------");
cache.count();
loger.info(String.valueOf(longAccumulator));
loger.info("------------------------------------");
cache.count();
loger.info(String.valueOf(longAccumulator));
loger.info("------------------------------------");
结果
--------------------
0
--------------------
4
--------------------
4
//因为cache切断了和之前的血缘,无需再进行map操作获取mapRdd,所以,累加器不会操作
二、RDD作业性能优化
1、基础篇
- 避免创建重复的RDD
对于一个相同的数据,只创建一个RDD。比如读取数据,返回的RDD,那么之后如果要使用最原始的数据,就直接用这个RDD,不要重复再读取数据获取重复的RDD。 - 尽可能多的复用同一个RDD
这里与上面一条有相同的地方,就是相同作用的RDD就不要再创建了。但是不同的地方在于,对于可以复用的RDD就复用。比如RDD1是key-value的,RDD2是value的,如果RDD1仅仅只是给RDD2加了个key,也就是说value一样,那么直接使用RDD1.value就可以,复用一个RDD。 - 对多次使用的RDD进行持久化
在Spark中,一个RDD执行多次算子的默认原理是:每次对一个RDD进行一个算子操作时,分为两步:第一步、从源头重新计算一遍得到当前这个RDD;第二步、进行算子运算
因此,将多次使用的RDD持久化,这样下次使用的时候就不用再重新计算一遍得到这个RDD了。而持久化要根据场景,选择哪种持久化方案,目前所有方案如下:
持久化级别 | 含义 |
MEMORY_ONLY | 使用未序列化的Java对象格式,RDD持久化在内存中。如果内存不够存放RDD的数据,那么RDD就不会被持久化。cache()就是这种持久化。 |
MEMORY_AND_DISK | 使用未序列化的Java对象格式,优先将其放入内存,内存不够就写入磁盘。下次使用RDD,就从内存或磁盘中读取使用。 |
MEMORY_ONLY_SER | 作用同MEMORY_ONLY一样,只是会将RDD序列化。RDD的每一个partition会被序列化成一个字节数组,更节省内存防止内存占用过多导致频繁GC。 |
MEMORY_AND_DISK_SER | 作用同MEMORY_AND_DISK一样,只是会将RDD进行序列化,先写内存,内存不够再写磁盘,防止全部都写入内存,导致频繁的GC。 |
DISK_ONLY | 使用未序列化的Java对象存储,将全部数据写入磁盘文件。 |
MEMORY_ONLY_2、MEMORY_AND_DISK_2等 | 这种是对上述所有的模式后面加个_2,含义就是将所有持久化的数据都复制一份放入其他节点,保证如果数据丢失还可以使用副本,不用再从头计算。 |
场景选择:
- 默认情况下,性能最高的是MEMORY_ONLY,但是对于RDD数据较大的话,容易造成OOM
- 如果MEMORY_ONLY发生了内存溢出,那么应该选择MEMORY_ONLY_SER,也就是将RDD数据序列化再放入内存,这样大大减少了对象数据,降低了内存占用。但是同样也会带来序列化过程的消耗,但是后面的算子是纯内存的不用再重新执行一遍链路获取RDD,性能还是比较高的,但是全部都放入内存仍然有可能带来OOM。
- 上述两种都不行的话,就选择MEMORY_AND_DISK_SER,因为RDD数据已经很大了,直接无序列化存入内存和磁盘,消耗资源太大,所以不建议使用MEMORY_AND_DISK。通过序列化后,减少对象数据节省内存和硬盘空间开销,同时先写内存,不够再写硬盘。
- 一般不建议使用DISK_ONLY和DISK_ONLY_2这种纯磁盘的,因为会导致性能差,还不如重新计算一遍RDD。对于_2这种,还要保存一份到磁盘其他节点,更会产生较大的性能开销比如网络资源。
三、SparkSQL的学习笔记
1、SparkSQL的相关概念
数据表:在Spark中,数据表类似于数据库的表,呈现的形式是DataFrame。可以执行类SQL的操作进行快速使用。在Spark中没有临时表这个概念,也就是说表必须有数据,没有数据的只能是视图。
托管表和非托管表:非托管表就是定磁盘上的若干个文件作为一个数据表来使用。托管表就是在DataFrame上使用saveAsTable函数创建一个数据表时,此时就是托管表,Spark将跟踪托管表的所有相关信息。
2、SparkSQL的相关基础操作语句(SQL)
- 创建表:
基本语句:
CREATE TABLE flights ( DEST_COUNTRY_NAME STRING, ORIGIN_COUNTRY_NAME STRING, count LONG) USING JSON OPTIONS (path '/data/test.json'
其中using语法,未指定格式默认就是HiveSerDe配置,但是Hive SerDes比Spark本身的慢很多。Hive可以使用Stored as语法来指定这是一个Hive表。
通过查询的方式创建表:
CREATE TABLE flights_from_select USING parquet AS SELECT * FROM flights
--如果想要与Hive兼容,也就是Hive也可以进行使用,那么就是
CREATE TABLE IF NOT EXISTS flights_from_select AS SELECT * FROM flights
创建外部表和Hive差不多,Spark将管理表的元数据,但是数据文件不是由Spark管理的。
- 插入表
- 遵循标准SQL语法
- 修改元数据需要刷新表元数据:REFRESH table partitioned_flights
- 删除表
- 不能删除表,只能drop他们。Drop针对托管表那么数据和表定义都会被删除。Drop针对非托管表那么不会删除数据,也无法再按表明引用此数据。
- 视图相关操作
- 创建视图
CREATE VIEW test_view AS SELECT * FROM test WHERE name = 'United States'
- 要求仅在当前会话可用
CREATE TEMP VIEW view_temp AS SELECT * FROM test WHERE name = 'United States
- 全局临时视图
CREATE GLOBAL TEMP VIEW global_view_temp AS SELECT * FROM test WHERE name = 'United States' SHOW TABLES
--在会话结束时会删掉
- 删除视图
DROP VIEW IF EXISTS global_view_temp;
3、SparkSQL相关参数
Property Name | Default | Meaning |
spark.sql.inMemoryColumnar Storage.compressed | True | 如果设置为 true, 则Spark SQL 会根据数据的统 计信息自动为每一列选择压缩编解码器。 |
spark.sql.inMemoryColumnar Storage.batchSize | 10000 | 控制柱状缓存的批处理大小。较大的批处理可 以提高内存利用率和压缩能力, 但在缓存数据时 有OutOfMemoryErrors (OOMs)风险。 |
spark.sql.files.maxPartition Bytes | 134217728 (128M) | 单个分区中的最大字节数 |
spark.sql.files.openCostIn Bytes | 4194304 (4MB) | |
spark.sql.broadcastTimeout | 300 | 广播连接中广播等待时间的超时秒数 (以秒为单 位)。 |
spark.sql.autoBroadcastJoin Threshold | 10485760 (10M) | 命令分析表计算 STA TISTICS noscan已运行 |
spark.sql.shuffle.partitions | 200 | 配置在为连接或聚合shuffle数据时要使用的分区 数 |
在SQL中设置shuffle分区个数的方法:SET spark.sql.shuffle.partitions=20
4、SparkSQL在程序上的操作
4-1、SparkSQL利用程序在Java上进行操作(仅SQL)
- 基本操作篇
- 首先在IDEA上进行代码的编写
- 配置相关依赖
<?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>com.gklearlove.SparkTest</groupId>
<artifactId>SparkDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>fmp-offline</name>
<url>http://maven.apache.org</url>
<!--设置编码格式、java版本等-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<java-version>1.8</java-version>
<kafka.version>0.8.2.2</kafka.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.6.5</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-hive_2.11</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.openx.data</groupId>
<artifactId>JsonSerDe</artifactId>
<version>1.3</version>
</dependency>
<!-- json4s end -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
- 书写代码
package com.gklearlove;
import com.google.inject.internal.cglib.core.$LocalVariablesSorter;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SparkSession;
import org.slf4j.LoggerFactory;
/**
* @author 20018824_郭凯
* @since 2020/8/7
*/
public class SparkTest {
public static void main(String[] args) {
//构建SparkSession,并设置相关参数
SparkSession spark = SparkSession
.builder()
.appName("TestNeo4j")
.config("spark.sql.warehouse.dir", "./spark-warehouse")
.enableHiveSupport().getOrCreate();
//执行sparksql
Dataset<Row> sql = spark.sql("select * from spark_input_test");
spark.sql("insert into spark_input_test values(100,100,'success')");
}
}
4-2、SparkSQL搭配Dataset在程序上的操作
- 创建SparkConf并进行相关配置
SparkConf conf = new SparkConf();
conf.setAppName("TestSpark");
具体配置参数如下:
1、spark.scheduler.mode:
设置方式:conf.set("spark.scheduler.mode", "FAIR");
调度模式主要有两种:FIFO和FAIR。默认情况下,spark的调度模式是FIFO(先进先出),谁先提交谁先执行,后面的任务需要等待前面的任务执行。而FAIR(公平调度)模式,支持在调度池中为任务进行分组,不同的调度池权重不同,任务可以按照权重来决定执行顺序。
2、spark.speculation:
设置方式: conf.set("spark.speculation", "true");
推测执行优化机制采用了典型的以空间换时间的优化策略,它同时启动多个相同的task(备份任务)处理相同的数据块,哪个完成的早,则采用哪个task的结果,这样可以防止拖后腿Task任务出现,进而提高作业计算速度,但是这样却会占用更多的资源。在集群资源紧缺的情况下,设计合理的推测执行机制可在多用少量资源情况下,减少大作业的计算时间。
推测任务是指对于一个stage(阶段)里面拖后腿的Task,会在其他节点的Executor上再次启动这个task,如果其中一个Task实例运行成功则将这个最先完成的Task的计算结果作为最终结果,同时会干掉其他Executor上运行的实例。spark的推测任务默认是关闭的,可通过spark.speculation属性来开启。
3、spark.sql.codegen:
设置方式:conf.set("spark.sql.codegen", "true");
spark sql在每次执行时,先把SQL查询编译为JAVA字节码。针对执行时间长的sql查询或频繁执行的sql查询,此配置能加快查询速度,因为它产生特殊的字节码去执行。但针对很短的(1-2秒)的临时查询,这可能增加开销,因为它必须先编译每一个查询。
spark是一个快速的内存计算框架;同时是一个并行运算的框架。在计算性能调优的时候,除了要考虑广为人知的木桶原理外,还要考虑平行运算的Amdahl定理。 木桶原理又称短板理论,其核心思想是:一只木桶盛水的多少,并不取决于桶壁上最高的那块木块,而是取决于桶壁上最短的那块。将这个理论应用到系统性能优化上,系统的最终性能取决于系统中性能表现最差的组件。例如,即使系统拥有充足的内存资源和CPU资源,但是如果磁盘I/O性能低下,那么系统的总体性能是取决于当前最慢的磁盘I/O速度,而不是当前最优越的CPU或者内存。在这种情况下,如果需要进一步提升系统性能,优化内存或者CPU资源是毫无用处的。只有提高磁盘I/O性能才能对系统的整体性能进行优化。 Amdahl定理,一个计算机科学界的经验法则,因吉恩•阿姆达尔而得名。它代表了处理器平行运算之后效率提升的能力。并行计算中的加速比是用并行前的执行速度和并行后的执行速度之比来表示的,它表示了在并行化之后的效率提升情况。阿姆达尔定律是固定负载(计算总量不变时)时的量化标准。可用公式:\frac{W_s + W_p}{W_s + \frac{W_p}{p}}来表示。式中W_s, W_p分别表示问题规模的串行分量(问题中不能并行化的那一部分)和并行分量,p表示处理器数量。当p\to \infty时,上式的极限是\frac{W}{W_s},其中,{W}={W_s}+{W_p}。这意味着无论我们如何增大处理器数目,加速比是无法高于这个数的。
4、spark.broadcast.compress:
设置方式:conf.set("spark.broadcast.compress", "true");
是否对Broadcast的数据进行压缩,默认值为True。Broadcast机制是用来减少运行每个Task时,所需要发送给TASK的RDD所使用到的相关数据的尺寸,一个Executor只需要在第一个Task启动时,获得一份Broadcast数据,之后的Task都从本地的BlockManager中获取相关数据。在1.1最新版本的代码中,RDD本身也改为以Broadcast的形式发送给Executor(之前的实现RDD本身是随每个任务发送的),因此基本上不太需要显式的决定哪些数据需要broadcast了。因为Broadcast的数据需要通过网络发送,而在Executor端又需要存储在本地BlockMananger中,加上最新的实现,默认RDD通过Boradcast机制发送,因此大大增加了Broadcast变量的比重,所以通过压缩减小尺寸,来减少网络传输开销和内存占用,通常都是有利于提高整体性能的。
5、spark.sql.autoBroadcastJoinThreshold
设置方式:conf.set("spark.sql.autoBroadcastJoinThreshold", "-1");
这个配置的最大字节大小是用于当执行连接时,该表将广播到所有工作节点。通过将此值设置为-1,广播可以被禁用。
6、spark.shuffle.file.buffer
设置方式:conf.set("spark.shuffle.file.buffer", "64k");
默认值:32k
参数说明:该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲中,待缓冲写满之后,才会溢写到磁盘。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如64k),从而减少shuffle write过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
7、spark.reducer.maxSizeInFlight
设置方式:conf.set("spark.reducer.maxSizeInFlight", "96m");
默认值:48m
参数说明:该参数用于设置shuffle read task的buffer缓冲大小,而这个buffer缓冲决定了每次能够拉取多少数据。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
错误:reduce oom
reduce task去map拉数据,reduce 一边拉数据一边聚合 reduce段有一块聚合内存(executor memory * 0.2)
解决办法:1、增加reduce 聚合的内存的比例 设置spark.shuffle.memoryFraction
2、 增加executor memory的大小 --executor-memory 5G
3、减少reduce task每次拉取的数据量 设置spark.reducer.maxSizeInFlight 24m
8、spark.sql.shuffle.partitions
设置方式:
int maxCore = 3 * Integer.parseInt(conf.get("spark.executor.instances")); String maxCoreVal = maxCore + "";
conf.set("spark.sql.shuffle.partitions", maxCoreVal);
Spark.sql.shuffle.partitions是一个参数,它在进行连接或聚合等混洗时决定分区的数量,即节点之间的数据移动。 另一部分spark.default.parallelism将根据您的数据大小和最大块大小计算,HDFS为128mb。 因此,如果您的工作没有进行任何随机播放,则会考虑默认的并行度值,或者如果您使用的是rdd,则可以自行设置。
9、mapred.output.compress
设置方式:conf.set("mapred.output.compress", "false");
reduce的输出是否压缩
10、spark.driver.maxResultSize
设置方式:conf.set("spark.driver.maxResultSize", "10g");
对Spark每个action结果集大小的限制,最少是1M,若设为0则不限制大小。若Job结果超过限制则会异常退出,若结果集限制过大也可能造成OOM问题。
- 创建SparkSession
//开启了Hive的支持
SparkSession sparkSession = SparkSession.builder().config(conf).enableHiveSupport().getOrCreate();
- 编写代码
Dataset<Row> name_result = sparkSession.sql("select user_id,user_name from spark_input_test where user_name is not null");
name_result.createOrReplaceTempView("name_result");
sparkSession.sql("insert into spark_output_test select user_id,substring(user_name,0,1) from name_result");
sparkSession.stop();
5、SparkSQL中DataSet的API(基于Java)
在SpakrSQL中,利用SQL查询出来的内容会返回DataSet,DataSet其实就是DataFrame,只是在Java上用Dataset来表示DataFrame。所以就包含Schema和数据,也就是表结构和数据,其实就是表。
由于DataSet底层仍然是RDD,所以具备了算子运算,也就是转换算子和动作算子。同时,转换算子是惰性的,动作算子会触发所有的操作。并且,转换算子返回的仍然是DataSet,而动作算子返回的是值。
5-1、转换算子
- map算子
//基础map
/*
*map算子的含义就是对Dataset集中的每一个元素都进行操作,也就是每一个元素经过操作后返回新的元素
*/
//下面的程序含义就是将Dataset ds中的每一个值通过map计算出其长度,返回新的dataset
public class TestSparkJava {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().master("local[2]").appName("DataSetEncoderExample").getOrCreate();
List<String> data = Arrays.asList("chen", "li", "huang");
//创建dataset
Dataset<String> ds = spark.createDataset(data, Encoders.STRING());
//转换算子
//1、map算子
Dataset<Integer> map = ds.map(new MapFunction<String, Integer>() {
@Override
public Integer call(String v) throws Exception {
return v.length();
}
}, Encoders.INT());
map.show();
}
}
//结果如下:
+-----+
|value|
+-----+
| 4|
| 2|
| 5|
+-----+
/*
*其中,MapFunction<String, Integer>函数表示输入的类型和输出的类型
*Encoders.INT()表示最后要生成的类型,也就是显示转换
*/
//当然map也可以将每一个值转换成自己的数据类型,也就是对象
//首先,对象:
public class Bike implements Serializable {
private static final long serialVersionUID = 1L;
private String bike_type;
private int bike_money;
public String getBike_type() {
return bike_type;
}
public void setBike_type(String bike_type) {
this.bike_type = bike_type;
}
public int getBike_money() {
return bike_money;
}
public void setBike_money(int bike_money) {
this.bike_money = bike_money;
}
public Bike(String bike_type, int bike_money) {
this.bike_type = bike_type;
this.bike_money = bike_money;
}
}
//转换成对象的代码如下
public class TestSparkJava {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().master("local[2]").appName("DataSetEncoderExample").getOrCreate();
List<String> data = Arrays.asList("chen", "li", "huang");
//创建dataset
Dataset<String> ds = spark.createDataset(data, Encoders.STRING());
//转换算子
//1、map算子
//1-2高级map
Encoder<Bike> bikeEncoders = Encoders.kryo(Bike.class);
Dataset<Bike> map = ds.map(new MapFunction<String, Bike>() {
@Override
public Bike call(String s) throws Exception {
return new Bike(s, s.length());
}
}, bikeEncoders);
map.show();
}
}
//结果
+--------------------+
| value|
+--------------------+
|[01 00 63 6F 6D 2...|
|[01 00 63 6F 6D 2...|
|[01 00 63 6F 6D 2...|
+--------------------+
/*
*其中,Encoder<Bike> bikeEncoders = Encoders.kryo(Bike.class);就是创建在显示转型时的类型
*打印的是对象的字节码,但是效果已经达到了,可以供给之后使用
*/
- flatmap
- mapPartitions
- filter算子
/*
*filter算子的作用就是根据某一条件进行过滤,返回的仍然是Dataset
*/
public class TestSparkJava {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().master("local[2]").appName("DataSetEncoderExample").getOrCreate();
List<Integer> data2 = Arrays.asList(30, 40, 50);
//创建dataset
Dataset<Integer> ds2 = spark.createDataset(data2,Encoders.INT());
//filter算子
Dataset<Integer> value = ds2.filter(ds2.col("value").gt(30));
value.show();
}
}
//结果
+-----+
|value|
+-----+
| 40|
| 50|
+-----+
- groupBy算子
/*
*按照某列或某几列进行分组,然后汇总数据
*/
public class TestSparkJava {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().master("local[2]").appName("DataSetEncoderExample").getOrCreate();
List<String> data = Arrays.asList("chen_as", "li_as", "huang_as");
//创建dataset
Dataset<String> ds = spark.createDataset(data, Encoders.STRING());
Dataset<Row> num = ds.withColumn("num", lit(12));
//转换算子
//4、groupby算子
Dataset<Row> max = num.groupBy("value").max("num");
max.show();
}
}
//结果
+--------+--------+
| value|max(num)|
+--------+--------+
| chen_as| 12|
|huang_as| 12|
| li_as| 12|
+--------+--------+
- groupByKey算子
5-2、动作算子
- collect算子
/*
*将Dataset的全部数据变成一个列表
*/
public class TestSparkJava {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().master("local[2]").appName("DataSetEncoderExample").getOrCreate();
List<String> data = Arrays.asList("chen_as", "li_as", "huang_as");
List<Integer> data2 = Arrays.asList(30, 40, 50);
//创建dataset
Dataset<String> ds = spark.createDataset(data, Encoders.STRING());
Dataset<Integer> ds2 = spark.createDataset(data2,Encoders.INT());
Dataset<Row> num = ds.withColumn("num", lit(12));
Dataset<Row> num2 = num.withColumn("num2", lit(12));
//动作算子
//1、collect算子
System.out.println(java.util.Arrays.toString((Object[]) num2.collect()));
}
}
//结果如下
[[chen_as,12,12], [li_as,12,12], [huang_as,12,12]]
- take算子
/*
*获取前几个row,然后转成列表
*/
public class TestSparkJava {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().master("local[2]").appName("DataSetEncoderExample").getOrCreate();
List<String> data = Arrays.asList("chen_as", "li_as", "huang_as");
List<Integer> data2 = Arrays.asList(30, 40, 50);
//创建dataset
Dataset<String> ds = spark.createDataset(data, Encoders.STRING());
Dataset<Integer> ds2 = spark.createDataset(data2,Encoders.INT());
Dataset<Row> num = ds.withColumn("num", lit(12));
Dataset<Row> num2 = num.withColumn("num2", lit(12));
//2、take算子
System.out.println(Arrays.toString((Object[]) num2.take(2)));
}
}
//结果如下
[[chen_as,12,12], [li_as,12,12]
- count算子
/*
*获取DataSet的获取行数
*/
public class TestSparkJava {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().master("local[2]").appName("DataSetEncoderExample").getOrCreate();
List<String> data = Arrays.asList("chen_as", "li_as", "huang_as");
List<Integer> data2 = Arrays.asList(30, 40, 50);
//创建dataset
Dataset<String> ds = spark.createDataset(data, Encoders.STRING());
Dataset<Integer> ds2 = spark.createDataset(data2,Encoders.INT());
Dataset<Row> num = ds.withColumn("num", lit(12));
Dataset<Row> num2 = num.withColumn("num2", lit(12));
//3、count算子
System.out.println(num2.count());
}
}
//结果如下:
3
- foreach算子
/*
*foreach算子的作用是对Dataset的每一行进行操作
*/
public class TestSparkJava {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().master("local[2]").appName("DataSetEncoderExample").getOrCreate();
List<String> data = Arrays.asList("chen_as", "li_as", "huang_as");
List<Integer> data2 = Arrays.asList(30, 40, 50);
//创建dataset
Dataset<String> ds = spark.createDataset(data, Encoders.STRING());
Dataset<Integer> ds2 = spark.createDataset(data2,Encoders.INT());
Dataset<Row> num = ds.withColumn("num", lit(12));
Dataset<Row> num2 = num.withColumn("num2", lit(12));
//4、foreach算子
final Data data1 = new Data();
num2.foreach(new ForeachFunction<Row>() {
@Override
public void call(Row row) throws Exception {
String value = row.getAs("value");
int num = row.getAs("num");
int num2 = row.getAs("num2");
data1.setValue(value);
data1.setNum(num);
data1.setNum2(num2);
System.out.println(data1.toString());
}
});
}
}
//最后的输出结果
Data{value='li_as', num=12, num2=12}
Data{value='huang_as', num=12, num2=12}
Data{value='chen_as', num=12, num2=12}
- foreachPartition算子
/*
*foreachPartition算子的作用就是对每一个分区进行操作
*/
6、SparkSQL调优
6-1、基本SQL调优
- 查询时尽量避免全表扫面,首先应该考虑where及order by上建立索引。
6-2、参数调优
- 查询调优
set spark.sql.codegen=true --默认是false,开启后让每条查询语句编译成java的二进制代码,在大型查询的时候,性能好,但是小规模查询的时候反而慢。
- 任务调优(写在一起的就是要同时开启)
set spark.shuffle.service.enabled=true; --因为Executor进程除了运行task,还负责写shuffle数据,给其他Excutor提供shuffle数据,但是如果task任务过重,就会影响提供shuffle数据从而影响其他Executor,所以开启后,由辅助进程External shuffle Service来专门负责操作shuffle数据。
set spark.dynamicAllocation.enabled=true; --要同时开启上面的参数,默认为false,开启后将开启可以动态的删除它们并且不用删除它们产生的shuffle文件,动态资源分配,防止资源浪费。也就是说executor在超过60s没有使用时就会被自动回收。但是即使回收,数量也不会低于spark.dynamicAllocation.minExecutors配置的个数。同时,资源不足时会自动向yarn进行申请,但是不会超过spark.dynamicAllocation.maxExecutors个数。
set spark.dynamicAllocation.executorIdleTimeout=时间; --如果需要修改动态分配时executor回收的超出时间,就要修改这个参数,默认是60s。
set spark.dynamicAllocation.minExecutors=个数; --与上述参数搭配使用,默认为0,也就是动态分配时executor的最小个数
set spark.dynamicAllocation.maxExecutors=个数; --与上上述参数搭配使用,默认为--num-executors个数,也就是动态分配时exector个数的上限
set spark.sql.autoBroadcastJoinThreshold=大小; --用户表的关联,类似于Hive的mapjoin,默认值是10485760(10M),改成-1时表示关闭。也就是当表的数据流小于这个大小时,被认为是小表,然后小表先进内存映射大表。
set spark.sql.broadcastTimeout=时间; --默认是5min,一般搭配上个参数使用,也就是当用小表映射大表时,超出这个时间就会被认为超时,任务就会失败。
set spark.sql.adaptive.enabled=true; --(下面都配置都要开启这个)默认是false,开启后,Spark的task就可以根据spark.sql.adaptive.shuffle.targetPostShuffleInputSize设置的工作量来自适应设置并行度,如果不开启的话,就不管数据量的多少,统一按指定并行来执行,这样的话,如果数据流特别大,就容易造成oom情况,如果数据量很小,就会消耗资源。
set spark.sql.adaptive.shuffle.targetPostShuffleInputSize=大小; --在开启上面的参数后进行设置,也就是一个task的数据量大小,默认是64M,设置好后,就可以根据此参数自动设置并行度。
set spark.sql.adaptive.skewedJoin.enabled=true; --默认是false,在adaptive.enabled开启后决定是否开启在join时自动处理数据倾斜
set spark.sql.adaptive.skewedPartitionMaxSplits=个数; --默认是5,用来控制一个倾斜的partition的Task个数的上限
set spark.sql.adaptive.skewedPartitionRowCountThreshold=行数; --默认是 10L * 1000 * 1000也就是一千万,数据倾斜行数的下限。就是说当数据量的行数小于这个的时候就不会被认为是数据倾斜的任务
set spark.sql.ataptive.skewedPartitionSizeThreshold=大小; --默认是64 * 1024 * 1024也就是64M,数据倾斜大小的下限,也就是说,档大小小于这个时,不会认为是数据倾斜的任务。
set spark.sql.adaptive.skewedPartitionFactor=个数; --默认是10,也就是再上述两个参数的基础上深化一下数据倾斜的判断,也就是如果数据的行数大于了spark.sql.adaptive.skewedPartitionRowCountThreshold设置的值,且大于每个partition行数的中位数*指定的个数的话,就会被认为是数据倾斜。同理,如果数据的大小大于了spark.sql.ataptive.skewedPartitionSizeThreshold,且大于每个partition的大小的中位数*指定的数,那么这个任务就会被认为是数据倾斜。
set spark.sql.adaptive.repartition.enabled=true;
set spark.sql.adaptive.minNumPostShufflePartitions=1;
--这两个参数的作用是开启小文件合并,并且指定shuffle的最小并发分区数
set spark.sql.join.preferSortMergeJoin=false; --默认为true就是优先采用SortMergeJoin,会排序,但是相对慢一些,对数据量大的效果明显。对于小的数据量,设置为false后,就会优先采用ShuffleHashJoin,不会排序,速度较快一些。
set spark.sql.inMemoryColumnarStorage.compressed=true; --默认为true,开启后SparkSQL会基于统计信息自动地为每一列选择一种压缩编码方式。
spark.sql.inMemoryColumnarStorage.batchSize=大小; --默认为10000 缓存处理大小,也就是批处理在内存的缓存大小,调大可以提高效率,但可能带来OOM,超出内存限制的风险。
set spark.sql.files.openCostInBytes=大小; --默认是4M,开启后就会估算打开文件的成本,也就是文件的大小,这样的话会优先打开小文件分区,对于往一个分区写入多个文件效果好。
--注意:默认为True的表示默认是打开的是使用的。但是默认值的不一定是打开的,要手动set一下才会打开。
三、Spark核心概念之Shuffle
- 在Spark中shuffle就是stage之间的相关操作,具体可以由stage write和stage read组成
- Shuffle可以分为:Hash shuffle和Sort Shuffle,因此shuffle引擎就可以分为Hash shuffleManager和Sort ShuffleManager,下面是其区别
- Hashshuffle:
- 会产生大量的中间磁盘文件,进而影响IO影响性能。
- Spark1.2之前的默认版本,从1.2后就不再默认
- SortShuffle
- 虽然也会产生中间较多的临时磁盘文件,但是最后会合并成一个磁盘文件
- 下一个stage可以利用索引获取需要的部分文件数据
- 一个Task最后只有一个磁盘文件
- Hashshuffle详解
- 运行机制可以分为两种:
- 普通运行机制:
- shuffle write阶段:
在一个stage结束后,为下一个stage创建。将stage中每个task处理的数据按key进行分区。所谓分区就是对相同的key执行hash算法,从而将相同key写入同一个磁盘文件中,然后每个磁盘文件属于下一个stage的一个task。
那么需要创建多少个文件呢?
下一个task有多少个,当前stage的每个task就得创建多少个磁盘文件。
比如下一个stage有100个task。当前stage有10个Executor,每个Exector执行5个task。那么会创建10x5x100=5000个小文件,所以当规模变大时,性能极差。
2. shuffle read阶段:
此时的stage的每一个task都都要从上游的所有stage的所有task根据相同key拉取数据,然后进行聚合或连接等操作。shuffle read的拉取过程是一边拉取一遍聚合的。
缺点:
1. 产生大量小文件,建立通信和拉取数据次数变多,消耗IO。
2. 可能导致OOM,内存耗尽。因为需要在内存中保存海量文件操作句柄和临时信息。
- 合并运行机制
开启合并运行机制的方法:spark.shuffle.consolidateFiles,设置为true,默认为false
设置为true后开启优化机制,也就是复用buffer。
相较于普通运行机制,改进的地方就是在shuffle write阶段,就是让stage中的Executor不管有多少个task最后给下一个stage的某一个task的文件个数为1,也就是一个stage给下一个stage的某个task的文件个数为exector个数。
1. 在一个stage结束后,为下一个stage创建。将stage中每个task处理的数据按key进行分区。所谓分区就是对相同的key执行hash算法,从而将相同key写入同一个磁盘文件中,然后每个磁盘文件属于下一个stage的一个task。
那么需要创建多少个文件呢?
下一个task有多少个,当前stage的每个task就得创建多少个磁盘文件,但是同一个exector流向同一个下游task会合并,最后会合并成一个。
比如下一个stage有100个task。当前stage有10个Executor,每个Exector执行5个task。此时会创建10*100=1000小文件,所以当规模变大时,性能极差。
- SortShuffle
运行机制可以分为:普通运行机制和bypass运行机制,其中当shuffle read task的数量小于等于spark.shuffle.sort.bypassMergeThreshold参数的值时(默认为200),就会启用bypass机制
- 普通运行机制
- sortshuffle与Hashshuffle的区别就在于sortshuffle最终的task只会产出一份磁盘文件,不管下游有多少个task,因为他会同时产生索引文件来获取指定task的内容。
- 如图所示,其原理就是:
1. 数据首先写到内存数据结构中(默认5m),根据不同的shuffle算子,有不同的数据结构,如果是reduceBykey这种聚合类算子,就是map数据结构,一遍聚合一边写入内存。如果是join这种普通算子,就会选用Array数据结构,直接写入内存。当内存超出阈值后将其写入磁盘。
- 注意:内存数据结构的大小是动态改变的:当超出内存数据结构大小时,会申请内存,大小为:当前内存数据结构存储的数据*2-内存数据结构的设定值,如果申请到了就设置,没有就溢出。
2. 写入磁盘之前,首选进行排序然后写入内存缓冲,当内存缓冲写满之后,刷新到磁盘文件中
3. 在结束之前会将所有的磁盘上的临时小文件进行合并,最后产出一个文件,减少了过多文件带来的不好影响。
4. 最后会同时输出一个索引文件
- bypass运行机制
- 运行机制触发的条件(都满足)
1. shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值
2. 不是聚合类的shuffle算子(比如reduceByKey)
- 与未优化的HashShuffle基本一致,只是在最后会将所有的临时小文件合并成一个磁盘文件并产出索引文件。
- 与SortShuffle的普通运行机制相比:
1. 磁盘写机制不同
2. 不会进行排序,也是bypass机制的最大好处,不需要排序,节省了部分性能开销。
最后,shuffle的性能决定了spark的性能,在1.2之前默认采用的是HashShuffle,会产生大量小文件,性能太差,从1.2后就采用了sorshuffle。
四、Spark核心概念之SparkContext、SparkSession、SparkConf等
- SparkSession:在Spark2.x引入的概念,为用户提供统一的切入点,就是创建会话或者连接spark,将以前的HiveContext、SQLContext、StreamingContext全部集成在一起。
- SparkContext:适用于Spark1.x时代,用于和Spark进行连接,在2.x时,已经集成入SparkSession中,用于与Spark集群进行连接,通过它与一些Spark的低级API比如RDD、累加器、广播变量等进行通信,操作。
- SparkConf:配置SparkContext连接Spark的相关配置,比如配置App名字,配置线程数量等。