Spark学习总结(一)—— Spark Core
1、SPARK
基于内存的快速通用可扩展的大数据分析计算引擎包含流处理的批处理框架
一次性数据计算:
处理数据时会从存储设备中读取数据,进行逻辑操作,然后将处理的结果重新存储到介质中
处理复杂逻辑性能低
SPARK对该流程进行了更改,即不是放入磁盘而是放入内存中方便后续的操作
但这么做也可能导致内存资源的使用紧张
2、核心模块
spark core:核心功能,其他功能会基于该核心实现与扩展
spark sql:操作结构化数据的组件,使用该组件可以用sql语言来查询数据
spark streaming:针对实时数据进行流式计算的组件,提供了丰富的处理数据流的API
spqrk MLlib:机器学习算法库,不仅提供了模型评估、数据导入等功能,还提供了一些底层机器学习的原语 spark graphX:面向图计算提供的框架以及算法库
3、wordcount总结
- 版本要对不然可能报错
- 匿名函数的使用(下划线)
- maven下载慢就换源
4、SPARK运行环境
- Local模式
不需要其他任何节点资源就可以在本地执行SPARK代码的环境,一般用于教学、调试、演示等
- 独立部署模式
一种只使用自身节点进行运行的集群模式
【1】打开conf目录修改slaves和spark-env,去掉它们的后缀并修改内部的内容,添加上子客户端的IP地址
【2】使用sbin中的start-all.sh相关参数:
配置历史服务
【1】修改conf中的spark-default.conf,输入主机地址、端口和文件名
【2】在hadoop上脱离安全模式并创造对应文件名
sbin/start-dfs.sh hadoop fs -mkdir /directory
【3】修改conf中的spark-env文件
【4】分发
【5】启动
sbin/start-all.sh
sbin/start-history-server.sh
- Yarn模式
独立部署模式无需其他框架提供资源,但也降低了同第三方资源的耦合性,独立性强。但Spark是一种计算框架,而不是资源调度框架,这不是它的枪响,因此Yarn模式是在Yarn环境下的Spark工作
附注:
在启动和关闭spark时要在前面加上sbin/,否则会默认执行HADOOP的开启关闭
SPARK MASTER内部端口号:7077(别和HADOOP 冲突)
SPARK历史服务器端口:18080 Hadoop YARN:8088
SPARK 查看spark-shell运行任务 4040 5、运行架构
主从架构
Driver
Executor
ApplicationMaster
driver 如果直接和 master进行交互,会减少耦合性,因此需要使用ApplicationMaster作为中间件,增加灵活性
5、核心概念
- Executor 和 Core
- 并行度
并发:多个虚拟核去抢占单个真正核的操作 并行:有多个真正核,多个虚拟核可同时操作
- 有向无环图
各个部件的依赖不能形成回环,否则会导致部件无法运行
- SPARK YARN部署 : client&cluster
主要区别在于Driver程序的运行节点位置
6、spark 核心编程
Driver: 调度作用,用于数据、逻辑的准备
Executor: 用于执行
7、RDD:最小的计算单元
附加: FileInputStream()用于读取文件数据
BufferedInputStream()的作用
相当于开辟了一个缓冲区,读取的数据会直接放入缓冲区,只有超过缓冲区阈值的时候才会进行输出
这么做的好处是能够提升文件的读取效率,不再需要读一个输出一个,读一个输出一个(字节流)。类似于批处理。
InputStreamReader()的作用
当使用字符流时,由于不清楚几个字节为一个字符,因此需要一个转换流来进行转换组装成字符
8、RDD与IO关系
HadoopRDD -textFile 读取文件数据 MapPartitionsRDD -flatMap 扁平化操作,分词 MapPartitionsRDD -Map 转换功能
ShuffledRDD -reduceByKey 相同key做value聚合交给collect()输出
RDD的数据处理方式类似于IO流,也有装饰者设计模式
RDD的数据只有再调用collect方法时才会真正执行业务逻辑操作。之前的全部封装都是功能的扩展 RDD数据中间不存储,可以临时保存一部分数据
9、RDD特点
在读取数据还有,为了方便切分数据(分组),数据在读取时便会根据逻辑分配到不同的分区中
RDD叫做弹性分布式数据集,是Spark中最基本的数据处理模型。是一个不可变,可分区,内部元素可并行计算的集合】特点:
- 弹性
【1】存储弹性:内存磁盘自动切换
【2】容错弹性:数据丢失可自动恢复
【3】计算弹性:计算出错重试机制
【4】分片弹性:根据需要重新分片
- 分布式
数据存储在大数据集群不同节点上
- 数据集
RDD封装了计算逻辑,并不保存数据
- 数据抽象
RDD是一个抽象类,需要看具体封装实现
- 不可变
RDD封装了计算逻辑,不可改变,若要改变只能产生新的RDD,在新的RDD里封装计算逻辑
- 可分区、并行计算
10、核心属性
- 分区列表
用于执行并行计算,是实现分布式计算的重要属性
- 分区计算函数
- RDD依赖关系(一份依赖列表)
- 分区器(将数据进行分区处理)
- 计算分区的首选位置(判断计算发生到哪个节点效率最优)
11、RDD的创建
- 从内存中
- 从文件中
- 从集合
12、RDD集合数据源是如何设置分区并划分数据的 i 代表分区数遍历,从0开始(例子:0,1,2)
start = (i * 数量长度)/分区数量
end = ((i+1)*数量长度)/分区数量
(start , end)包头不包尾,代表的是用于划分数据的序号,第一个是0,以此类推。假设(0,2)则包含序号0,1。意味着第一个和第二个数据被划分在该分区。
13、RDD文件来源如何设置分区数量默认情况下: math.min(defaultParallelism,2)
defaultParallelism代表用户设置的工作线程数
用户设置的情况:
用户设置的分区数量实际上是最小数量,具体分区公式如下:所有文件的总字节数/(分区数量,如果分区为0就改成1)
14、RDD文件来源如何进行数据分配
- 数据以行为单位进行分区,与字节数无关
- 数据读取时以偏移量为单位,且偏移量不会被重新读取
- 数据分区的偏移量范围计算,包头包尾,任何行在被读取后该行所有内容将会被一次性全部读取,哪怕不在偏移量范围内。注意读取的内容包括占两个字节的回车
- 如果数据源为多个文件,那么计算分区时以文件为单位进行分区
15、RDD方法-》RDD算子(操作)
- 转换:功能与补充,将旧的RDD包装成新的RDD flatmap,map:包装添加新功能的函数,本身不执行什么
- 行动:触发任务的调度和作业的执行
collect:用于具体行动的函数
16、RDD转换算子根据处理方式分类:
- 单value类型
【1】map
def map[ClassTag](f:T->U):RDD[U]
将处理的数据按照给出的函数逐条进行映射转换,这里转换可以是类型的转换也可以是值的转换小功能:从服务器日志数据apache.log中获取用户请求URL的资源路径
附注:
rdd的计算一个分区内的数据是一个一个执行逻辑不同分区数据计算是无序的
【2】mapPartitions
def map Partitions[U:ClassTag]( f:Iterator[T]=>Iterator[U], preservesPartitioning:Boolean = false):RDD[U]
把一个分区的数据全拿到了在操作
优点:可以以分区为单位进行数据转换操作,数据少的时候效率高
缺点:由于需要将整个分区的数据加载到内存进行引用,如果处理完的数据是不会被释放掉的,存在对象的引用。内存较小数据较大时可能导致内存溢出
小功能:获取每个分区内的最大值
- 双value类型
- key-value类型
map和mapPartitions的区别
【3】mapPartitionsWithIndex
def mapPartitionsWithIndex[U:ClassTag]( f:(Int,Iterator[T])=>iTERATOR[U]
preservesParittioning:Boolean=false):RDD[U]
分区索引加上了分区号
将待处理的数据以分区为单位发送到计算节点进行处理,这里的处理是指可以进行任意的处理,哪怕只是过滤数据,在处理时同时可以获取当前分区的索引
小功能:获取第二个数据分区的数据
【4】flatMap
def faltMap[U:ClassTag](f:T =>TraversableOnce[U]):RDD[U]
将处理的数据进行扁平化后再进行映射,所以也叫做扁平映射
小功能: 将LIST(LIST(1,2),3,LIST(3,4))进行扁平化操作
【5】glom
def glom():RDD[Array[T]]
将同一分区的数据直接转换为相同类型的内存数组进行处理,分区不变
小功能:计算所有分区最大值求和(分区内取最大值,分区间最大值求和)
【6】groupBy
def groupBy[K](f:T=>)(implicit kt:ClassTag[K]):RDD[(K,Iterable[T])]
将数据根据指定的规则进行分组,分区默认不变,但是数据会被打乱重新组合,这样的操作称之为shuffle。极限情况下,数据可能被分在一个分区中
一个组的数据在一个分区中,但是并不是说一个分区中只有一个组
groupBy 会将数据打乱(打散),重新组合,这个操作我们称之为shuffle
【7】filter
def filter(f:T => Boolean):RDD[T]
将数据根据指定的规则进行筛选过滤,复合规则的数据保留,不符合的数据丢弃,当数据进行筛选过滤后,分区不变,但是分区内的数据可能不均衡,在生产环境下可能出现数据
【8】sample def sample(
withReplacement:Boolean, fraction:Double
seed:Long = Utils:random.nextLong):RDD[T]
根据指定的规则从数据集中抽取数据
【9】distinct
def distinct()(implicit ord:Ordering[T] = null):RDD[T]
def distinct(numPartitions:Int)(implicit ord:Ordering[T]=null):RDD[T]
将数据集中重复的数据去重
【10】coalesce
def coalesce(numPartitions:Int,shuffle:Boolean=false, partitionCoalescer:Option[PartitionCoalescer] = Option.empty):RDD[T]
根据数据量缩减分区,用于大数据集过滤后,提高小数据集的执行效率
当spark程序中存在过多的小任务时,可以通过该方法收缩合并分区,减少分区的个数,减小任务调度的成本
coalesce方法默认情况下不会将分区的数据打乱重新组合,某些情况下缩减分区可能会导致数据不均衡,引发数据倾斜。如果想要让数据均衡,可以进行shuffle处理
【11】repartitions
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
coalesce可用于扩大分区,但是必须要进行shuffle操作,否则无意义 spark提供了简化操作
缩减分区:coalesce,如果想要数据均衡可以采用shuffle
扩大分区:repartition,底层代码调用的就是coalesce,并采用shuffle
【12】sortBy def sortBy[K]( f: (T) =>
ascending: Boolean = true, numPartitions: Int = this.partitions.length) (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]
该操作用于排序数据。在排序之前,可以将数据通过 f 函数进行处理,之后按照 f 函数处理 的结果进行排序,默认为升序排列。排序后新产生
的 RDD 的分区数与原 RDD 的分区数一 致。中间存在 shuffle 的过程
- 双value类型
【13】intersection
def intersection(other: RDD[T]): RDD[T]
对源 RDD 和参数 RDD 求交集后返回一个新的 RDD
【14】union
def union(other: RDD[T]): RDD[T]
对源 RDD 和参数 RDD 求并集后返回一个新的 RDD
【15】subtract
def subtract(other: RDD[T]): RDD[T]
以一个 RDD 元素为主,去除两个 RDD 中重复元素,将其他元素保留下来。求差集
【16】zip
def zip[U: ClassTag](other: RDD[U]): RDD[(T, U)]
将两个 RDD 中的元素,以键值对的形式进行合并。其中,键值对中的 Key 为第 1 个 RDD 中的元素,Value 为第 2 个 RDD 中的相同位置的元素。
附注:
【1】
//Can't zip RDDs with unequal numbers of partitions: List(2, 4)
//zip要求数据源分区需要保持一致
//Can only zip RDDs with same number of elements in each partition
//两个数据源分区中的数据数量要保持一致
【2】
交集、并集和差集要求两个数据源的数据类型保持一致,但zip可以不一致
- key-value类型
【17】partitionBy
def partitionBy(partitioner: Partitioner): RDD[(K, V)]
将数据按照指定 Partitioner 重新进行分区。Spark 默认的分区器是 HashPartitioner
附注:
//分区数量和类型相同时会直接返回
//除了hash分区器还有RangePartitioner,一般用于排序
//想按照自己的规则分区怎么搞?自己写分区器
【18】reduceByKey
def reduceByKey(func: (V, V) => V): RDD[(K, V)]
def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]
可以将数据按照相同的 Key 对 Value 进行聚合
reduceByKey中key的数据如果只有一个,是不会参与运算的
【19】groupByKey
def groupByKey(): RDD[(K, Iterable[V])] def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])] def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])]
将数据源的数据根据key对value进行分组
reduceByKey 和 groupByKey
从shuffle角度:
【1】groupByKey会导致数据打乱重组,存在shuffle操作
但是在shuffle操作中,考虑到后续例如map操作需要等待所有分区的数据重组完毕会占用大量内存,因此这些数据必须落盘处理,也就是写到文件中,否则会导致内存溢出,尽管这会导致shuffle的性能降低(IO操作)
【2】在reduceByKey中,可以对相同的数据进行一个预聚合,即分区内预先进行聚合操作,可以减少落盘的数据量,从而能够提高shuffle性能,但是要求分区内和分区间的计算规则是相同的
从功能角度:
rduceByKey只是进行分区,不存在数据量减少的问题。GroupByKey只能分组,不能聚合,所以在分组聚合的场合下,推荐使用 reduceByKey。如果仅仅是分组而不需要聚合,那么还是只能使用groupByKey
【20】aggregateByKey
def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U, combOp: (U, U) => U): RDD[(K, U)]
将数据根据不同的规则进行分区内计算和分区间计算
【21】foldByKey
def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]
当分区内计算规则和分区间计算规则相同时,aggregateByKey 就可以简化为 foldByKey
【22】combineByKey
def combineByKey[C]( createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C): RDD[(K, C)]
最通用的对 key-value 型 rdd 进行聚集操作的聚集函数(aggregation function)。类似于 aggregate(),combineByKey()允许用户返回值的类型与输入不一致
这种方式可以在初始化的时候便对数据进行转换,从而提高了数据转换的效率
reduceByKey、foldByKey、aggregateByKey、combineByKey 的区别?
reduceByKey: 相同 key 的第一个数据不进行任何计算,分区内和分区间计算规则相同
FoldByKey: 相同 key 的第一个数据和初始值进行分区内计算,分区内和分区间计算规则相 同
AggregateByKey:相同 key 的第一个数据和初始值进行分区内计算,分区内和分区间计算规 则可以不相同
CombineByKey:当计算时,发现数据结构不满足要求时,可以让第一个数据转换结构。分区 内和分区间计算规则不相同。
【23】join
def sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length) : RDD[(K, V)]
在一个(K,V)的 RDD 上调用,K 必须实现 Ordered 接口(特质),返回一个按照 key 进行排序
【24】leftOuterJoin
def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]
类似于 SQL 语句的左外连接
【25】cogroup
def cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))]
在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD
17、RDD行动算子
所谓的行动算子,就是出发作业(job)执行的方法底层代码调用的是环境对象的runJob方法
底层代码中会创建ActiveJob,并提交执行
【1】reduce
def reduce(f: (T, T) => T): T
聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据
【2】collect
def collect(): Array[T]
在驱动程序中,以数组 Array 的形式返回数据集的所有元素
【3】count
def count(): Long
返回 RDD 中元素的个数
【4】first def first(): T
返回 RDD 中的第一个元素
【5】take
def take(num: Int): Array[T]
返回一个由 RDD 的前 n 个元素组成的数组
【6】takeOrder
def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T]
返回该 RDD 排序后的前 n 个元素组成的数组
【7】aggregate
def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U
分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合
aggregateByKey: 初始值只会参与分区内的计算
aggregate: 初始值会参与分区内计算,并且会参与分区间计算
【8】fold
def fold(zeroValue: T)(op: (T, T) => T): T aggregate 的简化版操作 ,分区内外计算规则相同
【9】countByKey
def countByKey(): Map[K, Long]
统计每种 key 的个数
【10】save算子
def saveAsTextFile(path: String): Unit def saveAsObjectFile(path: String): Unit def saveAsSequenceFile(
path: String,
codec: Option[Class[_ <: CompressionCodec]] = None): Unit
将数据保存到不同格式的文件中
【11】foreach
def foreach(f: T => Unit): Unit = withScope { val cleanF = sc.clean(f)
sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}
分布式遍历 RDD 中的每一个元素,调用指定函数
为什么RDD的方法称之为算子:
RDD的方法和Scala集合对象的方法不一样
集合对象的方法都是在同一个节点的内存中完成的
RDD的方法可以将计算逻辑发送到Executor(分布式节点)执行为了区分不同的处理效果,所以将RDD的方法称之为算子
RDD的方法外部的操作在driver端执行,方法内的逻辑代码在executor端执行
18、RDD序列化
1) 闭包检查
从计算的角度, 算子以外的代码都是在 Driver 端执行, 算子里面的代码都是在 Executor 端执行。那么在 scala 的函数式编程中,就会导致算子内经常会用到算子外的数据,这样就 形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给 Executor 端执行,就会发生错误,所以需要在执行任务计算前,检测闭包内的对象是否可以进行序列 化,这个操作我们称之为闭包检测。Scala2.12 版本后闭包编译方式发生了改变
19、kryo序列化框架
Java 的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也 比较大。Spark 出于性能的考虑,Spark2.0 开始支持另外一种 Kryo 序列化机制。Kryo 速度 是 Serializable 的 10 倍。当 RDD 在 Shuffle 数据的时候,简单数据类型、数组和字符串类型 已经在 Spark 内部使用 Kryo 来序列化。
20、RDD依赖关系
(1) RDD血缘关系
相邻两个RDD的关系称之为依赖关系
多个连续的RDD依赖关系称之为血缘关系每个RDD会保存血缘关系
RDD不保存数据
因此一旦出现报错需要从头开始读取
RDD为了提供容错性,需要将RDD间的关系保存下来
一旦出现错误就可以根据血缘关系将数据源重新读取进行计算
具体血缘关系保存路程
(2) 依赖关系
新的RDD的一个分区的数据依赖于旧的RDD一个分区的数据,这个依赖称之为OneToOne依赖,又名窄依赖。指代每一个父(上游)RDD的
Partition 最多被子(下游)RDD 的一个 Partition 使用, 窄依赖我们形象的比喻为独生子女。
新的RDD的一个分区的数据依赖于旧的RDD多个分区的数据,这个依赖称之为Shuffle依赖,又名宽依赖,指代同一个父(上游)RDD 的
Partition 被多个子(下游)RDD 的 Partition 依赖,会 引起 Shuffle,总结:宽依赖我们形象的比喻为多生。
当RDD中存在shuffle依赖时,阶段会自动增加一个阶段的数量 = shuffle依赖的数量 + 1(resultstage) ResultStage只有一个,最后需要执行的阶段
21、RDD任务划分
任务的数量 = 当前阶段中最后一个RDD的分区数量
22、持久化 cache & persist
如果一个RDD需要重复使用,那么需要从头再执行来获取数据
RDD对象可以重用,但是数据无法重用,因为RDD是不留存数据的
解决方法——持久化:在交给下一位时先放入内存或文件中
cache 默认持久化的操作只能把数据保存在内存中,如果想要保存到磁盘文件要用persist持久化操作必须在行动算子执行时完成,不然没有数据
除了重用外,持久化操作还可以再数据执行较长或数据比较重要时也可以使用持久化操作
checkpoint 需要落盘,需要指定检查点保存路径
检查点路径保存的文件,当作业执行完毕后,不会被删除一般保存路径都是在分布式存储系统:HDFS中
区别
(1) cache : 将数据临时存储在内存中进行数据重用,快但数据不够安全会在血缘关系中添加新的依赖,一旦出现问题可以重头读取数据
(2) persist : 将数据存储在磁盘文件中进行数据重用,涉及磁盘IO,性能较低但数据安全,
若作业执行完毕,临时保存的数据文件就会丢失
(3) checkpoint : 将数据长久地保存在磁盘文件中进行数据重用涉及磁盘IO,性能较低但数据安全,
为了保证数据安全,所以一般情况下,会独立执行作业
为了能够提高效率,一般情况下是需要和cache联合使用的执行过程中,会切断血缘关系,重新建立新的血缘关系
因为checkpoint相当于把计算结果保存在分布式存储中,比较安全,相当于数据源变化
23、RDD分区器
由于有时候默认分区器无法满足我们的需求,因此我们需要自己写分区器
(1) 自己用class构造分区器类,并继承Partitioner类
(2) 重写方法
(3) 使用partitionBy(new MyPartitioner)进行引用
24、RDD文件读取与保存
rdd.saveAsTextFile("ouput1") rdd.saveAsObjectFile("ouput2") rdd.saveAsSequenceFile("ouput3")
25、核心编程
26、累加器:分布式共享只写变量
在进行累加计算的时候不能直接使用for循环进行操作。因为实际上所有的累加操作都在Executor端执行,没有返回到Driver端进行聚合,这个时候需要使用累加器ACC
累加器重写
27、广播变量
闭包数据都是以Task为单位发送的,每个任务中包含闭包数据
可能会导致一个Executor中含有大量重复的数据,并且占用大量的内存
Executor其实就是一个JVM,所以在启动时会自动分配内存
完全可以将任务中的闭包数据放置在Executor的内存中,达到共享的目的
Spark中的广播变量就可以将闭包的数据保存到Executor的内存中但是它不能更改,是分布式共享只读变量