Spark基础知识

目录

1 Apache Spark 是什么?

1.2 Spark的设计哲学

1.3 Spark应用程序的简单架构

1.4 转换操作

1.5 惰性评估(lazy evaluation)

1.6 DataFrame和SQL

2 结构化API概述

2.1 结构化API执行概述

2.1.1 逻辑计划

2.1.2 物理计划


 


1 Apache Spark 是什么?

Spark是一个在集群上运行的统一计算引擎以及一组并行数据处理软件库

下面展示了Spark给用户提供的组件:

1.2 Spark的设计哲学

统一平台

Spark主要目标是为编写大数据应用程序提供统一的平台,例如,JavaEE的Spring框架。Spark的统一API使得编写大数据应用变得简单且高效。

计算引擎

Spark对比Hadoop这样即解决分布式存储(HDFS)又提供了计算范式(MapReduce)的大数据平台,它主要专注于计算引擎。Spark从存储系统加载数据并对其进行计算,加载结束时不负责永久存储。用户可以将数据存储到外部存储系统。

配套的软件库

Spark最后的一个模块就是它的软件库,Spark的设计理念就是设计一个统一的引擎,为通用的数据分析任务提供统一的API。今天Spark的核心库已经成为了一系列开源项目的集成。常用的Spark库包括Spark SQL、MLlib、Spark Streaming和GraphX。

1.3 Spark应用程序的简单架构

 

驱动器运行main()函数,位于集群中的一个节点上,它主要做三件事:

  • 维护spark应用程序的相关信息
  • 回应用户的程序或输入
  • 分析任务并分发给若干执行器处理

执行器复制执行分配到的实际计算工作,意味着它需要做两件事:

  • 执行代码
  • 将计算状态报告给驱动器结点

1.4 转换操作

Spark的核心数据结构在计算过程中是保持不变的,这符合Scala倡导的使用不可变数据以及函数编程的思想,不可变数据在分布式场景下有着天生的优势。

既然Spark的数据不能改变,所以它提供了一系列函数式API,这个过程被称为转换。

val number1DF = spark.range(100).toDF("number").where("number % 2 = 0")

注意:以上代码的转换并没有实际输出,只是指定了在其数据上进行的操作。

