数据分区
为了让多个执行器并行地工作,Spark将数据分解成多个数据块,每个数据块叫做一个分区。
分区是位于集群中的一台物理机上的多行数据的集合,
DataFrame的分区也说明了在执行过程中数据在集群中的物理分布。
如果只有一个分区,即使拥有数千个执行器,Spark也只有一个执行器在处理数据。
类似地,如果有多个分区,但只有一个执行器,那么Spark仍然只有一个执行器在处理数据,
就是因为只有一个计算资源单位。

当使用DataFrame时,(大部分时候)你不需要手动操作分区,只需指定数据的高级转换操作,
然后Spark决定此工作如何在集群上执行。

通过调用explain()函数可以观察到Spark创建一个执行计划,
并且可以看到这个计划将会怎样在集群上执行,
调用某个DataFrame的explain操作会显示DataFrame的来源(即:Spark是如何执行查询操作的)

默认情况下,shuffle操作会输出200个shuffle分区,我们可以设置减少shuffle输出分区的数量。
例如:spark.conf.set("spark.sql.shuffle.partitions","5")

DataFrame 和 SQL
不管使用什么语言,Spark以完全相同的方式执行转换操作。
使用Spark SQL,你可以将任何DataFrame注册为数据表或视图(临时表),并使用SQL对它进行查询。
编写SQL查询或编写DataFrame代码并不会造成性能差异,它们都会被"编译"成相同的底层执行计划。

Spark会针对物理执行计划做一系列优化,
这个执行计划是一个有向无环图(DAG)的转换,每个转换产生一个新的不可变的DataFrame,
我们可以在这个DataFrame上调用一个动作操作来产生一个结果。

Spark可以通过内置的命令行工具spark-submit轻松地将测试级别的交互式程序转化为生产级别的应用程序。
spark-submit将你的应用程序代码发送到一个集群并在那里执行,
应用程序将一直运行,直到它(完成任务)正确退出或遇到错误。

 

Spark是一个分布式编程模型,用户可以在其中指定转换操作,
通过多个转换构成一个指令的有向无环图(DAG)。
指令图的执行过程作为一个作业(Job)由一个动作操作触发,
在执行过程中一个作业(Job)被分解为多个阶段(Stage)和任务(Task)在集群上执行。

Spark内部使用一个名为:Catalyst的引擎,
在计划制定和执行作业的过程中使用Catalyst来维护它自己的类型信息,
这样就会带来很大的优化空间,这些优化可以显著提高性能。

DataFrame就是一些Row类型的Dataset的集合(DataSet[Row])。
"Row"类型是Spark用于支持内存计算而优化的数据格式。
这种格式有利于高效计算,因为它避免使用会带来昂贵垃圾回收开销和对象实例化开销的JVM类型,
而是基于自己的内部格式运行,所以并不会产生这种开销。

使用DataFrame就会大大受益于这种优化过的Spark内部格式。

列:表示一个简单类型(如:string)
行:一行对应一个数据记录。DataFrame中的每条记录都必须是Row类型。

LongType = Long
DateType = java.sql.Date
Timestamp = java.sql.Timestamp
StructType = org.apache.spark.sql.Row

用户代码到执行代码的过程
1、编写DataFrame/DataSet/SQL代码。
2、Spark将它转换为一个"逻辑执行计划"(Logical Plan)。
3、Spark将此逻辑计划转化为一个"物理执行计划"(Physical Plan),检查可行的优化策略,并在此过程中检查优化。
4、Spark在集群上执行该物理执行计划(RDD操作)。

逻辑计划
执行的第一阶段旨在获取用户代码并将其转换为逻辑计划。
这个逻辑计划仅代表一组抽象转换,并不涉及执行器或驱动器,
它只是将用户的表达式集合转换为最优的版本。
它通过将用户代码转换为未解析的逻辑计划来实现这一点。
Spark使用Catalyst(所有表和DataFrame信息的存储库)在分析器中解析列和表格。
如果分析器可以解析它,结果将通过Catalyst优化器,Catalyst优化器尝试通过下推谓语或选择操作来优化逻辑计划。

 

物理计划
在成功创建优化逻辑计划后,Spark开始执行物理计划流程。
物理计划(通过称为Spark计划)通过生成不同的物理执行策略,并通过代价模型进行比较分析,
从而指定如何在集群上执行逻辑计划。

物理执行计划产生一系列的RDD和转换操作。
这就是Spark被称为编译器的原因,
因为它将对DataFrame、Dataset、SQL中的查询操作为你编译一系列RDD的转换操作。

 

执行
在选择一个物理计划时,Spark将所有代码运行在Spark的底层编程接口RDD上。
Spark在运行时执行进一步优化,生成可以在执行期间优化任务或阶段的本地Java字节码,最终将结果返回给用户。

 

