Spark Core(3)

第三章 Spark Core

1 RDD相关算子

RDD算子:指的是RDD对象中提供了非常多的具有特殊功能的函数,我们将这些函数称为算子(大白话:指的RDD的API)

相关的算子的官方文档: https://spark.apache.org/docs/3.1.2/api/python/reference/pyspark.html#rdd-apis

1.1 RDD算子的分类

整个RDD算子,共分为两大类:【Transformation转换算子和Action动作算子】

Transformation转换算子:

1 所有的转换算子在执行完成后,都会返回一个新的RDD
2 所有的转换算子都是【Lazy惰性】,并不会立即执行,仅仅是在定义计算的规则
3 转换算子必须遇到【Action动作算子】才会触发执行

Action动作算子:

1.动作算子在执行后,不会返回一个RDD。【要么没有返回值(saveAsTextFile),要么返回的数据类型不是RDD(collect)】
2 动作算子都是【立即执行】的,一个动作算子就会产生一个Job的任务,并且会执行这个动作算子所依赖的其他所有RDD(代码中如果存在多个需求,不同需求依赖相同的rdd,则该rdd默认情况会被执行多次,避免重复执行可以通过cache或者checkpoint)

2 RDD的转换算子

2.1 单值类型算子

map算子:
格式:rdd.map(fn)
说明: 主要根据传入的函数,对数据进行一对一的转换操作,传入一行,返回一行

>>> rdd = sc.parallelize([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
>>> rdd.map(lambda n:n-1).collect()
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

groupBy 算子:
格式: rdd.groupBy(fn)
说明: 根据传入的函数(作用就是针对每个元素产生key)对数据进行分组操作

>>> rdd = sc.parallelize([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
>>> rdd.groupBy(lambda n:'偶数:' if n%2==0 else '奇数:').collect()
[('奇数:', <pyspark.resultiterable.ResultIterable object at 0x7f141ca1bf10>), ('偶数:', <pyspark.resultiterable.ResultIterable object at 0x7f141c9a4700>)]
# mapValues(list)将kv数据类型中value转换成列表,不转换返回id值
>>> rdd.groupBy(lambda n:'偶数:' if n%2==0 else '奇数:').mapValues(list).collect()
[('奇数:', [1, 3, 5, 7, 9]), ('偶数:', [2, 4, 6, 8, 10])]

filter算子:
格式:rdd.filter(fn)
说明:过滤算子,接收一个函数,在接收的函数中指定过滤条件,对数据进行过滤操作,条件返回True保留,返回False过滤,接收的这个函数也是作用在每个元素中

>>> rdd = sc.parallelize([0,1,2,3,4,5,6,7,8,9])
>>> rdd.filter(lambda num:num>5).collect()   #过滤掉数值<5的数据
[6, 7, 8, 9]

flatMap算子:
格式:rdd.flatMap(fn)
说明:在map算子的基础上,加入一个压扁的操作, 主要适用于一行中包含多个内容的操作,实现一转多的操作

>>> rdd = sc.parallelize(['张三 李四 王五','赵六 周日'])
>>> rdd.flatMap(lambda content:content.split()).collect()
['张三', '李四', '王五', '赵六', '周日']

2.2 双值类型算子

union(并集) 和intersection(交集)
格式: rdd1.union(rdd2) rdd1.intersection(rdd2)

>>> a=sc.parallelize([1,2,3])
>>> b=sc.parallelize([2,3,4,5,6])
>>> a.union(b).collect() 			 #并集
[1, 2, 3, 2, 3, 4, 5, 6]
>>> a.union(b).distinct().collect()  # 并集去重
[4, 1, 5, 2, 6, 3]
>>> a.intersection(b).collect()      # 交集
[2, 3]

2.3 key-value数据类型算子

groupByKey()
格式: rdd.groupByKey()
说明: 根据key进行分组操作,这个函数是没有聚合功能,但是可以通过后续使用mapValues进行聚合操作

>>> rdd = sc.parallelize([('c01','张三'),('c02','李四'),('c02','王五'),('c01','赵六'),('c03','田七'),('c03','周八'),('c02','李九')])
>>> rdd.groupByKey().collect()
[('c01', <pyspark.resultiterable.ResultIterable object at 0x7f141c9ada90>), ('c02', <pyspark.resultiterable.ResultIterable object at 0x7f141c9adb50>), ('c03', <pyspark.resultiterable.ResultIterable object at 0x7f141c9ad820>)]
>>> rdd.groupByKey().mapValues(list).collect()
[('c01', ['张三', '赵六']), ('c02', ['李四', '王五', '李九']), ('c03', ['田七', '周八'])]

reduceByKey()
格式: rdd.reduceByKey(fn)
说明: 根据key进行分组,将一个组内的value数据放置到一个列表中,对这个列表基于fn进行聚合计算操作

>>> init_rdd = sc.parallelize(['张三 李四 王五','赵六 周日'])
>>> rdd.map(lambda tup:(tup[0],1)).reduceByKey(lambda x,y:x+y).collect()
[('c01', 2), ('c02', 3), ('c03', 2)]

sortByKey()算子:
格式:rdd.sortByKey(ascending=True|False)
说明: 根据key进行排序操作,默认按照key进行升序排序,如果需要降序,设置 ascending 参数的值为False

>>> rdd = sc.parallelize([(10,2),(15,3),(8,4),(7,4),(2,4),(12,4)])
>>> rdd.sortByKey().collect()					# 升序
[(2, 4), (7, 4), (8, 4), (10, 2), (12, 4), (15, 3)]
>>> rdd.sortByKey(False).collect()				# 降序
[(15, 3), (12, 4), (10, 2), (8, 4), (7, 4), (2, 4)]
>>> rdd.sortByKey(ascending=False).collect()	# 降序
[(15, 3), (12, 4), (10, 2), (8, 4), (7, 4), (2, 4)]

3 RDD的动作算子action算子

collect() 算子:
格式:collect()
作用:收集各个分区的数据,将数据汇总到一个大的列表返回

reduce() 算子:
格式:reduce(fn)
作用:根据传入的函数对数据进行聚合操作

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10])
>>> rdd.reduce(lambda x,y:x+y)
55

first() 算子:
格式:first()
说明:获取第一个元素

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10])
>>> rdd.first()
1