Spark的转换操作是表达业务逻辑的核心,有两类转换操作:窄依赖转换与宽依赖转换。

  • 窄依赖是指父RDD的每个分区只被子RDD的一个分区所使用,子RDD分区通常对应常数个父RDD分区(O(1),与数据规模无关
  • 宽依赖是指父RDD的每个分区都可能被多个子RDD分区所使用,子RDD分区通常对应所有的父RDD分区(O(n),与数据规模有关

区分这两种依赖很有用:

  • 首先,窄依赖允许在一个集群节点上以流水线的方式(pipeline)计算所有父分区。例如,逐个元素地执行map、然后filter操作;而宽依赖则需要首先计算好所有父分区数据,然后在节点之间进行Shuffle,这与MapReduce类似。
  • 其次,窄依赖能够更有效地进行失效节点的恢复,即只需重新计算丢失RDD分区的父分区,而且不同节点之间可以并行计算;而对于一个宽依赖关系的Lineage图,单个节点失效可能导致这个RDD的所有祖先丢失部分分区,因而需要整体重新计算。

1.5 惰性评估(lazy evaluation)

惰性评估的意思就是等到绝对需要时才执行计算,例如:

val number1DF = spark.range(100).toDF("number").where("number % 2 = 0").show

DF.show需要真实的数据,这时候Spark才会出发从原始数据的转化操作。Spark会将一系列转化操作根据依赖关系划分成不同的Stage,然后再Stage里面执行流水线操作,这时候也会优化整个Stage的数据流。

1.6 DataFrame和SQL

//加载数据到DF
val flightDF = spark.read.option("inferSchema","true").option("header","true").csv("filePath")

//创建临时视图
flightDF.createOrReplaceTempView("flight_data")

//使用DataFrame分析数据
val data1=flightDF.groupBy("DEST_COUNTRY_NAME").count().explain

//使用SQL来分析数据
spark.sql("""
   select DEST_COUNTRY_NAME,count(*)
   from flight_data
   group by DEST_COUNTRY_NAME
   """).explain

//explain打印出底层计划是相同的

如上代码,sparkSession提供数据加载的接口,可以使用两种方式来分析数据,且不管使用什么语言,Spark会以完全相同的方式编译到底层的执行计划。如上Scala中使用两种方式的底层执行计算式相同的。


2 结构化API概述

  • Dataset类型
  • DataFrame类型
  • SQL表和视图

Dataset与DataFrame是具有行和列的类似于数据表的集合类型。DataFrame表示非类型化,由Spark维护它的类型(schema);Dataset表示类型化,会在编译的时候检查类型是否符合规范,可以通过Java beans与case class指定其类型。

Spark SQL与Dataset都会被Catalyst引擎解析、优化以及生成物理(RDD)执行计划。

                                                                                   

所以,不同的语言的Dataset API其实都会调用Spark内部的Catalyst,最终得到一样的优化效果。

val df = spark.range(500).toDF("number")
df.select($"number" + 10)

//并不会在Scala中加载执行,而是将其转化为功能相同的Spark内部的Catalyst表达式,在Spark中执行。

2.1 结构化API执行概述

从用户代码到执行代码的过程中,大体的步骤如下:

  • 编写DataFrame/Dataset/SQL代码。
  • 编写的代码首先通过Catalyst优化器的Parser模块被解析为语法树,此棵树称为Unresolved Logical Plan。
  • Unresolved Logical Plan通过Analyzer模块借助于数据元数据解析为Logical Plan。
  • 此时再通过各种基于规则的优化策略进行深入优化,得到Optimized Logical Plan。
  • 优化后的逻辑执行计划依然是逻辑的,并不能被Spark系统理解,此时需要将此逻辑执行计划转换为Physical Plan。
  • Spark检查可行的优化策略,然后在集群上执行最优的物理计划。

可以分为逻辑计划物理计划

2.1.1 逻辑计划

逻辑计划仅代表一组抽象转换,并不涉及执行器或驱动器,它只是将用户的表达式集合转换为最优版本。Spark使用Catalog(所有表和DataFrame信息的存储库)在分析器中解析列和表格。之后Catalyst优化器尝试通过下推谓词或选择操作来优化逻辑计划。

下面部分转载于:http://hbasefly.com/2017/03/01/sparksql-catalyst/?mevone=abuib2

Parser

Parser简单来说是将SQL字符串切分成一个一个Token,再根据一定语义规则解析为一棵语法树。Parser模块目前基本都使用第三方类库ANTLR进行实现,比如Hive、 Presto、SparkSQL等。下图是一个示例性的SQL语句(有两张表,其中people表主要存储用户基本信息,score表存储用户的各种成绩),通过Parser解析后的AST语法树如右图所示:

                                                                     

Analyzer

通过解析后的逻辑执行计划基本有了骨架,但是系统并不知道score、sum这些都是些什么鬼,此时需要基本的元数据信息来表达这些词素,最重要的元数据信息主要包括两部分:表的Scheme和基本函数信息,表的scheme主要包括表的基本定义(列名、数据类型)、表的数据格式(Json、Text)、表的物理位置等,基本函数信息主要指类信息。

Analyzer会再次遍历整个语法树,对树上的每个节点进行数据类型绑定以及函数绑定,比如people词素会根据元数据表信息解析为包含age、id以及name三列的表,people.age会被解析为数据类型为int的变量,sum会被解析为特定的聚合函数,如下图所示:

                                                                       

Optimizer

优化器是整个Catalyst的核心,上文提到优化器分为基于规则优化和基于代价优化两种,当前SparkSQL 2.1依然没有很好的支持基于代价优化(下文细讲),此处只介绍基于规则的优化策略,基于规则的优化策略实际上就是对语法树进行一次遍历,模式匹配能够满足特定规则的节点,再进行相应的等价转换。因此,基于规则优化说到底就是一棵树等价地转换为另一棵树。SQL中经典的优化规则有很多,下文结合示例介绍三种比较常见的规则:谓词下推(Predicate Pushdown)、常量累加(Constant Folding)和列值裁剪(Column Pruning)。

                                                                                             

上图左边是经过Analyzer解析后的语法树,语法树中两个表先做join,之后再使用age>10对结果进行过滤。大家知道join算子通常是一个非常耗时的算子,耗时多少一般取决于参与join的两个表的大小,如果能够减少参与join两表的大小,就可以大大降低join算子所需时间。谓词下推就是这样一种功能,它会将过滤操作下推到join之前进行,上图中过滤条件age>0以及id!=null两个条件就分别下推到了join之前。这样,系统在扫描数据的时候就对数据进行了过滤,参与join的数据量将会得到显著的减少,join耗时必然也会降低。

                                                                                  

 

常量累加其实很简单,就是上文中提到的规则  x+(1+2)  -> x+3,虽然是一个很小的改动,但是意义巨大。示例如果没有进行优化的话,每一条结果都需要执行一次100+80的操作,然后再与变量math_score以及english_score相加,而优化后就不需要再执行100+80操作。

                                                                             

列值裁剪是另一个经典的规则,示例中对于people表来说,并不需要扫描它的所有列值,而只需要列值id,所以在扫描people之后需要将其他列进行裁剪,只留下列id。这个优化一方面大幅度减少了网络、内存数据量消耗,另一方面对于列存数据库(Parquet)来说大大提高了扫描效率。

至此,逻辑执行计划已经得到了比较完善的优化,然而,逻辑执行计划依然没办法真正执行,他们只是逻辑上可行,实际上Spark并不知道如何去执行这个东西。比如Join只是一个抽象概念,代表两个表根据相同的id进行合并,然而具体怎么实现这个合并,逻辑执行计划并没有说明。

2.1.2 物理计划

在成功创建优化的逻辑计划后,Spark开始执行物理计划流程。物理计划通过生成不同的物理执行策略,并通过代价模型进行比较分析,从而指定如何在集群上执行逻辑计划,例如,一个join操作会涉及到代价比较,它通过分析数据表的物理属性(表的大小或分区的大小),选择合适的物理执行计划。

                                                      

使用queryExecution方法查看逻辑执行计划,使用explain方法查看物理执行计划,分别如下所示:

2.2 基本的结构化操作

创建DataFrame

spark.read.format("json").load("/path")

从支持的文件类型中加载DF。

Seq(  ( "hello", 2 , 3L ) ,  ( ... ... ) ).toDF("c1" , "c2" , "c3" )

在Seq类型上调用toDF函数,利用Spark的隐式方法转化为DF。注意:null不能隐式转换。

列表表达式

 df.col( " col name " ),
  col( " col name " ),
  column( " col name " ),
  `col name,
  $"col name",
  expr( " col name " )

不能将col与字符串混淆:

select ( col(" name1 "), " name2 " )

以上代码是错误的。

添加列

df.withColumn("numberOne",lit(1)).show(2),前面是添加的列名,后面是列值

// In SQL
select * ,1 as One from dfTable limit 2

//In scala
import org.apache.spark.sql.funcions.lit
df.select($"*",lit(1).as("One")).show(2)

//Or in scala
df.withColumn("numberOne",lit(1)).show(2)

lit表示spark的字面量,它其实就是Column类型。

重命名列

df.withColumnRenamed("DEST_COUNTRY_NAME","dest"),前面是需要修改的列,后面是新的列名。

scala> df.withColumnRenamed("DEST_COUNTRY_NAME","dest").columns
res2: Array[String] = Array(dest, ORIGIN_COUNTRY_NAME, count)

scala> df.columns
res3: Array[String] = Array(DEST_COUNTRY_NAME, ORIGIN_COUNTRY_NAME, count)

保留字与关键字

当列名包含空格或者一些保留字时,我们可以显示的用`col name`来表示一个字符串

df.withColumn("This Long Col Name",$"ORIGIN_COUNTRY_NAME").select($"this Long Col Name",expr("`This Long Col Name` as `new col`")).show(2)

// in SQL
select `This Long Col Name`,`This Long Col Name` as `new col` from dfTable limit 2

如上,名字有空格时,需要用``来表示成字符串。

区分大小写

spark默认是不区分大小写的,如$"COUNT"与$"count"是等价的,但是可以通过以下配置使Spark区分大小写:

//in SQL
set spark.sql.caseSensitive true

删除列

df.drop("col name")

scala> df.drop("COUNT").columns
res6: Array[String] = Array(DEST_COUNTRY_NAME, ORIGIN_COUNTRY_NAME)

修改列的类型

scala> df.withColumn("count2",$"count".cast("Int")).printSchema
root
 |-- DEST_COUNTRY_NAME: string (nullable = true)
 |-- ORIGIN_COUNTRY_NAME: string (nullable = true)
 |-- count: long (nullable = true)
 |-- count2: integer (nullable = true)


// in sql
scala> spark.sql("select *,cast(count as int) as count2 from dfTable").printSchema
root
 |-- DEST_COUNTRY_NAME: string (nullable = true)
 |-- ORIGIN_COUNTRY_NAME: string (nullable = true)
 |-- count: long (nullable = true)
 |-- count2: integer (nullable = true)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值