声明: 1. 本文为我的个人复习总结, 并非那种从零基础开始普及知识 内容详细全面, 言辞官方的文章
2. 由于是个人总结, 所以用最精简的话语来写文章
3. 若有错误不当之处, 请指出
一、Spark概述:
Spark模块:
- Core
- SQL
- Streaming
- MLlib
- Graphx
Spark VS MapReduce:
- 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
是程序的入口, 是任务的调度者
功能:
- 将用户程序转化为Job作业
- 调度Task, 分配哪个Task由哪个Executor执行
- 向Yarn申请Container资源
- 监控Executor的执行情况
- Executor
执行Task
对于Standalone独立部署模式, Master(相当于ResourceManager)和Worker(相当于NodeManager)来负责资源的管理调度
整个集群并行执行任务的数量
称之为并行度
DAG 有向无环图, 是高度抽象后的 单向无闭环的任务流程图, 用于表示程序的拓扑结构
向Yarn提交Job:
- 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执行
二、Spark-Core:
概述:
三大数据结构:
- RDD 弹性分布式数据集
- 累加器 分布式共享只写变量
- 广播变量 分布式共享只读变量
RDD算子的特点:
- 弹性
- 容错的弹性, 有持久化机制, 数据丢失后可以自动恢复; 且可以切断血缘避免对父级的依赖, 减少重复计算
- 计算的弹性, 计算失败后自动重试
- 存储的弹性: 自动切换 内存和磁盘 去存储数据
- 分片的弹性:可根据需要重新分片
先将数据集分片, 然后将各个分片放到各个分区
- 分布式
- 数据集:RDD封装了
计算逻辑
,并不保存数据 - 数据抽象:RDD是一个抽象类,需要子类具体实现
- 不可变:RDD是不可变的, 要想改变只能产生新的RDD
- 可分区、各分区间是并行计算的
算子类型:
- 转换(Transform)算子, 并不会触发Job的执行
- Value类型
- 双Value类型
- Key-Value类型
- 行动(Action)算子, 真正触发Job的执行
一、RDD算子:
算子以外的代码都是在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<V>,Iterable<W>)), 二者的K类型得相同
行动算子:
- 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中的每一个元素
会导致Shuffle的算子:
- repartition操作:repartition、repartitionAndSortWithinPartitions、coalesce等
- byKey操作: reduceByKey、groupByKey、sortByKey等
- join操作: join、cogroup
大对比:
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
缓存是临时存储
CheckPoint检查点:
将RDD计算的中间数据写到磁盘
- 并不会立刻执行, 而是遇到Action算子时才执行
- 由于血缘依赖过长会造成容错成本过高, 检查点可以切断血缘关系, 避免从头到尾全部重新计算
检查点是长期存储
建议在checkpoint( )前先使用.cache( ), 这样做持久化操作时 只需从Cache缓存中读取数据即可, 否则需要重新计算一次RDD进行持久化
缓存和检查点区别:
- Cache缓存只是将数据保存起来, 不切断血缘依赖; 而Checkpoint检查点切断血缘依赖
- Cache缓存将数据存储在内存, 可靠性低, 但可以使用persist指定到磁盘; 而Checkpoint将数据存储到磁盘, 可靠性高
- 缓存是临时存储, 检查点是长期存储
缓存和检查点相同的应用场景:
- 为了
复用前面RDD计算的中间结果
, 避免大量的重复计算 - 依赖过长时, 避免后面的RDD计算出错后要
从最初的RDD开始
全部重新计算一遍
分区器:
只有Key-Value类型的RDD才有分区器,非Key-Value类型的RDD都分到None分区
- Hash分区(默认) hash(key)%分区数量
- Range分区 将一定
范围
内的数据分到一个分区中, 并且尽量使每个分区数据均匀, 分区内数据是有序的 - 自定义分区器
使用文件进行数据的读取和保存
文件格式:
- text
- csv
- sequence(二进制文件)
- Object(对象的序列化文件)
文件系统:
- HDFS
- HBase
- 本地磁盘
二、累加器:
为什么要有累加器?
各个Executor端计算的结果数据并不会影响到Driver端最终结果, 所以需要累加器
累加器的作用:
累加器用来把各个Executor端计算的结果数据聚合到Driver端
三、广播变量:
为什么要有广播变量?
Driver向Executor端的每个Task
都发一份数据, 开销太大
广播变量的作用:
不需要给Executor端的每个Task
都发一份数据, 而是只给Executor节点
发一份数据即可
三、Spark-SQL:
HiveOnSpark:
计算引擎是是Spark, 语法是HiveSQL
SparkOnHive:
计算引擎是是Spark, 语法是SparkSQL
DataFrame:
是一个二维表格, 有一个个字段; 是弱类型
DataSet:
在DataFrame的基础上, 将字段映射为实体类的属性, 相当于多了表名; 是强类型
DataFrame=DataSet[ROW]
RDD & DataFrame & DataSet之间的转换:
数据存储格式:
SparkSQL默认读取和保存的文件格式为Parquet格式
四、Spark-Streaming:
Receiver:
其中一个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操作:
- 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
统计窗口内每个元素出现了多少次
DStream输出:
类似于RDD的行动算子, 触发计算
- foreach
- foreachRDD
- saveAsTextFiles
- saveAsObjectFiles
- saveAsHadoopFiles
注意:
- Connection对象不能写在Driver层面, 因为Connection对象不能被序列化(安全起见), 而Driver发往Executor又非得把数据进行序列化
- 如果用foreach则每一条数据都使用一个Connection, 太浪费, 且最大连接数有限制
- 最好使用foreachPartition, 每个分区共用一个Connection
优雅关闭:
使用外部文件系统来控制内部程序关闭
//关闭时使用优雅关闭
sparkConf.set("spark.streaming.stopGracefullyOnShutdown", "true")