在Spark中,DataFrame的每一行都是一个记录,记录是Row类型的对象。
Spark使用列表达式操纵Row类型对象,Row对象内部其实是字节数组(byte[])。

import org.apache.spark.sql.Row
import org.apache.spark.sql.types.{StructType, StructField}

val schema = new StructField(Array(
new StructField("order_id", StringType, true),
new StructField("store_id", IntegerType, true)
))

 

重划分和合并
另一个重要的优化是根据一些经常过滤的列对数据进行分区,
控制跨集群数据的物理布局,包括分区方案和分区数。

df.repartition(4)

如果你知道你经常按某一列(channel)执行过滤操作,则根据该列进行重新分区是很有必要的
df.repartition(col("channel"))

还可以指定你想要的分区数量
df.repartition(4, col("channel"))

合并操作(coalesce)不会导致数据的全面清洗(shuffle),但会尝试合并分区。

基于"channel"的列将数据重新划分成4个分区,然后再合并它们(没有导致数据全面清洗)
df.repartition(4, col("channel")).coalesce(2)

 

通信策略
在连接过程中,Spark以两种不同的方式处理集群通信问题。
它要么执行导致"all-to-all"通信的 shuffle join,
要么就采用 broadcast join。

 

大表连接大表
当一个大表连接另一个大表时,最终就是个 shuffle join。
执行shuffle join 则每个节点都与所有其他节点进行通信,并根据哪个节点具有某个键或某一组键来共享数据。
由于网络会因通信量而阻塞,所以这种方法很耗时,特别是如果数据没有合理分区的情况下。
如果没有合理进行数据分区,所有工作节点(以及潜在的每个分区)在整个连接过程中都需要互相通信。

 

大表与小表连接
当表的大小足够小,以便能够放入单节点内存中且还有空闲空间的时候,我们就可以优化连接。
使用"broadcast join"通常更高效。也就是说,我们可以把数据量较小的DataFrame复制到集群中的所有工作节点上。
虽然听起来很耗时,但这样做会避免在整个连接过程中执行"all-to-all"的通信,
只需在开始时执行一次,然后让每个工作节点独立执行作业,而无需等待其他工作节点,也无需与其他工作节点通信。

这种连接通信模式在开始时会有一次大的通信,就像大表之间连接时一样。
但是,在第一次通信后,节点之间将不再有其他的通信。
这意味着,连接操作将在每个节点上独立执行,而最大限度的利用CPU资源专注于执行连接操作。

SQL接口支持显式指定连接操作的物理通信模式,但这些并不是强制执行的,所以优化程序可能会选择忽略它们。
可以使用特殊的注释语法来设置一个提示,
mapjoin、broadcast 和 broadcast join 都是指示同样广播连接。

select /*+ mapjoin(t_deparment) */ *
from wm_order_line
join t_deparment on wm_order_line.dep_id=t_deparment.dep_id

这也不是毫无开销的,如果你试图广播很大的表,可能导致驱动器节点崩溃。
因为广播大表的代价是昂贵的。

如果在连接之前正确划分数据,则可以更高效的执行连接操作,
因为即使选择的是shuffle join,如果来自不同的DataFrame的数据已经位于同一台机器,Spark可以避免Shuffle操作。

两个执行连接操作的数据集在不同顺序时,也会影响性能,
因为连接操作的在前面或后面的数据集往往起到过滤器作用,过滤器的大小会决定网络传输的通信开销。

JSON文件
每行必须包含一个单独的、独立的有效JSON对象
换行符分割JSON对象还是一个对象可以跨越多行,这个可以由"multiLine"选项控制,
当"multiLine"为true时,则可以将整个文件作为一个json对象读取,并且Spark将其解析为DataFrame。
换行符分割的JSON实际上是一种更稳定的格式,因为它可以在文件末尾追加新记录,而不是必须读入整个文件然后再写出。

 

Parquet文件
Parquet是一种面向列的数据存储格式,它提供了各种存储优化,尤其适合数据分析。
Parquet提供列压缩从而可以节省空间,而且它支持按列读取而非整个文件地读取。

 

ORC文件
它针对大型流式数据读取进行优化。

Parquet和ORC有什么区别?
在大多数情况下,它们非常相似,本质区别是:
Parquet针对Spark进行了优化,
ORC则是针对Hive进行了优化。

高级I/O概念
我们可以通过在写入之前控制"数据分片"来控制写入文件的"并行度",
还可以通过控制数据分桶(bucketing)和数据划分(partitions)来控制特定的数据布局方式。

 