take() 算子
格式 :take(N)
说明:获取前N个元素 ,类似于 limit操作

>>> rdd = sc.parallelize([1,2,3,4,5,6])
>>> rdd.take(3)
[1, 2, 3]

top()算子:
格式:top(N,[fn])
说明:对数据集进行倒序排序操作,如果kv类型,针对key进行排序,获取前N个元素
fn: 可以自定义排序,按照谁来排序

>>> rdd = sc.parallelize([1,2,3,4,5])
>>> rdd.top(2)
[5, 4]
>>> rdd = sc.parallelize([('a班',5),('b班',7),('c班',1),('f班',12)])
>>> rdd.top(2,lambda tup:tup[1])
[('f班', 12), ('b班', 7)]

count() 算子:计总数

>>> rdd = sc.parallelize([1,2,3,4,5])
>>> rdd.count()
5

foreach() 算子
格式:foreach(fn)
作用: 对数据集进行遍历操作,遍历后做什么,取决于传入的fn,类似于map函数,只不过这个foreach是action算子

>>> rdd = sc.parallelize([0,1,2,3])
>>> rdd.foreach(lambda n:print(n))
0
1
2
3
>>> rdd.map(lambda n:print(n)).collect()
0
1
2
3
[None, None, None, None]
>>> rdd = sc.parallelize([[0,1],[2,3]])
>>> rdd.map(lambda n:print(n)).collect()
[0, 1]
[2, 3]
[None, None]
>>> rdd.foreach(lambda n:print(n))
[2, 3]
[0, 1]

4 RDD的重要算子

4.1 基本算子

RDD中map、filter、flatMap及foreach等函数为最基本函数,都是都RDD中每个元素进行操作,将元素传递到函数中进行转换。

