1. IO流中的BufferedInputStream体现出装饰者设计模式。
1)装饰者设计模式由装饰者和被装饰者两部分组成;
2)装饰者扩增被装饰者的功能,BufferedInputStream(装)增加了FileInputStream(被装)的功能,
由一个字节一个字节的读取变成一次读取一批数据;
3)装饰者与被装饰者有相同的方法,这样装饰者使用方法时,就感觉使用被装饰者的方法一样,只不过对功能进行了扩增。
比如如果是文本,还可以进一步包装,对字节流(FileInpuStream)包装成InputStreamReader(一次读一行),再包装成缓冲流。
2. 在RDD中也体现了装饰者设计模式,RDD是一个抽象类,RDD的方法才叫算子(sc.textFile不叫算子,他不是RDD的方法)。
3. IO流BufferedInputStream只有在调用read方式才真正执行,RDD也是在调用collect方法时才真正执行,都是体现了装饰者设计模式,懒加载。
4. RDD算子相关的代码在Executor执行,RDD算子外的代码在Driver执行。
5. 从HDFS上读取数据,如果放在集合中,那么会实实在在存放在内存中,但是RDD不是真正的把数据拿过来,而存放着业务逻辑。
6. 执行流程:对于sc.textFile("路径").flatmap(_split(" ")).map((_,1)).reduceByKey(_+_).collect
1)客户端通过sparkSubmit提交jar给RM,发送一个请求。
2)RM对请求进行处理,将客户端提交的jar包做一个封装,封装成AppMaster。
3)AppMaster需要在NM上创建Container运行,AppMaster启动一个Driver线程,处理客户端的请求,协调Executor资源;
4)Executor创建好后,需要在Driver上反向注册,Driver就知道有多少个Executor为他服务;
5)RDD算子相关的代码在Executor执行,RDD算子外的代码在Driver执行。RDD是抽象的业务逻辑。
6)对于sc.textFile是在Driver上执行,首先会new HadoopRDD()出来,RDD叫弹性分布式数据集。比如读取时有大量的数据,
不可能都交给一个Executor去处理,会经过一定的计算规则,将数据分区,不同的分区分为不同Executor。
7)RDD不一定在一个节点上,他可以跨多个节点,每个Executor处理不同分区的内容,这样就有多个节点并行执行,所以叫分布式;
8)flatmap(_split(" ")),执行flatmap时,会创建一个新的RDD,new MapPartitionsRDD(this,..)
9)reduceByKey需要等所有分区的数据计算完毕才能执行,比如有200个分区,有199个执行完毕,reduceByKey做汇总操作,
不能一直等待,所以需要落盘,避免内存溢出。落盘就是Shuffle过程。
9)Shuffle落盘可以理解为写的过程,reduceByKey从磁盘读取数据可以理解为读的过程。
10)collect触发行动执行,是行动算子。行动算子调用之前的算子并未真正执行,只是封装业务逻辑。相当于装饰者不断装饰被装饰者,
(new ...(new MapPartitionsRDD(new HadoopRDD(),..)..),只能真正调用collect才执行,与BufferedInputStream调用read一样。
11)spark是基于内存的计算,适合迭代式计算,中间结果不落盘,但是RDD处理过程中有Shuffle还是会落盘的。
MR计算时也是在内存中,但是不适合迭代式计算,因为中间结果落盘。
12)一般算子做统计时需要落盘,需要等待其他分区的数据,将数据打乱重组,像..By..。
13)不同的分区放在不同的Executor上执行。
7. RDD特性:RDD抽象类中定义的5个抽象方法
1)一组分区:RDD的基本组成单位,通过getPartitions()获取分区
2)计算每个分区的函数:compute()计算数据流向哪个分区,计算单值,分区计算函数。
3) RDD之间的依赖关系:得到一个分区可能需要多个RDD,将多个RDD之间的关系成为血缘,通过getDependecies()获取RDD之间
的依赖关系(宽依赖,窄依赖),从而判断哪些RDD需要落盘。
4)一个Partitoner:不是所有RDD都有,只针对键值对才有,要是RDD处理单值就没有。默认的分区器是HashPartitioner。
5)getPreferredLocations(split:Partition)记录当前数据的存储位置(RDD的数据从哪里来),每个Patition的优先位置。
一个节点既是DN,又是NM,RDD执行需要在NM上的Executor上,分配任务时就将Executor分配到有数据的节点。
移动数据不如移动计算,除非资源不够。当资源不够时,不是立即移动资源,需要有一个等待时间。比如从102移动资源需要
花费10秒,但是2秒后102就有资源可以计算,那么就等待102一会。
8. RDD中的方法叫算子,算子分为两类:
1)转换算子,并不真正执行,只是进行逻辑转换,每次都会创建新的RDD;
2)行动算子,真正执行,懒加载,延时计算。
9. 创建RDD的三种方式:
1)集合:集合是在内存中,RDD从内存中读取数据,因为集合存储数据量有限,所以只是在测试环境中用。
2)从外部储存创建RDD:磁盘或HDFS
10. 抽象类RDD中的模板方法设计模式
RDD中只是定义了计算分区的业务逻辑,一套模板,但是具体怎么获取分区没有实现,具体在子类中实现,
//模板方法
protected def getPartitions: Array[Partition]
/**
* Get the array of partitions of this RDD, taking into account whether the
* RDD is checkpointed or not.
*/
final def partitions: Array[Partition] = {
checkpointRDD.map(_.partitions).getOrElse {
if (partitions_ == null) {
stateLock.synchronized {
if (partitions_ == null) {
//获取分区
partitions_ = getPartitions
partitions_.zipWithIndex.foreach { case (partition, index) =>
require(partition.index == index,
s"partitions($index).partition == ${partition.index}, but it should equal $index")
}
}
}
}
partitions_
}
}
11. 转换算子分类:当你调用时会帮你创建一个新的RDD,并不会执行真正的计算操作,只是做计算逻辑的封装,只有行动算子触发后,才会做真正的计算。
1)value类型:RDD内存放单个值
2)双value类型:指将两个RDD的值放在一起做运算
3)keyvalue类型:RDD内存放着一对对键值对
12. mapPartitions:相当于批处理,一次处理一个分区的数据,内部再调用map函数,尽管处理次数没变,但是与数据库的连接次数大幅度减少,
相当于缓冲,连接是一个重量级的操作,消耗性能。当一次把一个分区的数据拿过来,再在函数内部进行处理,比每次只取一个,
效率明显提高。
注意:批量处理也不一定好,因为是一次把一个分区的数据放到内存中,对服务器内存要求高。当服务器内存资源不足的情况下,
还是应该使用map。
13. mapPartitions:
1)一般适用于批处理的操作,比如:将RDD中的元素插入到数据库中,需要数据库连接,
如果每一个元素都创建一个连接,效率很低,可以对每个分区的元素,创建一个连接
2)rdd.mapPartitions(datas => datas.map(_ * 2)) 中datas.map(_ * 2)的map不叫算子,他只是集合中的方法,只有RDD中的方法叫算子,
他只是在将一个分区的数据拿到后,当做一个集合(实实在在的集合)进行集合中map方法。
14. Spark是基于内存的,不一定Spark一定比MR好,他们各自有应用场景,内存是有天花板的。有些数据量很大,spark无法解决的场景,还是得用MR。
15.mapPartitionsWithIndex:以分区为单位,对RDD中的元素进行映射,并且带分区编号
1)解读index为分区号,datas为一组分区,对一组分区的数据map遍历,转变数据类型,添加上分区号,当然也可以不加。
rdd.mapPartitionsWithIndex((index,datas) => datas.map((index,_)))
2)rdd.mapPartitionsWithIndex((index,datas)=>datas) 什么处理都不做,原封不动输出
16. flatmap::对集合中的元素进行扁平化处理(把整个转化为个体),但是flatMap只能对元素为集合的进行扁平化,如果不是,则报错
val rdd1: RDD[Any] = sc.makeRDD(List(List(1, 2), List(3, 4), List(5, 6), List(7, 8), 9), 2)
val newRDD1: RDD[Int] = rdd1.flatMap(datas => datas) //error
17. glom(): 分区转换为数组(个体转换为整体),因为数组放在内存中,如果数据量大内存可能装不下。
注意:由于由于将分区转换为数组,所以对于分区这个集合他里面的元素就是一个数组,数组里面再装一个个分区元素。
18. 创建RDD分区方式:
1)通过内存集合创建
1.1)默认的分区方式:取决于分配给当前应用的CPU核数
1.2)指定分区:通过positions方法决定
2)读取外部文件创建
1.1)默认的分区方式:取决于分配给当前应用的CPU核数和2的最小值
1.2)指定分区:
》FileInputFormat --- getSplits 切片方法
》LineRecordReader --- next 读取
19. RDD的常用算子
1)转换算子Transformation:转换算子执行完毕之后,会创建新的RDD,并不会马上执行计算
1.1)map:对RDD中的元素进行一个个映射
1.2)mapPartitions:以分区为单位,对RDD中的元素进行映射
1.3)mapPartitionsWithIndex:以分区为单位,对RDD中的元素进行映射,并且带分区编号
1.4)flatMap:对RDD中的元素进行扁平化处理
1.5)glom:将RDD中每一个分区中的单个元素,转换为数组
1.6)groupBy:按照一定的规则,对RDD中的元素进行分组,注意分组与分区不同。
1.7) filter: 按照一定的规则,对RDD中的元素进行过滤
1.8)sample:不可以控制抽样个数
>参数1: 是否抽样放回 true 放回 fasle 不放回
>参数2:
参数1:true 期望元素出现的次数 >0
参数2:false 每一个元素出现的概率 [0,1]
>参数3: 随机算法的初始值(种子)
>takeSample(行动算子):可以控制抽样个数
1.9) distinct:去重
底层是通过map + reduceByKey完成去重操作
1.10)改变分区:
>coalesce:一般用于缩减分区,默认不执行Shuffle,当然可以叫Shuffle设置为true,这样就可以扩大分区
>repartition:一般用于扩大分区,默认执行Shuffle,底层调用的就是coalesce。
1.11)sortByKey:按照指定规则,对RDD中的元素进行排序,默认升序。
1.12)pipe:对于RDD中的每一个分区,都会执行pipe算子中指定的脚本,将每个分区中的每个元素传递给脚本处理
1.13) union合集
1.14)intersection交集
1.15) subtract 差集
1.16)zip拉链:必须要保证分区数以及每一个分区中元素的个数一致。
1.17)partitionBy:按照指定的分区器,通过key对RDD中的元素进行分区,默认是HashPartitioner,可以自定义分区器
1.18)reduceByKey:将相同的key放在一起,对value进行聚合操作
1.19)groupByKey:按照key对RDD中的元素进行分组
1.20)aggregateByKey(zeroValue)(分区内计算规则,分区间计算规则)
1.21)foldByKey(zeroValue)(内间计算规则),是aggregateByKey的简化,区内和分区间计算规则相同,
1.22)combineByKey(对当前key的第一个value进行转换,分区内计算规则,分区间计算规则)
1.23)几种聚合算子对比:
>reduceByKey(_+_):预聚合combiner
combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
>aggregateByKey(zeroValue)(cleanedSeqOp,combOp): cleanedSeqOp:分区内规则,combOp:分区间规则
combineByKeyWithClassTag[U]((v: V) => cleanedSeqOp(createZero(), v),cleanedSeqOp, combOp, partitioner)
>foldByKey:
combineByKeyWithClassTag[V]((v: V) => cleanedFunc(createZero(), v),cleanedFunc, cleanedFunc, partitioner)
>combineByKey:
combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners, defaultPartitioner(self))
1.24)sortByKey:按照RDD中的key对元素进行排序
1.25)mapValues:只对RDD中的Value进行操作
1.26)join&cogroup
2)行动算子Action:行动算子执行后,才会触发计算。
1.27)reduce:对RDD中的元素进行聚合
1.28)collect.foreach 和 foreach
>collect.foreach:将每一个Executor中的数据收集到Driver,形成一个新的数组,.foreach不是一个算子,是集合中的方法,是对数组中的元素进行遍历
>foreach:对RDD中的元素进行遍历
1.29)count:获取RDD中元素的个数
1.30)countByKey:获取RDD中每个key对应的元素个数
1.31)first:获取RDD中第一个元素
1.32)take:获取RDD中的前几个元素
1.33)takeOrdered:获取排序后的RDD中的前几个元素
1.34)aggregate&fold
>aggregateByKey:处理kv类型的RDD,并且在进行分区间聚合的时候,初始值不参与运算
>fold 是aggregate的简化版
1.35)save相关的算子:
>saveAsTextFile
>saveAsObjectFile
>saveAsSequenceFile(只针对kv类型的RDD)
20. 分区影响的是开多少个Executor来执行计算,对我们执行程序没有任何影响,不管是开4个,还是2个,对每个分区执行的操作都是相同的。
21. 计算机的随机数是伪随机,是根据随机算法参数的。
22. sample:随机抽样
1)创建Random对象,如果没有给初始值(seed),就会默认生成一个以时间为单位的初始值,作为随机算法的初始值
2)种子就是随机算法的初始值
23. filter过滤RDD中的元素,没有提供缩减分区的参数。比如100W条数据,过滤了50W,那么就没有必要需要之前那么多分区。
注意:分区不是越多越好,比如100G的数据,给10个分区可以,但是100KB的数据,给10个分区就浪费了。
24. 判断某个算子是否经过Shuffle的两种方法:
1) 让程序sleep一会,看localhost:4040,是否出现Shuffle;
2) 看底层源码是否创建ShuffledRDD对象
3) 一般xxxByKey,排序算子需要经过Shuffle
25. reduceByKey有重载的方法,可以指定分区数。因为是聚合操作,所以可以减少分区数。比如以前100个数据,reduce之后50个,就可以减少分区了。
1)先分区内聚合,为了合并不同分区的数据,必须落盘(Shuffle);
2)落盘后拿取相同的key到同一分区,再进行聚合。
26. 对落盘的理解:1)当RDD中的操作不需要依赖其他分区的数据,比如map,那么他就可以一直在内存中执行下去,进入下一个RDD。
2)但是当要RDD算子要执行分组、排序等操作,只在一个分区中无法完成,必须依赖于所有分区的数据,不能在内存中等,因为对内存压力很大,
所以需要落盘。落盘就要经历Shuffle的过程,落盘后就可以根据分区器或分区数同一组key进去指定分区(默认HashPartitioner)
27. aggregateByKey:按照key对分区内和分区间的数据进行处理,适合于分区内与分区间之间不同业务逻辑的聚合,且可以赋初始值。
1)场景举例:比如求出每个分区中同一组key的最大值,再对不同分区聚合求和。
2)如果分区内和分区间的计算逻辑相同,就可以使用reduceBykey
28. foldByKey是aggregateByKey的简化版,当分区内和分区间的业务逻辑相同时,可以简写成foldByKey,但是还是有初始值的,
reduceByKey没有初始值。
29. reduce与reduceByKey计算逻辑都是相同的,先聚合分区内,再聚合分区间元素。只不过reduceByKey只针对于KV类型,reduce可针对于单值类型。
30. Spark存在序列化,但是并不是所有的序列化都使用Kryo序列化。java序列化是比较重的,会包含header,继承体系等。
比如对于一个自定义的user(name,age),java序列化之后是87字节,Kryo序列化之后是10字节。
1)Kryo序列化效率这么高,但是Spark2.0只是部分序列化使用Kryo。是因为Kryo不能控制不参与序列化的属性,
比如对name加上transient关键字,java序列化之后是44字节,而Kryo序列化之后还是10字节,与之前没有变化。
2)目前使用的Spark版本,既有java序列化,也有Kryo序列化。也许在未来Spark像Hadoop一样实现自己的序列化,
或者Kryo序列化更加完善。
3)Spark使用Kryo的场景是,RDD在Shuffle数据时,简单数据类型,数组和字符串类型,这些Spark已经实现了。
4)Serializable接口(特质)是一个标识性接口,说明他是可以序列化的,里面没有任何东西。
trait Serializable extends scala.Any with java.io.Serializable
31. 在序列化时,在属性前加上transient关键字,用transient修饰的属性是不参与序列化的。
32. 查看RDD的血缘关系以及依赖关系
1)血缘关系:toDebugString
2)依赖关系:dependencies
>窄依赖:父RDD一个分区中的数据,还是交给子RDD的一个分区处理
>宽依赖:父RDD一个分区中的数据,交给子RDD的多个分区处理
33. Spark的job调度
1)集群(Standalone | Yarn):一个Spark集群可以同时运行多个Spark应用
2)应用:
*我们所编写的完成某些功能的程序
*一个应用可以并发的运行多个Job
3)Job:
*job对应着我们应用中的行动算子:每次执行一个行动算子,都提交一个job
*一个job由多个stage组成
4)stage:
*一个宽依赖做一次阶段的划分
*阶段个数 = 宽依赖个数 + 1
*一个stage由多个task组成。
5)task:每一个阶段最后一个RDD的分区数,就是当前阶段的task个数。
34. 关于分区、task和Executor的理解?
1)不同分区交给不同的Executor执行,这只是简单的理解,其实在分区和Executor之间还有Task,在每一个阶段的最后一个RDD,
看他的分区个数,每一个分区对应创建一个task,把task放在Executor上执行,其实真正提交给执行器,
让Executor帮我们执行的是封装的task。任务到底有多少个?就是每个阶段最后一个RDD的分区个数。同时每个阶段的分区数
可能是不一样的,就是通过每个阶段最后一个RDD的分区数确定task个数。
2)一个Executor可以运行多个task。比如一个job有2个阶段,阶段0有2个task,阶段1有3个task,那么他会同时
将5个task交给Executor执行。(2个executor可以在同一个节点,也可以在不同节点上)。在executor上运行task,
一个executor运行可以运行多个task。注意executor与hadoop的MapTask不一样,executor的效率要更高一些,因为
hadoop每次执行一个MapTask就会开启一个进程,而spark是在启动程序的时候就把相应的executor启动好,比如现在
需要4个Executor,这4个Executor就已经启动好了,严格说是ExecutorBackend进程,然后任务是多个,其实就是并行计算,
他会在Executor上开启线程去处理,就像现在我们处理的Executor任务其实就是ExecutorBackend上的线程,他在这里开启多个
线程来做并行处理,一个是开进程,一个是开线程,当然是开多线程的效率会更高一些。
3)在yarn集群模式下,在NM上开启开启一个ExecutorBackend进程,一个ExecutorBackend内可以创建多个Executor对象。
一个Executor上可以运行多个task,其实task就是一个个对象,就是开启相应的线程运行task对象。通过开线程达到并行。
35. 一般数据传输的格式:
1)json:本质还是文本文件,用[]表示多条json文件,用{}表示一条json数据,属性与值之间用:分割,多个属性之间用,分割。
[{"name":"zhangsan","age":"18"},{"name":"lisi","age":"20"}]
2)xml
36. RDD的持久化:
1)缓存:程序结束,就清除缓存,无论是内存还是磁盘
1.1)cache:底层调用的就是persist,默认存储在内存中,增加副本没有意义,只是会消耗内存
1.2)persist:可以通过参数指定存储级别
2)检查点checkponit:
2.1)可以当做缓存理解,存储在HDFS上更稳定
2.2)为了避免容错执行时间过长。
3) 缓存不会切断血缘,但是检查点会切断血缘
4)由于checkpoint会重头计算一遍,所以生产上通常选择checkpoint和cache一起使用。
37. 数据的读取和保存:
1)textFile
2)sequenceFile
3)objectFile
4)Json:本质还是通过textFile读取文本,对读到的内容进行处理
5)HDFS
6)MySQL:map--mapPartition foreach--foreachPartition
38. 在Spark里面,有三大结构:
1)RDD:弹性分布式数据集
2)累加器:分布式共享只写变量
3)广播变量:分布式共享只读变量
39. 累加器:分布式共享只写变量
1)累加器用来对信息进行聚合,并不仅仅是加法操作
40. RDD是一个抽象的概念,体现是一个抽象类,在其下面有很多实现类,比如hadoopRDD,JdbcRDD。不同的实现其对应的分区规则是不同的,
比如JDBCRDD的分区规则与makeRDD/textFile创建的RDD的分区规则是不同的。具体取决于getPartitions和compute对分区数据的计算。
41. 为什么stage的个数等于宽依赖的个数+1?因为在Spark源码中,首先为会当前RDD创建一个阶段叫resultStage,然后会根据宽依赖的个数,
为每一个宽依赖创建一个ShuffleMapStage,所以stage的个数等于宽依赖的个数+1,这个1就是resultStage。
42. 在真正提交任务的时候,会对Stage的粒度进一步划分,每一个Stage划分为最后一个RDD的task数。