可分割的文件类型和压缩
某些文件格式是"可分割的",因此Spark可以只获取该文件中的满足查询条件的某一部分,无需读取整个文件,从而提高读取效率。
此外,假设你使用的是Hadoop HDFS,则如果该文件包含多个文件块,分割文件则可进一步优化提高性能。
与此同时需要进行压缩管理,并非所有的压缩格式都是可分割的。

 

并行读数据
多个执行器不能同时读取同一个文件,但可以同时读取不同的文件。
通常这意味着,当你从包含多个文件的文件夹中读取时,每个文件都将视为DataFrame的一个分片,
并由执行器并行读取,多余的文件会进入该读取队列等候。

 

并行写数据
写数据涉及的文件数量取决于DataFrame的分区数。
默认情况是每个数据分片都会有一定的数据写入,
这意味着虽然我们指定的是一个"文件",但实际上它是由一个文件夹中的多个文件组成,每个文件对应着一个数据分区。

 

数据划分
数据划分工具支持你在写入数据时控制存储数据以及存储数据的位置。
将文件写出时,你可以将列编码文件夹,
这使得你在之后读取时可跳过大量数据,只读入与问题相关的列数据而不必扫描整个数据集。

数据分桶
数据分桶是另一种文件组织方法,你可以使用该方法控制写入每个文件的数据。
具有相同桶ID(哈希分桶的ID)的数据将放置到一个物理分区中,这样就可以避免在稍后读取数据时进行shuffle(清洗)。
根据你之后希望如何使用该数据来对数据进行预分区,就可以避免连接或聚合操作时执行代价很大的shuffle操作。

与其根据某列进行数据划分(partitions),不如考虑对数据进行分桶(bucketing),
因为某列如果存在很多不同的值,就可能写出一大堆目录。
这将创建一定数量的文件,数据也可以按照要求组织起来放置到这些"桶"中。

df.bucketBy(5,"order_id").SaveAsTable("wm_order_line")

管理文件大小
当你写入大量的小文件时,由于管理所有的这些小文件而产生很大的元数据开销。
许多文件系统(如:HDFS)都不能很好地处理大量的小文件,
而Spark特别不适合处理小文件。

Spark2.2中引入了更自动化地控制文件大小的新方法。
现在可以利用一个工具来限制输出文件大小,从而可以选出最优的文件大小。
可以使用maxRecordsPerFile选项来指定每个文件的最大记录数,
这使得你可以控制写入每个文件的记录数来控制文件大小。
例如:df.write.option("maxRecordsPerFile",5000),
Spark将确保每个文件最多包含5000条记录。

 

Spark SQL
使用Spark SQL,你可以对存储到数据库中的表或视图进行SQL查询。
你可以使用SQL和DataFrame表示数据操作,它们都会编译成相同的低级代码。

Spark2.0发布了一个支持Hive操作的超集,并提供了一个能够同时支持ANSI-SQL和HiveQL的原生SQL解析器。

 

Catalog
Spark SQL中最高级别的抽象是Catalog。
用于存储用户数据中的元数据以及其他有用的东西,
如:数据库、数据表,函数和视图。
它在 org.apache.spark.sql.catalog.Catalog包中。

刷新表元数据
维护表的元数据确保从最新的数据集读取数据。
有两个命令可刷新表的元数据。
1、 refresh table 用来刷新与表关联的所有缓存项(实质上是文件),如果之前缓存了该表,则在下次扫描时会惰性缓存它。
refresh table xxx

2、repair table,它刷新该表在Catalog中维护的分区,此命令重点是收集新的分区信息。
例如,可以手动写出新分区,并相应地修复表:
msck repair table xxx

 

当调用一个DataFrame的转换操作时,实际上等价于一组RDD的转换操作。
无论是DataFrame还是Dataset,运行的所有Spark代码都将编译成一个RDD。
RDD是一个只读不可变的且已分块的记录集合。

检查点
DataFrame API中没有检查点(checkpoint)这个概念。
检查点是将RDD保存到磁盘上的操作,
以便将来对此RDD的引用能直接访问磁盘上的那些中间结果,而不需要从其源头重新计算RDD。
它与缓存类似,只是它不存储在内存中,只存储在磁盘上。

 

分布式共享变量--广播变量
广播变量允许你在所有工作节点上保存一个共享值,以便在Spark的各种操作中重用这个值时,就不需要将其重新在机器间传输。
广播变量是共享的、不可修改的变量,它们缓存在集群中的每个节点上,而不是在每个任务中都反复序列化。

val spark = SparkSession.builder().config("spark.sql.warehouse.dir","/user/hive/warehouse").getOrCreate()

 

一次shuffle操作意味着一次对数据的物理重分区
Spark在每次shuffle之后开始一个新阶段,并按照顺序执行各阶段以计算最终结果。

避免Shuffle操作,避免跨节点移动数据。

有多少分区就有多少个任务并行执行。