map 函数map(f:T=>U) : RDD[T]=>RDD[U],表示将 RDD 经由某一函数 f 后,转变为另一个RDD。
flatMap 函数flatMap(f:T=>Seq[U]) : RDD[T]=>RDD[U]),表示将 RDD 经由某一函数 f 后,转变为一个新的 RDD,但是与 map 不同,RDD 中的每一个元素会被映射成新的 0 到多个元素(f 函数返回的是一个序列 Seq)。
filter 函数filter(f:T=>Bool) : RDD[T]=>RDD[T],表示将 RDD 经由某一函数 f 后,只保留 f 返回为 true 的数据,组成新的 RDD。
foreach 函数foreach(func),将函数 func 应用在数据集的每一个元素上,通常用于更新一个累加器,或者和外部存储系统进行交互,例如 Redis。关于 foreach,在后续章节中还会使用,到时会详细介绍它的使用方法及注意事项。
saveAsTextFile 函数saveAsTextFile(path:String),数据集内部的元素会调用其 toString 方法,转换为字符串形式,然后根据传入的路径保存成文本文件,既可以是本地文件系统,也可以是HDFS 等。

4.2 分区相关算子

在spark中,对于map算子和foreach算子都提供了分区函数,分别是mapPartitions()foreachPartition()
map算子和foreach算子,针对的是RDD中每个分区下的每一条数据。而分区函数,则是一次性处理一个分区
RDD->多个分区,每个分区->多条数据
mapPartitions

>>> rdd = sc.parallelize([1,2,3,4,5],3)
>>> rdd.glom().collect()
[[1], [2, 3], [4, 5]]
>>> def fn1(num):
...     print(num)
...     return num+1
...
>>> rdd.map(fn1).collect()
1
2
3
4
5
[2, 3, 4, 5, 6]
>>> def fn1(list):
...     print(list)
...     for i in list:
...             yield i + 1
...     return list
...
>>> rdd.mapPartitions(fn1).collect()		#分成3个区所以调用了三次
<itertools.chain object at 0x7fac31a8eb20>
<itertools.chain object at 0x7fac31a8e460>
<itertools.chain object at 0x7fac31a8e460>
[2, 3, 4, 5, 6]

foreachPartition

>>> def fn2(i):
...  print(i)
...
>>> rdd.foreach(fn2)
1
2
3
4
5
>>> def fn3(list):
...     print(list)
...     for i in list:
...             print(i)
...
>>> rdd.foreachPartition(fn3)
<itertools.chain object at 0x7fac31e3d160>
1
<itertools.chain object at 0x7fac31e3d160>
2
3
<itertools.chain object at 0x7fac31a849d0>
4
5

4.3 重分区算子

repartition算子
格式:repartition(分区数)
作用:重新对RDD分区数量进行调整,得到一个新的RDD。【既可以进行增大分区,也可以减小分区,必定会触发Shuffle操作】

>>> rdd = sc.parallelize([1,2,3,4,5,6,7],3)
>>> rdd.glom().collect()	## 查看分区情况
[[1, 2], [3, 4], [5, 6, 7]]
>>> rdd.repartition(2).glom().collect()		## 减少分区,分为2分区
[[1, 2, 5, 6, 7], [3, 4]]
>>> rdd.repartition(8).glom().collect()		## 增大分区,分为8分区
[[], [], [3, 4], [], [], [], [1, 2], [5, 6, 7]]

coalesce算子
格式:coalesce(分区数,[参数2]),参数2表示的是否开启shuffle,如果开启了,即可实现增大分区;如果不开启,仅能减少分区,默认是关闭shuffle
作用:重新对RDD分区数量进行调整,得到一个新的RDD。默认只能减少分区数量

>>> rdd = sc.parallelize([1,2,3,4,5,6,7],3)
>>> rdd.glom().collect()					## 查看分区情况
[[1, 2], [3, 4], [5, 6, 7]]
>>> rdd.coalesce(2).glom().collect()		## 减少分区为2
[[1, 2], [3, 4, 5, 6, 7]]
>>> rdd.coalesce(10).glom().collect()		## 增大分区为10,结果不生效
[[1, 2], [3, 4], [5, 6, 7]]
>>> rdd.coalesce(10,shuffle=True).glom().collect()	## 将参数2设置为True,再增大分区
[[], [1, 2], [5, 6, 7], [3, 4], [], [], [], [], [], []]

repartition 和 coalesce总结:

1、repartitioncoalesce两个算子都【可以修改RDD的分区数量】
2、repartition本质上是coalesce的一种当参数2为True的简写方案,因为repartition底层调用coalesce函数,将参数2设置为True
3、repartition既可以【增大分区】,也可以【减小分区】,触发shuffle
4、coalesce默认【只能减小分区】,无法增大分区,不触发shuffle。如果要增大分区,需要将参数2调整为True,这时就会触发shuffle

