窄依赖中,父RDD和子RDD间的分区是一对一的。换句话说父RDD中,一个分区内的数据是不能被分割的,只能由RDD中的一个分区整个利用。
上图中P代表RDD中的每个分区,我们看到,RDD中每个分区内的数据在上面的几种转移操作之后被一个分区所使用,即其依赖的父分区只有一个。比如图中的map、union和join操作,都是窄依赖的。注意:join操作比较特殊,可能同时存在宽、窄依赖
Shuffle依赖一会打乱原RDD结构的操作。具体来说,父RDD中的分区可能会被多个子RDD分区使用。因为父RDD中一个分区内的数据会被分割并发送给子RDD的所有分区,因此Shuffle依赖也意味着父RDD与子RDD之间存在着Shuffle过程
上图中P代表RDD中对的多个分区,我么会发现对于Shuffle类操作而言,结果RDD中的每个分区可能会依赖多个父RDD中的分区。需要说明的是,依赖关系是RDD到RDD之间的一种映射关系,是两个RDD之间的依赖,如果在一次操作中设计多个父RDD,也有可能同时包含窄依赖和Shuffle依赖
区分RDD之间的依赖为宽依赖还是窄依赖,主要在于父RDD分区数据与子RDD分区数据关系
Spark中DAG生成过程的重点是对Stage的划分,其划分的依据是RDD的依赖关系,对于不同的依赖关系,高层调度器会进行不同的处理
在Spark中,DAG生成的流程关键在于回溯,在程序提交后,高层调度器将所有的RDD看成是一个Stage,然后对此Stage进行从后往前的回溯,遇到Shuffle就断开,遇到窄依赖,则归并到同一个Stage。的能到所有的步骤回溯完成,便生成一个DAG图
为什么要划分Stage?–并行计算
Spark在DAG调度阶段会将一个Job划分为多个Stage,上游Stage做map工作,下游Stage做reduce工作,其本质上还是MapReduce计算框架。Shuffle是连接map和reduce之间的桥梁,它将map的输出对应到reduce输入中,涉及到序列化和反序列化、跨节点网络IO以及磁盘读写IO等
Spark的Shuffle分为Write和Read两个阶段,分属于两个不同的Stage,前者是Parent Stage的最后一步,后者是Child Stage的第一步
执行Shuffle的主体是Stage中的并发任务,这些任务分ShuffleMapTask和ResultTask两种,ShuffleMapTask要进行Shuffle,ResultTask负责返回计算结果,一个Job中只有最后的Stage采用ResultTask,其他的均为ShuffleMapTask。如果要按照map端和reduce端来分析的话,ShuffleMapTask可以即是map端任务,又是reduce端任务,因为Spark中的Shuffle是可以串行的;ResultTask则只能充当reduce端任务的角色。
根据下游的task决定生成几个文件,先生成缓冲区文件在写入磁盘文件,再将block文件进行合并。
未经优化的shuffle write操作所产生的磁盘文件的数量是极其惊人的。
SortShuffleManager的运行机制主要分成两种,一种是普通运行机制,另一种是bypass运行机制。当shuffle write task的数量小于等于spark.shuffle.sort.bypassMergeThreshold参数的值时(默认为200),就会启用bypass机制。
主要是调整缓冲的大小,拉取次数重试重试次数与等待时间,内存比例分配,是否进行排序操作等等
参数说明:该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小(默认是32K)。将数据写到磁盘文件之前,会先写入buffer缓冲中,待缓冲写满之后,才会溢写到磁盘。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如64k),从而减少shuffle write过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
参数说明:该参数用于设置shuffle read task的buffer缓冲大小,而这个buffer缓冲决定了每次能够拉取多少数据。(默认48M)
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
spark.shuffle.io.maxRetries :shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。(默认是3次)
spark.shuffle.io.retryWait:该参数代表了每次重试拉取数据的等待间隔。(默认为5s)
调优建议:一般的调优都是将重试次数调高,不调整时间间隔。
参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作。
调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些
Spark RDD通过其Transactions操作,形成了RDD血缘关系图,即DAG,最后通过Action的调用,触发Job并调度执行
DAGScheduler负责Stage级的调度,主要是将DAG切分成若干Stages,并将每个Stage打包成TaskSet交给TaskScheduler调度
TaskScheduler负责Task级的调度,将DAGScheduler给过来的TaskSet按照规定的调度策略分发到Executor上执行,调度过程中SchedulerBackend负责提供可用资源,其中SchedulerBackend有多种实现,分别对接不通的资源管理系统
Spark的任务调度总提来说分两路进行,一路是Stage级的调度,一路是Task级的调度
一个Spark应用程序包括Job、Stage及Task:
将Task数量设置成与Application总CPU Core 数量相同(理想情况,150个core,分配150 Task)官方推荐,Task数量,设置成Application总CPU Core数量的2~3倍(150个cpu core,设置task数量为300~500)与理想情况不同的是:有些Task会运行快一点,比如50s就完了,有些Task可能会慢一点,要一分半才运行完,所以如果你的Task数量,刚好设置的跟CPU Core数量相同,也可能会导致资源的浪费,比如150 Task,10个先运行完了,剩余140个还在运行,但是这个时候,就有10个CPU Core空闲出来了,导致浪费。如果设置2~3倍,那么一个Task运行完以后,另外一个Task马上补上来,尽量让CPU Core不要空闲。
参数spark.defalut.parallelism默认是没有值的,如果设置了值,是在shuffle的过程才会起作用
if __name__ == '__main__':
print('PySpark First Program')
# 输入数据
data = ["hello", "world", "hello", "world"]
conf = SparkConf().setAppName("miniProject").setMaster("local[*]")
conf.set("spark.defalut.parallelism", 4)
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
# sc = SparkContext.getOrCreate(conf)
sc = SparkContext(conf=conf)
# 将collection的data转为spark中的rdd并进行操作
rdd = sc.parallelize(data)
# rdd = sc.textFile("file:///export/pyfolder1/pyspark-chapter02_3.8/data/word.txt") \
# .flatMap(lambda line: line.split(" "))
print("rdd numpartitions:", rdd.getNumPartitions())
# 执行map转化操作以及reduceByKey的聚合操作
res_rdd = rdd.map(lambda word: (word, 1)).reduceByKey(lambda a, b: a + b)
# 并行度决定了可以同时处理多少个分区
print("shuffle numpartitions:", res_rdd.getNumPartitions())
print('停止 PySpark SparkSession 对象')
sc.stop()
combineByKey是Spark中一个比较核心的高级且底层函数,其他一些高阶键值对函数底层都是用它实现的。诸如 groupByKey,reduceByKey等等
如下解释下3个重要的函数参数:
案例一:实现将相同Key的Value进行合并,使用groupBy很容易实现
# -*- coding: utf-8 -*-
# Program function:外部集合转为RDD
from pyspark import SparkConf, SparkContext
import re
# 1-准备环境
conf = SparkConf().setAppName("collection").setMaster("local[*]")
sc = SparkContext(conf=conf)
sc.setLogLevel("WARN")
x = sc.parallelize([("a", 1), ("b", 1), ("a", 2)])
def to_list(a):
return [a]
def append(a, b):
a.append(b)
return a
def extend(a, b):
a.extend(b)
return a
print(sorted(x.combineByKey(to_list, append, extend).collect()))
#[('a', [1, 2]), ('b', [1])]
案例二:求平均分的案例代码
# -*- coding: utf-8 -*-
# Program function:外部集合转为RDD
from pyspark import SparkConf, SparkContext
import re
# 1-准备环境
conf = SparkConf().setAppName("collection").setMaster("local[*]")
sc = SparkContext(conf=conf)
sc.setLogLevel("WARN")
x = sc.parallelize([("Fred", 88), ("Fred", 95), ("Fred", 91), ("Wilma", 93), ("Wilma", 95), ("Wilma", 98)])
# (v)=>(v,1),得到的是(88,1),因为这是combineByKey是按照key处理value操作,
# acc:(Int,Int)代表的是(88,1),其中acc._1代表的是88,acc._2代表1值,v代表是同为Fred名称的95的数值,
# 所以acc._1+v=88+95,即相同Key的Value相加结果,第三个参数是分区间的相同key的value进行累加,
# 得到Fred的88+95+91,Wilma累加和为93+95+98。
def createCombiner(a):
return [a, 1]
def mergeValue(a, b):
return [a[0] + b, a[1] + 1]
def mergeCombiners(a, b):
return [a[0] + b[0], a[1] + b[1]]
resultKey = x.combineByKey(createCombiner, mergeValue, mergeCombiners)
print(sorted(resultKey.collect()))
# [('Fred', [274, 3]), ('Wilma', [286, 3])]
print(resultKey.map(lambda score: (score[0], int(score[1][0]) / int(score[1][1]))).collect())
# [('Fred', 91.33333333333333), ('Wilma', 95.33333333333333)]
#lambda表达式版本
resultKey = x.combineByKey(lambda x:[x,1], lambda x,y:[x[0]+y,x[1]+1], lambda x,y:[x[0]+y[0],x[1]+y[1]])
print(sorted(resultKey.collect()))