声明: 1. 本文为我的个人复习总结, 并非那种从零基础开始普及知识 内容详细全面, 言辞官方的文章
2. 由于是个人总结, 所以用最精简的话语来写文章
3. 若有错误不当之处, 请指出
Spark比MapReduce更适合迭代式多任务计算:
MapReduce多个作业间的数据通信是基于磁盘, 而Sparke多个作业间的数据通信是基于内存
一个MapReduce程序只有map+reduce, 而Spark程序可以有多个算子
MapReduce是批处理, 而Spark是批处理 & 微批准实时处理
Shuffle中Spark不用落盘, 而MapReduce要磁盘
Spark有丰富的RDD算子, SQL操作, Streaming流处理, 还可以处理机器学习, 而MapReduce只有Mapper和Reducer
Spark有容错机制可以切断血缘, 避免失败后从源头数据开始全部重新计算
Spark 不能完全替代 MapReduce, 因为内存不充足时, Spark就无法工作了
Driver
是程序的入口, 是任务的调度者
功能:
Executor
执行Task
对于Standalone独立部署模式, Master(相当于ResourceManager)和Worker(相当于NodeManager)来负责资源的管理调度
整个集群并行执行任务的数量
称之为并行度
DAG 有向无环图, 是高度抽象后的 单向无闭环的任务流程图, 用于表示程序的拓扑结构
Yarn Client模式(用于测试)
Driver模块的计算运行在本地
Yarn Cluster模式(生产环境)
Driver模块的计算运行在Yarn
客户端向ResourceManager申请启动Driver(ApplicationMaster)
ResourceManager分配Container, 在合适的NodeManager上启动Driver(ApplicationMaster)
Driver(ApplicationMaster)向ResourceManager申请Executor需要的内存; ResourceManager进行分配Container, 然后在合适的NodeManager上启动Executor
Executor进程启动后会向Driver反向注册, 当所有Executor全部注册完成后Driver开始执行main函数
执行到Action算子时触发一个Job, 并根据宽依赖划分stage, 并生成对应的TaskSet, 之后将Task分配给Executor执行
弹性
容错的弹性, 有持久化机制, 数据丢失后可以自动恢复; 且可以切断血缘避免对父级的依赖, 减少重复计算
计算的弹性, 计算失败后自动重试
存储的弹性: 自动切换 内存和磁盘 去存储数据
分片的弹性:可根据需要重新分片
先将数据集分片, 然后将各个分片放到各个分区
分布式
数据集:RDD封装了计算逻辑
,并不保存数据
数据抽象:RDD是一个抽象类,需要子类具体实现
不可变:RDD是不可变的, 要想改变只能产生新的RDD
可分区、各分区间是并行计算的
转换(Transform)算子, 并不会触发Job的执行
行动(Action)算子, 真正触发Job的执行
算子以外的代码都是在Driver端执行, 算子里面的代码都是在Executor端执行
单Value类型:
map
以每条数据
为单位将数据发到Executor端
rdd.map(num => num * 2)
mapPartitions
以每个分区
为单位将数据发到Executor端
rdd.mapPartitions(datas => datas.filter(_%2==0))
mapPartitionsWithIndex
在mapPartitions基础上多了一个参数index, 即当前分区的索引序号
rdd.mapPartitions((index,datas) => datas.filter(index==0))
flatMap
扁平化处理, 输入参数循环下来有多个List, 而输出结果只有一个List
val dataRDD = sparkContext.makeRDD(List(
List(1,2),List(3,4)
),1)
// 计算结果 1,2,3,4
val dataRDD1 = dataRDD.flatMap(
list => list
)
glom
将同一个分区的数据
转换为同类型的数组
groupBy
会产生Shuffle, 数据被打乱分配到各个分区
一个组的数据在一个分区中, 一个分区中可以有多个组
filter
返回true/false来进行过滤
有些分区的数据过滤掉太多或太少的话, 可能会导致该分区发生数据倾斜
sample
根据一些规则进行随机抽取元素
抽取数据不放回(伯努利算法)
抽取数据放回(泊松算法)
distinct
去重
coalesce
缩减分区数量
会产生Shuffle
底层调的是repartition
repartition
扩大分区数量
会产生Shuffle
sortBy
会产生Shuffle
// 参数1 返回值是分区字段
// 参数2 是否升序
// 参数3 分区数量
dataRDD.sortBy(str=>str.subString(0,5), false, 4)
双Value类型:
intersection
对源RDD和参数RDD求交集
后返回一个新的RDD
数据类型得相同
dataRDD1.intersection(dataRDD2)
union
数据类型得相同
对源RDD和参数RDD求并集
后返回一个新的RDD
dataRDD1.union(dataRDD2)
subtract
数据类型得相同
求差集
, dataRDD1-公共元素
val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6))
// 结果为1,2
dataRDD1.subtract(dataRDD2)
zip
将两个RDD中的元素, 以键值对的形式进行合并
数据类型可以不同
两个RDD的分区数量得相等, 而且每个分区的数据个数也得相等才行, 否则会报错
Key-Value类型:
partitionBy
将数据按照指定Partitioner重新进行分区: partitionBy(partitioner: Partitioner)
groupByKey
reduceByKey
aggregateByKey
有每个分区的初始值(不算元素个数)
将数据进行分区内
的计算和分区间
的计算
// 每个分区内初始值(不算元素个数) & 分区内的计算规则 & 分区间的计算规则
dataRDD.aggregateByKey(0)(_+_ , _+_)
foldByKey
aggregateByKey分区内
的计算规则和分区间
的计算计算规则相同时, 可以简化为foldByKey
dataRDD.foldByKey(0)(_+_ )
combineByKey
没有每个分区的初始值,
第一个参数表示将分区内的第一个数据转换结构, 第二个参数为分区内的计算规则, 第三个参数为分区间的计算规则
// 求每个key的平均值
val list: List[(String, Int)] = List(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98))
val input: RDD[(String, Int)] = sc.makeRDD(list, 2)
val combineRdd: RDD[(String, (Int, Int))] = input.combineByKey(
v => (v, 1),
(acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1),
(acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2)
)
// 再进行除操作
sortByKey
参数true/false代表是否升序排序
join
将(K,V) 组合 (K,W) 形成 (K,(V,W)), 二者的K类型得相同
leftOuterJoin
按key左外连接
cogroup
将(K,V) 组合 (K,W) 形成 (K,(Iterable
reduce
collect
收集数据到Driver
count
统计RDD内元素的个数
first
take
takeOrdered
返回该RDD排序后的前n个元素
aggregate
分区内聚合计算要用到初始值, 分区间聚合计算也要用到初始值
fold
aggregate分区内
的计算规则和分区间
的计算计算规则相同时, 可以简化为fold
countByKey
统计每种key的个数
countByValue
统计每个元素value出现的个数, 这个value不是键值对的value, 而是单个元素的value
save相关算子
rdd.saveAsTextFile("textFile")rdd.saveAsObjectFile("objectFile")rdd.saveAsSequenceFile("sequenceFile")
foreach
分布式遍历RDD中的每一个元素
map VS mapPartitions:
数据处理角度:
map是分区内一个数据一个数据的执行, 而mapPartitions是以分区
为单位进行批处理
操作功能的角度
map是一对一, 处理后数据不会增加也不会减少mapPartitions是一个集合对一个集合, 集合里可以增加或减少数据
性能的角度
mapPartitions类似于批处理, 所以性能较高;但是mapPartitions会长时间占用内存;
所以内存不足时使用map, 充足时使用mapPartitions
groupByKey VS reduceByKey:
- 功能上: groupByKey是分组, reduceByKey是分组后聚合
- 从shuffle的角度: 二者都存在Shuffle;
- 但是reduceByKey可以在Shuffle前对分区内相同key的数据进行预聚合, 从而减少落盘的数据量
- 而groupByKey只是进行分组, 不存在数据量减少的问题, 从而不会减少Shuffle落盘的数据量
reduceByKey VS foldByKey VS aggregateByKey VS aggregate VS combineByKey:
- reduceByKey: 各个数据进行聚合, 没有分区内初始值, 分区内和分区间计算规则相同
- aggregateByKey: 分区内有初始值, 分区内和分区间计算规则不同
- foldByKey: 分区内有初始值, 分区内和分区间计算规则相同
- aggregate: 分区内聚合计算要用到初始值, 分区间聚合计算也要用到初始值, 分区内和分区间计算规则不同
- combineByKey: 将分区内的第一个数据转换数据结构, 分区内和分区间计算规则不相同
分布式计算中, Driver要往Executor端发数据, 所以数据要支持序列化(算子内经常会用到算子外的数据, 闭包检测)
RDD的Lineage(血统)会记录RDD间的元数据信息和转换行为, 当该RDD的部分分区数据丢失时 可以根据这些信息来恢复数据
并重新计算
多个RDD间可能有血缘依赖, 后者RDD恢复数据时, 也需要前者RDD重新计算
窄依赖: 一个父(上游)RDD的Partition
最多被子(下游)RDD的一个Partition
使用, 像独生子女
宽依赖: 一个父(上游)RDD的Partition
可以被子(下游)RDD的多个Partition
使用(会产生Shuffle), 像多生子女; 又称Shuffle依赖
RDD 任务划分:
Application:初始化一个SparkContext即生成一个Application
Job:一个Action算子就会生成一个Job
Stage:Stage个数等于产生宽依赖(ShuffleDependency)的RDD个数+1(ResultStage)
即每一次Shuffle后, 都会新起一个Stage
Task:一个Stage阶段中最后一个RDD的分区个数就是Task的个数
Application->Job->Stage->Task每一层都是1对n
的关系
Shuffle:
将上游各分区的数据打乱后 分到下游的各个分区, 即宽依赖
Shuffle要落盘, 因为得等待所有上游分区数据都到齐才能进行下一步操作, 所以Shuffle很耗时
窄依赖的话就不必等待所有分区数据全都到齐了, 故窄依赖不会引起Shuffle
Cache缓存:
RDD通过cache( )方法将前面的计算结果临时缓存到内存
可以通过persist( )方法将其改为临时缓存到磁盘
并不会立刻执行, 而是遇到Action算子时才执行
Cache操作不会切断血缘依赖
因内存不足原因导致数据丢失时, 由于RDD的各个Partition是相对独立的, 所以只需要计算丢失的那部分Partition即可, 不必全部重新计算
Spark会自动对一些Shuffle操作的中间结果数据做持久化操作
这是为了避免当有一个节点计算失败了, 导致任务还需要重新从起点进行计算, 重新执行耗时的Shuffle
缓存是临时存储
将RDD计算的中间数据写到磁盘
检查点是长期存储
建议在checkpoint( )前先使用.cache( ), 这样做持久化操作时 只需从Cache缓存中读取数据即可, 否则需要重新计算一次RDD进行持久化
缓存和检查点区别:
缓存和检查点相同的应用场景:
复用前面RDD计算的中间结果
, 避免大量的重复计算从最初的RDD开始
全部重新计算一遍只有Key-Value类型的RDD才有分区器,非Key-Value类型的RDD都分到None分区
范围
内的数据分到一个分区中, 并且尽量使每个分区数据均匀, 分区内数据是有序的文件格式:
文件系统:
各个Executor端计算的结果数据并不会影响到Driver端最终结果, 所以需要累加器
累加器用来把各个Executor端计算的结果数据聚合到Driver端
Driver向Executor端的每个Task
都发一份数据, 开销太大
不需要给Executor端的每个Task
都发一份数据, 而是只给Executor节点
发一份数据即可
计算引擎是是Spark, 语法是HiveSQL
计算引擎是是Spark, 语法是SparkSQL
是一个二维表格, 有一个个字段; 是弱类型
在DataFrame的基础上, 将字段映射为实体类的属性, 相当于多了表名; 是强类型
DataFrame=DataSet[ROW]
SparkSQL默认读取和保存的文件格式为Parquet格式
其中一个Executor作Receiver接收数据
背压机制: 根据JobScheduler反馈作业的执行信息来动态调整Receiver数据接收率
状态就是一块内存, 如果要访问历史窗口(或批次)的数据时就需要用到状态, 把历史窗口(或批次)的数据处理结果值保存到状态里
map, filter等
.transform(类似于RDD里的转换算子, 不会触发计算)转化为RDD进行操作
lineDStream.transform(rdd => {
val words: RDD[String] = rdd.flatMap(_.split(" "))
val wordAndOne: RDD[(String, Int)] = words.map((_, 1))
val value: RDD[(String, Int)] = wordAndOne.reduceByKey(_ + _)
value
})
状态操作需要设置检查点, 因为要用检查点来存状态数据
updateStateByKey:
// 定义更新状态方法,values为当前批次单词频度,state为之前批次单词频度
val updateFunc = (values: Seq[Int], state: Option[Int]) => {
val currentCount = values.foldLeft(0)(_ + _)
val previousCount = state.getOrElse(0)
Some(currentCount + previousCount)
}
pairs.updateStateByKey[Int](updateFunc)
window
开窗口, 窗口大小 & 滑动不长
reduceByWindow
窗口内做聚合
reduceByKeyAndWindow
窗口内按key做聚合
pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b),Seconds(12), Seconds(6))
reduceByKeyAndWindow
有状态操作, 为了避免窗口重叠部分的值
的重复计算, 采用减去旧窗口不包含重叠部分
的值,
pairs.reduceByKeyAndWindow(
{(x, y) => x + y}, // 减去旧窗口不包含重叠部分的值
{(x, y) => x - y}, // 增加新窗口不包含重叠部分的值
Seconds(30),
Seconds(10))
countByWindow
统计窗口内数据的数量
countByValueAndWindow
统计窗口内每个元素出现了多少次
类似于RDD的行动算子, 触发计算
注意:
Connection对象不能写在Driver层面, 因为Connection对象不能被序列化(安全起见), 而Driver发往Executor又非得把数据进行序列化
如果用foreach则每一条数据都使用一个Connection, 太浪费, 且最大连接数有限制
最好使用foreachPartition, 每个分区共用一个Connection
使用外部文件系统来控制内部程序关闭
//关闭时使用优雅关闭
sparkConf.set("spark.streaming.stopGracefullyOnShutdown", "true")