partitionBy算子
格式:partitionBy(分区数,[参数2]) 参数2是自定义分区规则
作用:专门针对kv类型数据进行重新分区,得到一个新的RDD。可以增大分区,也可以减少分区,但是会产生shuffle
注意:

默认根据key进行hash取模划分操作,如果不满意这个分区方案,可以通过参数2自定义分区规则。
自定义分区规则返回的必须是一个int类型的数据,它是分区的编号,编号从0开始

>>> rdd = sc.parallelize([(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(10,10)],5)
>>> rdd.glom().collect()   ## 查看分区情况
[[(1, 1), (2, 2)], [(3, 3), (4, 4)], [(5, 5), (6, 6)], [(7, 7), (8, 8)], [(9, 9), (10, 10)]]
>>> rdd.partitionBy(11).glom().collect()  ## 增大分区,尝试分为11个分区
[[], [(1, 1)], [(2, 2)], [(3, 3)], [(4, 4)], [(5, 5)], [(6, 6)], [(7, 7)], [(8, 8)], [(9, 9)], [(10, 10)]]
>>> rdd.partitionBy(2).glom().collect()		## 减小分区。分为2个分区
[[(2, 2), (4, 4), (6, 6), (8, 8), (10, 10)], [(1, 1), (3, 3), (5, 5), (7, 7), (9, 9)]]
>>> rdd.partitionBy(2,lambda key:0 if key>5 else 1).glom().collect()	##将key>5放置在一个分区,剩余放到另一分区
[[(6, 6), (7, 7), (8, 8), (9, 9), (10, 10)], [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]]

4.4 聚合算子

单值类型的聚合算子
reduce(fn1):!
根据传入函数对数据进行聚合处理

>>> rdd = sc.parallelize([1,2,3,4,5,6,7,8,9,10],3)
>>> rdd.glom().collect()
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]
>>> rdd.reduce(lambda agg,curr:agg+curr)	##求和 结果为55
55

fold(defaultAgg,fn1):根据传入函数对数据进行聚合处理,同时支持给agg设置初始值defaultAgg,累计次数跟分区相关

>>> rdd.fold(100,lambda agg,curr:agg+curr)	## 是3分区,对3分区进行聚合操作,100被用了4次结果455
455

aggregate(defaultAgg, fn1, fn2):根据传入函数对数据进行聚合处理。defaultAgg设置agg的初始值,fn1对各个分区内的数据进行聚合计算,fn2 负责将各个分区的聚合结果进行汇总聚合

>>> rdd.aggregate(100,lambda x,y:x+y,lambda x1,y1:x1*y1)
163346000
# 以下代码模拟aggregate内部运算过程:[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]这是三个分区
>>> vals = [106, 115, 134]
>>> from functools import reduce
>>> reduce(lambda agg1, curr1: agg1*curr1, vals, 100)
163346000

4.5 关联算子

关联函数,主要是针对kv类型的数据,根据key进行关联操作
相关的算子:

join:实现两个RDD的join关联操作
leftOuterJoin:实现两个RDD的左关联操作
rightOuterJoin:实现两个RDD的右关联操作
fullOuterJoin:实现两个RDD的满外(全外)关联操作

第四章 RDD持久化、共享变量、内核调度

1 RDD的持久化

为什么需要缓存:

当RDD产生时,代价都是比较大的(从磁盘中读取(耗时较多)通过复杂计算(消耗资源)),RDD可能会被多方使用,默认情况每次使用RDD都会重新计算一遍(RDD本身不存储数据的)。期望RDD能够不用重复计算,提升执行的效率。
可以将RDD设置成缓存,后续使用时不用再重复计算。

Spark有容错能力,如果某个RDd计算失败(失败的原因有很多,例如当前资源不够)需要对整个RDD链进行回溯。如果有缓存,则可以继续从缓存的地方继续进行计算。只用从环境的地方计算,不用从头计算。

缓存使用的场景:

1 当RDD重复使用时,避免重复计算,可以用缓存解决
2 当RDD计算代价很大时,设置缓存
3 当需要提升容错能力时,设置缓存

注意事项:

1 缓存是一种临时存储,可以把数据存在内存中,磁盘中
2 由于缓存时临时存储机制,所以数据有可能会丢失。所以缓存不会清理RDD之间的依赖关系,万一缓存丢失,还可以从头计算一遍。
3 rdd的transformation操作都是惰性,所以并不会立即触发,如果需要立即触发,则可以调用一个action操作,一般推荐count()

