目录
五、SparkSQL和Hive的集成(Spark on Hive)
3、在pycharm编写的代码中配置Spark on Hive
一、Spark的内核调度(面试重点)
1、DAG
有向无环图(DAG):有方向没有形成闭环的一个执行流程图(就是各个RDD之间的血缘关系),其作用就是标识代码的逻辑执行流程:
job子任务与Action算子的关系:一个程序job子任务的个数等于Action算子调用次数。例如下面的代码:
该程序Application一共有三个Action算子,Action算子就是转换算子的开关,每个Action算子控制/触发着一个执行流,每个执行流就是一个子job,上述代码的DAG图为:
上图可以看作有3个DAG图。利用time.sleep(100000)使程序一直运行,观察4040端口,发现共有3个job,打开每个job可以看到对应的DAG:
一个Application可以有多个job,每个job内含一个DAG,同时每个job都是由一个Action产生。上述画的DAG是没有分区的DAG,我们通过分析代码可以很简单的画出来,它的目的是为了构建物理上的Spark详细执行计划(带分区的DAG),Spark是分布式的,因此DAG和分区是有关联的:
普通DAG:
带分区的DAG:(假设所有的RDD都有3个分区)
2、DAG的宽窄依赖和Stage阶段划分
两两SparkRDD之间有两种关系:窄依赖和宽依赖
窄依赖:父RDD的一个分区全部将数据发送给子RDD的一个分区
宽依赖:父RDD的一个分区将数据发送给子RDD的多个分区,宽依赖又称shuffle
判断是否为宽依赖:父RDD有分叉就是宽依赖。
思考:将RDD之间的关系划分为宽依赖和窄依赖的目的是什么?为了完成Stage阶段划分
对于Spark来说,会根据DAG,按照宽依赖,划分不同的DAG阶段。划分规则是:从后向前,遇到宽依赖就划分出一个阶段,称之为Stage。在Stage之间一定都是宽依赖,在Stage的内部,一定都是窄依赖:
这样划分以后使得每个Stage内部都很规整,每个Stage会被进一步划分为多个Task任务(线程)来具体的执行程序。Task任务的划分就如上图所示是按照分区划分的,3个分区就是3个Task,由于Stage内部都是窄依赖(一对一),这样可以保证各个Task之间是相互独立的,可以并行执行,效率很高。
3、内存迭代计算
在Spark中一个Exectuor是一个进程,拥有独立的计算机资源,每个Executor内部有多个具体的Task任务(线程)来执行程序,在同一个Executor内部的所有计算过程都是在内存中计算的,因此每个Task任务就是一个"内存迭代计算管道",阶段0,1都是有3个独立的内存迭代计算管道,都是由3个线程并行工作。在Stage之间的宽依赖需要进行网络传输,但并不是绝对100%的网络传输,例如Task1与Task4都在Executor1内部,因此这部分的传输是在内存中的。同理Task2与Task5,Task3与Task6也是走内存的。
注意Spark的性能保证是优先保证并行度的前提下,尽可能做到内存迭代计算。因此不能将Task1-6都放到一个Executor内部。
Spark默认受全局并行度的限制(就是数据输入时自己指定的那个分区数),除了个别算子有特殊的分区情况,大部分算子都会遵循全局并行度的要求来规划自己的分区数。例如我们将分区数设为3,基本大多数的算子RDD的分区数都是3,这样做可以保证内存迭代计算,如果我们修改了分区数大概率会破坏内存迭代计算管道的长度和数量,产生新的shuffle。产生新的shuffle就是宽依赖,必然导致新的Stage划分。这就是为什么不建议使用repartition,coalesce,partitionBy算子增大分区数的原因。
4、Spark的并行度
并行度:在同一时间,有多少个Task并行执行。例如并行度设为6,其实就是有6个Task并行执行。设置并行度不是设置RDD的分区,而是设置Task并行执行的数量,因为Task并行执行的数量,所以RDD的分区才被构造的与并行度一样。
我们可以在代码中和配置文件中以及提交程序的客户端参数中设置并行度,优先级从高到低分别是:代码>客户端提交参数>配置文件。如果不设置并行度默认为1,但不会全部以1来跑,多数情况下会基于读取文件的分片数作为默认并行度。
(1)设置全局并行度
全局并行度配置参数:spark.default.parallelism
(2)全局并行度在集群中如何规划
一般设置为CPU总核心数的2-10倍,例如集群可用CPU核心为100个,我们建议将并行度设置为200-1000,确保是CPU核心数的整数倍。最大一般10倍或者更高20倍,50倍,不建议太高,适当即可。
思考:为什么设置最少为CPU核心的2倍呢?
5、Spark的任务调度
Spark的任务是由Driver进行调度的,主要包含:逻辑DAG产生,分区DAG产生,Task划分,将Task分配给Executor并监控其工作。
A:首先构建出Driver,Driver生成执行环境入口对象SparkContext
B:提交当前的Job任务给DAG调度器,DAG调度器会生成该Job的逻辑DAG和分区DAG,然后基于分区DAG进行Stage划分和Task划分。
DAG调度器的主要任务就是:将逻辑的DAG图进行处理,最终得到逻辑上的Task划分
例如:给定一个job,该job被划分为两个stage,每个stage有xxx个task,每个task的具
体工作是xxx,那些task之间需要进行数据交互:
C:划分完以后Driver会基于Task调度器将各个Task任务分配给相应的Executor,并监控它们工作。
Task调度器的主要任务就是:基于DAG调度器的结果,来规划这些逻辑的Task应该在那
些物理的Executor上运行,以及监控管理它们的运行。
例如:当前有3个Executor,显然Task1,2,3需要并行执行,因此将Task1,2,3分别
放到不同的Executor上,同理,将Task4,5,6也放到不同的Executor上:
假设有2个Executor:
D:Worker下的Executor被Task调度器管理监控,听从它们的指令干活,并定期汇报进度。
先DAG调度然后在Task调度,就好比在公司有一个大型项目,项目经理(Driver)先根据git搭建项目环境(SC),将项目进行分解为多个Task,选出一个队长(Task调度器)让其分配这些任务各个各个员工(Executor),最后由队长来监控整个项目的开发,并定时向他(Driver)汇报。
注意:并不是一个服务器只能有一个Executor,可以有多个:例如当前有2台服务器,我们可以开辟2个Executor(一般情况下为了效率有多少个服务器就开辟多少个Executor),当然可以开辟4个Executor:
走内存效率 > 走本地回环效率 > 走网络通信效率
6、Spark的shuffle
在Spark中shuffle是无时无刻都存在的,一般是无法避免的。
Spark在DAG调度阶段会将一个Job划分为多个Stage,上游Stage做Map工作(提供数据的称为Map端,也叫Shuffle Write),下游Stage做Reduce工作(接收数据的称为Reduce端,也叫Shuffle Read),其本质上还是MR计算框架。Shuffle是连接Map和Reduce之间的桥梁,它将Map的输出对应到Reduce的输入中,涉及序列化反序列化,跨节点网络IO以及磁盘读写IO等。
Spark提供两种shuffle管理器:HashShuffleManager和SortShuffleManager
(1)HashShuffleManager
HashShuffle就是根据某个字段进行hash取模,模数相同的数据会保存在一起。针对未优化的HashShuffle它是以task为单位的,每个task独立处理,独立分配磁盘空间。
(2)SortShuffleManager
SortShuffleManager的运行机制有两种分别是:普通运行机制和bypass运行机制
可以看出SortShuffle相比于HashShuffle磁盘文件的数量大大减少了,这样磁盘IO就大幅度降低了,提高了shuffle操作的性能,这就是为什么spark在新版本中丢弃了HashShuffle。
bypass运行机制的触发条件:shuffle map task的数量小于spark.shuffle.sort.bypassMergeThreshold=200设置的值;不是聚合类的shuffle例如reduceByKey。bypass运行机制整体与普通机制一致,唯一不同的是写入磁盘临时文件的时候不会在内存中进行排序。
SortShuffle主要通过对磁盘文件进行合并,以减少文件数量,同时两类shuffle都需要经过内存缓冲区溢写磁盘的场景。因此,尽管spark是内存迭代计算的框架,但是内存迭代主要是在窄依赖中进行的。遇到shuffle还是需要进行磁盘IO的。
二、SparkSQL基础
1、SparkSQL基本概念
(1)介绍
SparkSQL是Spark的一个模块,用于处理海量结构化数据(分布式SQL计算引擎)。它不像RDD一样什么数据都能处理。SparkSQL支持SQL语言/性能强/可以自动优化/API简单/兼容Hive,目前在国内外都很火热,常用于离线开发,数仓搭建,科学计算,和数据分析。从本质上讲SparkSQL与Hive很类似。
SparkSQL的特定:
可见SQL语言和python代码可以很好的融合在一起使用。
(2)SparkSQL与Hive的异同
SparkSQL由于其使用内存计算,且底层使用丰富的RDD算子,因此其具有更好的性能。但是SparkSQL本身没有元数据管理,尽管如此,由于SparkSQL可以和Hive集成因此可以借用Hive的Metasotre。此外SparkSQL支持纯SQL运行,纯Python运行,和混合运行,而Hive只支持SQL运行。
(3)SparkSQL的数据抽象
SparkCore中的数据抽象是RDD对象,可以存储任意类型的数据,数据转化为RDD对象后是分区存放的(分布式存储);python中常见的pandas模块的数据抽象是DataFrame对象,它是一个二维表数据结构,存储的数据是本地存放的;SparkSQL的数据抽象也叫做DataFrame,同样也是一个二维表数据结构,这与pandas中的一样,唯一不同的是SparkSQL中的DataFrame对象是存储在分区中的(即分布式存储的)
当然SparkSQL的数据抽象不仅仅只是DataFrame,它总共有三种数据抽象对象,分别是SchemaRDD(已废弃),DataSet对象(可用于java,scala),DataFrame对象(可用于java,scala,python,R),我们以python开发SparkSQL,主要使用DataFrame对象作为核心数据结构。
SparkSQL的DataFrame与RDD的唯一区别就是一个只支持存储二维表结构数据,一个支持存储任意类型数据(以任意格式存储):
例如有如下数据:
SparkSQL中DataFram只能以二维表格式存储上述表:
RDD按理说可以按任意类型存储,它是一个抽象的概念,具体的数据是无法查看的,只能强制转换为lsit,.....来查看
显然SparkSQL的存储方式更适合于处理结构化数据。
(4)SparkSession对象
在上文进行RDD迭代计算之前,必须创建程序执行入口对象SparkContext。在Spark2.0之后推出了一种新的程序执行入口对象SparkSession,SparkSession就是SparkContext的升级/扩充。SparkSession不仅仅支持RDD编程(可以通过SparkSession对象来获取SparkContext对象),而且支持SparkSQL编程,而SparkContext只能进行RDD编程。后续开发的代码我们都会使用全新的SparkSession对象作为程序执行的入口。
SparkSession对象的创建:
SparkSQL的HelloWorld:
可以看出SparkSQL的API也是很好理解的,运行结果:
2、SparkSQL DataFrame API开发
(1)DataFrame的组成
DataFrame是一个二维表结构数据,由行,列,表结构描述(列名,列类型,列约束)组成
在结构层面:StructType对象描述了整个DataFrame的表结构信息,StructField对象描述一个列的信息。
在数据层面:Row对象记录一行数据,Column对象记录一列数据并包含列信息(记录具体列数据+StructField描述的列信息)
一个StructField记录单个列的列名,列类型,列是否允许为空,多个StructField组成一个StructType对象。StructType描述整个DataFrame的表结构,记录各个列的名字,类型,各个列是否允许为空。
(2)DataFrame的创建
方式一:RDD对象转换1
DataFrame对象和RDD对象都是用于存储数据的,只是存储的格式不同。因此DataFrame对象可以从RDD转换而来。假设我们有如下文件,现需要现构建RDD对象,然后将RDD对象转化为DataFrame对象。
方式二:RDD对象转换2
在将RDD对象转换为DataFrame对象过程中,通过StructType对象来定义DataFrame的“表结构”。使用StructType()需要导包:from pyspark.sql.types import StructType
方式三:RDD对象转换3
使用RDD的toDF算子完成转换
方式四:基于Pandas的DataFrame转换
将Pandas的DF对象转换为分布式的DF对象
方式五:通过读取外部数据直接创建DataFrame
通过SparkSQL的通一API进行数据读取构建DataFrame对象,语法如下:
A:text文本格式数据读取
当format指定为text格式时,会将一整行数据当为一列,这显然不是我们想要的,为了解决这个问题,可以指定format为csv格式读取txt文件,系统可以自动识别,此时schema需要手动修改。
B:json格式数据读取
json类型的数据自带列名name和age因此可以直接加载,系统自动识别:
C:csv格式数据读取,语法如下:
注意列类型必须大写
D:parquet格式数据读取
parquet格式是Spark中常见的一种列式存储文件格式。与hive中的orc类似,直接打开是一堆乱码,因此parquet文件不能直接打开查看。但是其内部内置了schema(列名/列类型/是否为空)
array类型是一个嵌套类型。我们还可以使用pycharm中的插件打开parquet文件
(3)DataFrame的编程
DataFrame的编程支持两种风格,分别是DSL风格和SQL风格。DSL风格是分布式DataFrame的特有API,例如df.where().limit()。SQL风格就是写sql语句来处理DataFrame数据,例如spark.sql(‘select * from xxx’)。
数据准备:stu_score..txt
DLS风格演示:
注意:select,filter,where API的返回值还是DataFrame,但是group by API的返回值是一个特殊的类型GroupedData。聚合后的数据又变为了DataFrame类型,因此GroupedData对象是一个中转对象(临时对象)。能够调用show()方法的对象都是DataFrame对象,调用完show()方法后就不是DataFrame对象了。
SQL风格:
使用SQL风格时,需要将DataFrame注册成一个表,起一个名字,之后就可以基于这个名字使用sql语句进行操作了。
全局表:跨SparkSession对象使用,在一个程序内的多个SparkSession中均可调用,查询前需要带上前缀:global_temp.。临时表:只在当前SparkSession内有效。
(4)pyspark.sql.functions包
PySpark提供了一个有很多内置sql函数的包pyspark.sql.functions,from pyspark.sql import funcations as F导入以后我们可以很方便的使用各种各样的内置sql函数实现复制的计算任务。包中的函数返回值多数都是Column对象。
F.split(被切分的列Column对象,切分字符串) 返回值:Column对象
作用:实现字符串分割
F.explode(被转换的列Column对象) 返回值:Columnn对象
作用:实现数组转列,将数组拆解,类似于flatMap()
(5)单词计数案例WordCount
数据准备:
ascending=False表示降序排序
(6)电影评分数据分析案例
数据准备:Index of /datasets/movielens/ml-100k/u.data
需求分析:
说明:df.where,df.select的返回值是DataFrame对象;F函数的返回值一般都是Column对象;first()函数用于取第一行数据,返回值为Row对象;df.select(F.avg(df['rank'])).first()['avg(rank)']表示平均分的具体值;这里count()函数相当于RDD中的Action算子,返回值不再是DataFrame而是具体的一个值。
说明:可以在agg API中实现多个聚合操作,withColumnRenamed API是DataFrame对象下的改名API,不能在Column对象上使用,我们可以使用alias API对Column对象直接改名。
总结:
(7)SparkSQL Shuffle分区数目
上诉电影评分数据分析案例在执行时利用time.sleep睡眠一段时间,然后打开node11:4040 WEB UI界面:
与RDD编程一样SparkSQL也有Action算子,Action算子的个数等于job的个数,这里我们知道即可。
打开其中一个job任务:
我们发现有的stage任务数有200个(也就是说RDD有200个分区),有的却很少,这是为什么呢?因为在SparkSQL中当job产生shuffle时,默认的分区数为200(spark.sql.shuffle.partitions),即系统默认会构建200个分区来处理复杂的shuffle,这实际项目中需要按照实际情况来调整(集群模式下分区数是总cpu核心数的2-10倍),我们这里的SparkSQL程序是在local[*]模式下运行的,因此不需要将shuffle分区数设置的太大,这样会减低执行效率。
SparkSQL Shuffle分区数设置:
(8)SparkSQL的数据清洗API
在实际生产过程中,获取的数据往往无法直接使用,需要先对数据进行清洗,将杂乱无章的数据整理为符合处理要求的规整数据。
数据准备:
去重API:dropDuplicates----对DF数据进行去重,如果重复数据有多条,取第一条。如果没有指定参数,则对DF进行整体去重;如果指定了参数则按照指定的参数去重。
缺失值处理API(删除缺失值):dropna----对DF数据进行缺失值处理。如果没有指定参数,则DF数据中只要有空数据就会被删除;指定参数的情况如下:
结果与上述一样
缺失值处理API(填充缺失值):fillna----对DF数据的缺失值进行填充处理。
(9)DataFrame数据写出
语法格式:(与spark.read.format()类似)
数据准备:还是使用上述电影分析案例的u.data数据
由于我们当前使用的python环境为远程node11的python环境,因此所生成的文件全部都在linux系统下,需要将linux系统下的文件同步到本地:
(10)DataFrame通过JDBC读写数据库(MySQL)
读取jdbc需要配置jdbc驱动:
如果使用的是本地的python环境,需要将对应的jar包放在:python安装目录\Lib\site-packages\pyspark\jars下。
如果使用的是linux系统的python环境,需要将对应的jar包放在:Anaconda3安装目录/envs/虚拟环境/lib/python3.8/site-packages/pyspark/jars下
数据准备:还是使用上述电影分析案例的u.data数据。
注意:在jdbc写出时,如果表不存在系统会自动在mysql中创建,因为DataFrame数据自带表结构信息StructType
三、SparkSQL函数定义
1、UDF函数
无论是hive还是SparkSQL,其原有的内置函数在使用过程中往往是不够的,需要我们自定义一些函数。hive中的自定义函数如下:
hive非常强大三种自定义函数都可支持,但是SparkSQL针对不同的编程语言以及不同的spark版本,支持不同的自定义函数:
目前SparkSQL仅仅支持定义UDF和UDAF函数,对于python语言仅仅支持直接定义UDF函数。想要定义UDAF需要借助RDD代码间接定义。
2、SparkSQL定义int类型UDF函数
方式一:sparksession.udf.register() ---- 注册一个UDF函数,可用于DSL,SQL风格
方式二:pyspark.sql.functions.udf ---- 自定义一个UDF函数,仅能用于DSL风格
注意:上述两个代码的结果一样,DSL风格结果的列名同样是udf1(num),无论什么风格结果的列名都会依据参数1来设定,一般情况下参数1和UDF对象名是一样的。
3、SparkSQL定义Array数组类型UDF函数
设置show(truncate=False)
4、SparkSQL定义字典类型UDF函数
注意:字典类型的返回值,需要用StructType来描述,因为from pyspark.sql.types import 类型中没有字典类型。
5、SparkSQL定义UDAF函数
(1)将DataFrame对象强制转化为RDD对象
这里需要注意将DataFrame对象强制转化为RDD对象后,返回的结果是ROW对象。
(2)创建UDAF函数
6、SparkSQL使用窗口函数(与mysql中的窗口函数一样)
数据集构建:
四、SparkSQL的运行流程
1、SparkSQL的自动优化
RDD的运行会完全按照开发者的代码执行,如果开发者水平有限,RDD的执行效率也会受到影响。而SparkSQL会对写完的代码执行“自动优化”,以提升代码运行效率,避免开发者水平影响到代码的执行效率。
思考:为什么SparkSQL可以自动对代码进行优化,而RDD不可以?
因为RDD的内含数据类型是不限格式和结构的,优化起来很难,无法进行针对性优化。而SparkSQL的数据类型就是DataFrame这种二维表结构,可以被针对,且二维表结构类型比较好优化。SparkSQL的自动优化依赖于Catalyst优化器。
我们所写的SparkSQL代码最终是会变成RDD代码来运行的。SparkSQL是在Spark Core API(RDD)基础上的一个模块,其底层依赖于RDD。
思考:Catalyst优化器是如何将SparkSQL代码转变为RDD代码的呢?步骤如下:
就是将所有表的列做一下标记,为了方便后续优化过程中快速调用列数据。
score.id-->id#1#L:将score.id标记为1,类型为Long;其它有一样。
优化一:断言下推 Predicate Pushdown(谓词下推),即将Filter这种可以减少数据集的操作下推,放在临近Scan的位置,这样可以减少后续操作的数据量。上述代码在断言下推后会先对people.age进行filter,然后在join,这样减少了join的数据量,提高了性能。----行过滤
优化二:列值裁剪 Column Pruning,在断言下推之后执行裁剪,就是将SQL语句中各个表不会用到的列裁剪掉。比如score表(id,name,class,math_score,english_score,music_score,.....),SQL语句的执行会先执行from,from操作会将表中的所有列全部加载,然而上述SQL语句只用到score.id和score.math_score,因此其他加载的列完全没用。如果不进行列值裁剪操作,那么代码在执行from-->where-->group by-->having-->select(执行到select后代码才会知道只会用到score.id和score.math_score)的过程中会加载全部的列进行操作,会带来较大的IO。列值裁剪就是在刚刚执行完from之后就把后续没用的列全部裁剪掉,大大加快执行速度。
列值裁剪的进一步优化:对于CSV,JSON格式的文件,裁剪操作是先把所有的列加载后,紧接着把无用的列裁剪掉,会涉及加载全部列的过程。因为这些文件格式是以行存储的。对于orc,parquet列示存储的文件,在读取表时可以只读有用的列,不涉及加载全部列的过程。----列过滤
.......除了上述两种常见的优化为SparkSQL还有近100多种优化手段,这里就不一一介绍了。step3最终会生成优化后的AST树(逻辑计划)。
step 4:基于逻辑计划,生成物理执行计划,最终生成RDD代码。
2、SparkSQL的运行流程
五、SparkSQL和Hive的集成(Spark on Hive)
1、基本原理
Hive的核心组件有两个分别是:SQL优化翻译器(执行引擎),翻译SQL到MR并提交到YARN中执行,以及MetaStore元数据管理中心。
注意,前文所学的Spark ON YARN只能实现将编写的RDD代码放到YARN中运行,还无法实现将SQL代码转化为Spark程序,然后放到YARN中运行。将SparkSQL和Hive集成后,就可以实现了。
2、配置
根据原理,配置Spark on Hive就是要告诉Spark,Hive的MetaStore在哪里(IP和端口号)。保证MetaStore是启动的。
在spark的conf目录下创建hive-site.xml文件,并输入以下内容:
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
<!-- 告诉spark,hive的数据仓库地址在哪里,从哪里读取表 -->
<property>
<name>hive.metastore.warehouse.dir</name>
<value>/user/hive/warehouse</value>
</property>
<!-- 告诉spark,hive的metastore在哪里 -->
<property>
<name>hive.metastore.uris</name>
<value>thrift://node11:9083</value>
</property>
</configuration>
由于hive的元数据是存储在MySQL中的。因此spark在连接元数据时,会有部分功能连接到mysql数据库,需要在spark中配置mysql的jar包。
确保hive配置了Metastore服务,检查hive的hive-site.xml文件,确保有如下配置
启动Metastore服务:nohup hive --service metastore >> logs/metastore.log 2>&1 &
检查是否启动:
配置完成,接下来可以测试能否直接编写SQL来运行了:
可以看到在spark程序中可以直接运行SQL了,且创建的表在hive中确确实实存在。Spark on Hive配置成功,我们发现在bin/pyspark解析器环境中需要写spark.sql()。这显然比较费劲,每写一个SQL就必须写spark.sql()。为了解决这个问题spark在bin目录下有一个spark-sql的编程环境入口,进去以后可以直接写sql,非常方便。
在hive中查看表是个被删除:
可见,SparkSQL和Hive使用的是同一个元数据,只是翻译器不同而已,在hive中执行sql语句走的是MR,在SparkSQL中执行sql语句走的是Spark RDD。
在spark中执行:
在hive中执行 :(可见MR没有spark快)
3、在pycharm编写的代码中配置Spark on Hive
显然,我们并没有提前生成DataFrame,且没有将DataFrame创建成临时表。由于配置了Metastore,因此student表具体是谁在哪里,由Metastore来提供。