Spark的基本术语及结构

官网文档:https://spark.apache.org/docs/latest/cluster-overview.html

术语

SPARK pack sparkpack erp_jar包


这里是官网上对术语的解释的一张截图

Application:这是一段通用的程序构建在spark上的,将会由1个driver和n个executers这两种进程所组成。(driver和executers重要)

Application jar:这是一个jar包,里面包含了我们要运行的Spark程序,所以我们可以理解为将我们编辑好的Spark程序打成jar包,这个jar包是用来运行在Spark上面的。

Driver program:首先这是一个进程(Spark里面唯二的两个进程),其主要运行了Spark程序里面的主方法,同时负责创建SparkContext这个对象。

Cluster manager:一个外部的服务,用于为我们的集群获取资源。我们的Spark作业是用运行在集群上面的,但是Spark就是一个代码的载体,你要执行计算肯定是需要资源的。所以我们就需要一个集群管理器来为我们要执行的作业来获取和分配资源。

Deploy mode:这个主要是描述我们的driver端是在哪里运行的,在’cluster’模式下,跑在cluster里,在‘client’模式下,跑在外边。

Worker node:其实就是集群上的一堆工作节点,和yarn的NodeManager所在的节点一样。

Executor:这个就是Spark上面另外一个进程了,其主要是执行在worker node上面的工作进程,和NodeManager是一样的。同时负责执行计算和存储数据到内存和磁盘上。

Task:Spark上的最小工作单元,它将会被分发到个executer上面去执行。

Job:包含多个task的一个平行计算,每一次action定义一个job。

Stage:是一个Job中的一个执行阶段,其内部也同时包含多个task,以一个shuffle定义一次stage。

组成

这里放一张官网的结构图:

SPARK pack sparkpack erp_SPARK pack_02


首先我们可以从图中了解到,一个Spark作业是需要一个driver端和N个executers端的。在这个driver的进程里面有一个SparkContext这样的对象用于和我们的集群管理器进行通信,并要求它分配资源给我们的executers这些进程,所以SparkContext也是整个Spark作业的协调者,其会将你的spark代码分发到各个executer中,然后才是将task分发到executer上面去,这样task在执行的时候才能有代码的支撑。而每一个executer进程都是运行在我们的worker node里面,executer这个进程里面将包含一堆的task线程(这也是为什么说spark和MapReduce不一样了,后者是进程级别的)用于执行计算,同时executer也会负责将计算过程中所产生的数据存储下来(可以是内存也可以是磁盘)。最后,driver进程需要和各个excuter进程保存通信以便与其对各个executer进程的执行情况进行监督,而各个executer之间有时候也是要通信的。但是,不同的spark作业之间是完全隔离的,所以不同的作业之间也是不共享数据的,所以即使你的两个作业之间有一部分的执行过程一样,所用数据一样,也将会产生一样的额外数据占用到我们的内存中,每一个Spark Application都有一个属于自己的JVM。

Spark上面的一些算子

map 和 mapPartition

SPARK pack sparkpack erp_数据_03


SPARK pack sparkpack erp_数据_04


根据源码给出的我们看出来map中所传入的参数是RDD里面的每一个元素,所以我们在map里面传入的函数是针对一个元素的操作。而mapPartition里面所传入的是一个partition,每一个partition里面可能包含多个元素的,所以里面传入的函数是针对一个序列来操作的。

当什么时候我们要使用mapPartition呢?当我们对每一个元素执行的操作是要其一个进程这种很耗性能的时候,我们可以考虑使用mapParition来处理,每一个partition执行一次操作,这样可以节省大量的资源消耗。

coalesce 和 repartition
其实这两个算子都是用来对RDD的partition进程改变的,那为什么我们要改变一个RDD里面partition的个数呢?这生产上面我们可能会用到很多的filter算子,而它是直接作用到每一个partition上面的,这样经过多次filter以后,每个partition上面的数据都会不一样,并且有的partition上面可能还是空的(按照我们之前的说法一个partition对应一个task),这样还是要起很多个线程,也是一种浪费资源的行为。
而coalesce有两个参数,一个是新partition的个数,一个是是否要开启shuffle操作,这个参数默认是false,所以当是false的时候,coalesce是只能减少partition的个数的。当我们要增加partition的个数时可以直接使用repartition这个算子,其底层也是调用了coalesce这个算子,但shuffle是开启的。

shuffle
怎么理解shuffle呢?简单来说,它就是一个数据重新分配的过程,由于原来的数据是存储在各个partition上面的,而不同的partition有存储在不同的worker node上面,所以当要使用这些数据来重新构建一些新的partition的时候,肯定是需要通过网络来传输数据到新的worker node上面的,所以一般的shuffle也是会消耗磁盘的io,数据的序列化和网络的io等资源的。相信很多人都听过大数据是要尽量避免shuffle的,但是有的时候因为数据倾斜等原因,我们还是要人为地制造一些shuffle上去。

窄依赖 和 宽依赖

我们首先给出一个定义:当一个父partition被用来产生多个子partition的时候是宽依赖,而一个父partition只被用来产生一个子partition的时候就是窄依赖了。

SPARK pack sparkpack erp_SPARK pack_05


图中,前面的图就是窄依赖了(join有的时候也可以是窄依赖的,有时也是宽依赖),后面的是宽依赖。那么我们可以理解为宽依赖是有shuffle的,而窄依赖中的join是不用的(只要join的两个rdd的分区数是一样的,同时生成新的rdd的分区数也是一样的,那么这里只需要执行网络传输缺没有shuffle,典型的例子将同key的数据都落在同一个partition里面,怎么落呢?groupByKey这个算子呀)

这里总结一句话,但使用窄依赖的时候是没有shuffle的,里面的父partiton将直接生辰新的子partition(这里存在网络传输),可以注意一下窄依赖的partition个数的变化。而宽依赖是有shuffle的,其partition的个数肯定是不相同的。

groupByKey 和 reduceByKey
我贴出两段代码

val A = sc.textFile("file:///home/hadoop/data/test_data/helloword.txt").flatMap(x => x.split('\t')).map((_,1)).reduceByKey(_ + _).collect
val B = sc.textFile("file:///home/hadoop/data/test_data/helloword.txt").flatMap(x => x.split('\t')).map((_,1)).groupByKey().map(x => (x._1,x._2.sum)).collect

两者在SparkUI上的显示:

这个是使用了reduceByKey算子的

SPARK pack sparkpack erp_数据_06


这个是使用了groupByKey算子的

SPARK pack sparkpack erp_spark_07


为什么使用了reduceByKey这个算子可以在shuffle阶段更减少数据量呢?小伙伴可以了解一下combiner这个过程,就是在shuffle之前对各个partition上的元素先进行预reduceByKey,然后在进行shuffle,那么数据量就会减少了嘛。