定义:把数据存储在内存中或磁盘
持久化的原因:
1、避免重复计算
2、后续计算可能会失败,失败后会重新计算

1.1 chche缓存

缓存如何使用:相关API
rdd.cache() 缓存默认设置在内存中,本质就是调用persist函数
rdd.persist() 缓存默认设置在内存中,可以指定缓存其他位置

DISK_ONLY: ClassVar[StorageLevel] 仅存在磁盘中
DISK_ONLY_2: ClassVar[StorageLevel] 仅存在磁盘中,保存两份
MEMORY_ONLY: ClassVar[StorageLevel] 仅存在内存中
MEMORY_ONLY_SER: 先进行序列化操作,再进行缓存,占用空间少,花费时间多
MEMORY_ONLY_2: ClassVar[StorageLevel]
DISK_ONLY_3: ClassVar[StorageLevel]
MEMORY_AND_DISK: ClassVar[StorageLevel] 有限保存到内存中,内存不够可以保存至磁盘中
MEMORY_AND_DISK_2: ClassVar[StorageLevel]
OFF_HEAP: ClassVar[StorageLevel]

序列化:把对象转成二进制,序列化后占用空间少,是需要时间进行转换的
反序列化:把二进制转成对象

1.2 checkpoint检查点

检查点和缓存类似的,也是把rdd的结果进行存储的操作。一般把结果存储在hdfs中,数据存储就更加可靠(利用hdfs的存储机制),所以检查点会清除之前的RDD的依赖关系(因为数据存储更加可靠)。

checkpoint出现之后是否可以提升执行效率?
相对来说并不会提升太多的执行效率,但是可以提升缓存数据的安全性

checkpoint是由spark进行设置的,但是存在hdfs中,需要手动删除。即使程序停止、运行结束也不会自动删除。但是这个保存的结果在程序下一次运行时没什么帮助的。

如何设置检查点:

1,使用SparkContext.setCheckpointDir设置检查点保存的目录
2,rdd.checkpoint()必须在任何一个action操作之前被调用
3,最好设置rdd缓存在内存中,否则设置检查点时会重新计算
4,设置完成后调用action操作,立即触发

缓存和检查点区别:

1,存储位置
缓存:内存和磁盘都可以存储
检查点:hdfs中,也可以存在本地磁盘,仅限于使用local模式运行,local模式运行时没有必要取设置检查点
2,依赖关系
缓存:缓存不会删除依赖关系,缓存是临时存储,不能删除依赖关系,防止回溯计算
检查点:会删除(截断)依赖关系,因为检查点把数据存在hdfs中,认为是安全的
3,生命周期
缓存:当程序运行完成后,缓存会被自动清理。也可以手动清理,unpersist()函数进行清理
检查点:保存在hdfs中,并且不会自动删除的,需手动删除。即使不删除检查点,下一次运行也没有用了

实际使用时用checkpoint还是缓存:两个可以同时使用,官方强烈建议
rdd_filter.checkpoint() # 设置检查点,如果在checkpoint之前执行了action操作,检查点无效
rdd_filter.cache() # 在Memory中进行缓存
rdd_filter.count()

2 RDD的共享变量

2.1 广播变量

作用:减少Driver和Executor之间的数据传输,减少每个线程中内存的占用
使用场景:多个线程使用同一个变量时

如果不设置广播变量,则每个线程都会拷贝一份这个变量的副本,拷贝时占用带宽,拷贝完成会占用过多的内存
解决方案:让executor从driver中拉取一个副本,不需要每个线程中的都持有者个副本,可以减少带宽和内存

广播变量只能读取,不能修改
如何使用:在driver中创建

定义一个变量a:a = 100
设置广播变量:广播变量名= sc.broadcast(a)
获取变量值:广播变量名.value

import os
from pyspark import SparkConf, SparkContext
os.environ['SPARK_HOME'] = '/export/server/spark'
os.environ['PYSPARK_PYTHON'] = '/root/anaconda3/bin/python'
os.environ['PYSPARK_DRIVER_PYTHON'] = '/root/anaconda3/bin/python'
os.environ['JAVA_HOME'] = '/export/server/jdk1.8.0_241'
if __name__ == '__main__':
    print("pyspark")
    # 1 准备运行环境
    conf = SparkConf().setMaster("local[*]").setAppName("pyspark案例")
    sc = SparkContext(conf=conf)
    # 2 定义广播变量
    a = 1000
    bc_a = sc.broadcast(a)
    # 3 使用广播变量
    rdd1 = sc.parallelize([1,2,3,4,5,6,7,8,9,10], 5)
    rdd2 = rdd1.map(lambda x: x+bc_a.value)
    print(rdd2.collect())

2.2 累加器

累加器在多个线程中对同一个变量提供累加的操作,对于每个线程只能写数据到这个累加器,不能读取累加器的值,读取的操作只能由Driver进行。
应用场景:全局中需要进行累加的场景
如何使用:

1 在Driver中设置一个累加器
累加器变量名 = sc.accumulator(初始值)
2 在线程中进行累加操作
累加器变量名.add(累加值)
3 在Driver中获取
累加器变量名.value

累加器的现象:

如果对设置累加器的RDD进行多次的action操作,则这个累加器会累加多次
如何避免?当累加器执行完成RDD可以设置一个缓存或者检查点,可以避免多次累加
原因:当调用action操作时,会产生多个Job(任务),RDD不存储值,只存储规则,每个任务执行时,对应累加器都会进行累加操作,累加器是全局性的,所以有多少个action就有多少次累加。

3 RDD的内核调度

基本概念:

Application:指用户编写的Spark应用程序/代码,包含Driver功能代码和分布在集群中多个节点上运行的Executor代码;
Driver:即运行上述Application的Main()函数并且创建SparkContext,SparkContext负责和ClusterManager通信,进行资源的申请、任务的分配和监控等;
Cluster Manager:指的是在集群上获取资源的外部服务,Standalone模式下由Master负责,Yarn模式下ResourceManager负责;
Executor:是运行在工作节点Worker上的进程,负责运行任务,并为应用程序存储数据,是执行分区计算任务的进程;
RDD:Resilient Distributed Dataset弹性分布式数据集,是分布式内存的一个抽象概念;
DAG:Directed Acyclic Graph有向无环图,反映RDD之间的依赖关系和执行流程;
Job:作业,按照DAG执行就是一个作业,Job==DAG;
Stage:阶段,是作业的基本调度单位,同一个Stage中的Task可以并行执行,多个Task组成TaskSet任务集;
Task:任务,运行在Executor上的工作单元,1个Task计算1个分区,包括pipline上的一系列操作;

3.1 RDD的依赖

RDD之间存在依赖关系,也是RDD非常重要的特性,RDD的依赖关系可以分成窄依赖宽依赖
1 窄依赖
作用:可以让各个分区中的数据进行并行运算
定义:父RDD的某个分区被子RDD的某个分区全部继承
窄依赖
2 宽依赖
作用:划分stage
定义:父RDD的一个分区被子RDD的多个分区接收并处理
宽依赖

判断两个RDD之间是宽依赖的依据为:是否发生shuffle,一旦产生shuffle,必须前面的RDD计算完成后才能计算后面的RDD

Spark中每个算子是否会产生shuffle,由Spark本身决定,实现spark框架代码时就已经确定了。
map一定不会产生shuffle,reduceByKey一定会产生shuffle
如何判断某个算子是否有shuffle,如果执行到某个算子,执行流程被断开生产新的stage,则说明该算子会产生shuffle。也可以查看官方文档。
reduceByKey一定会产生shuffle
会产生shuffle的算子分在前面这个stage中

3.2 DAG与stage

DAG有向无环图,整个的流程是有方向的,但是不会形成一个圆环
如何形成DAG:

第一步:driver程序处理rdd相关代码时,分析代码,遇到action操作时,就会把action依赖的rdd统一进行分析
第二步:对整个阶段进行回溯,回溯时判断依赖关系,如果宽依赖就会形成一个新的stage

在这里插入图片描述

3.3 RDD的shuffle

shuffle发展历史

① 1.1之前采用 hash shuffle
② 1.1之后引入sort shuffle 主要增加合并和排序操作,对hash shuffle进行优化
③ 1.5 钨丝计划 提升cpu和内存效率
④ 1.6 合并到sort shuffle
⑤ 2.0 删除hash shuffle 全部合并到sort shuffle

淘汰:
优化前的shuffle
shuffle过程
父RDD进行shuffle时会在各自的分区中产生和子RDD分区数量相等的文件数量(在内存中放不下时)每个文件对应子RDD的一个分区,shuffle结束后,子RDD的分区从对应文件获取数据

有问题:父RDD产生的文件数量太多,并且都是小文件,对应磁盘IO增大,打开关闭文件的次数就增多;子RDD读取时也会遇到相同问题

淘汰:
在这里插入图片描述

由executor进行统一的管理,产生和子RDD分区数量相同的文件数量
线程数据数据时输出到对应的文件即可
子RDD拉去数据时只用拉去分区对应的数据即可

在这里插入图片描述

父RDD把线程中的数据分好区之后写入到内存中,当内存中数据达到阈值(1w),触发溢写操作,把数据写入到小文件中,当父RDD的操作完成后,需要对小文件进行合并,最终合并成一个大文件,大文件有一个对应的索引文件。子RDD获取数据时,可以通过索引文件快速找到对应数据,进行拉取。

sort shuffle两种运行机制
① 普通运行机制:

带有排序操作,父RDD把线程中的数据分好区之后写入到内存中,当内存中数据达到阈值(1w),触发溢写操作,把数据写入到小文件中,写入过程中会进行排序操作。当父RDD的操作完成后,需要对小文件进行合并,最终合并成一个大文件,大文件有一个对应的索引文件。子RDD获取数据时,可以通过索引文件快速找到对应数据,进行拉取。

② bypass机制:

没有排序操作,并不是所有的sort shuffle都会走bypass机制
需要满足条件:
1 上游RDD分区数量小于200
2 上游不能提前进行聚合操作

bypass机制效率会更高一些

3.4 JOB调度流程

Spark Application应用的用户代码都是基于RDD的一系列计算操作,实际运行时,这些计算操作是Lazy执行的,并不是所有的RDD操作都会触发Spark往Cluster上提交实际作业,基本上只有一些需要返回数据或者向外部输出的操作才会触发实际计算工作(Action算子),其它的变换操作基本上只是生成对应的RDD记录依赖关系(Transformation算子)。

一个Spark应用程序包括Job、Stage及Task:

① Job是以Action方法为界,遇到一个Action方法则触发一个Job;一个JOB(任务)通常和一个action操作是对应的(sortBy()算子除外)
② Stage是Job的子集,以RDD宽依赖(即Shuffle)为界,遇到Shuffle做一次划分;
③ Task是Stage的子集,以并行度(分区数)来衡量,分区数是多少,则有多少个task。

Driver调度方案!

Driver会构建的四个核心对象:SparkContext、DAGscheduler、TaskSchedulerscheduleBackend
1 当main运行时,首先创建SparkContext 同时在底层会创建DAGscheduler 和TaskScheduler
2 当driver处理RDD相关代码时遇到一个action操作就会触发一个Job,有一个action就有一个Job
3 当触发了Job后,driver负责分配工作(构造DAG,确定每个stage中有多少个线程,每个线程分别在哪个executor中运行)
3.1 driver中运行DAGschudler,进而确定任务有几个stage,每个stage中有多少线程,把每个stage中运行的线程封装到TaskSet的列表中,有多少个Stage,就有多少个TaskSet,把TaskSet发给TaskScheduler
3.2 接下来就有TaskScheduler进行处理,根据接收的TaskSet中描述的线程信息,发送线程任务到executor中,由它进行执行。尽可能保证每个分区的task任务运行在不同的executor中,确保资源运行最大化。资源是由TaskScheduler进行申请

Job调度流程

3.5 Spark的并行度

spark的并行度,决定了spark执行效率的很重要的因素.
spark并行度取决于

① 资源: 提交任务时设置executor的数量以及每个executor的cpu memery
② 数据: 根据数据大小 分区数量

当申请资源比较大,但是数据量不大,虽然不会影响当前任务的效率,但是对资源是浪费的;申请资源较小,但是数据量很大,无法充分利用集群的并行能力

需要进行反复的调试:一般一个cpu核 运行2~3线程 一个cpu核对应3~5G

命令行提交时
--conf "spark.default.parallelism=10" 这里是针对集群设置的,如果是local,local[*]设置

这里并行度决定shuffle后的分区数量

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值