文章目录
RDD
RDD(Resilient Distributed Dataset,弹性分布式数据集)是Spark中的基本抽象。RDD代表一种可并行操作的不可变的分区元素集合,它有3个特性:
- RDD是不可变的
- RDD是分区的
- RDD是可以并行操作的
1. 不可变性
RDD是不可变的,只能在其他的RDD上通过Transformation算子(map、filter等)转换得到。RDD创建方式:
-
通过Hadoop文件系统中的文件(HDFS)创建
val rdd = sparkContext.textFile("hdfs://master-1:9000/file/test.txt", 3)
-
通过在已存在的Scala集合上创建
val rdd = sparkContext.parallelize(Seq(1, 2, 3, 4, 5), 4)
-
通过在其他RDD转换得到
val rdd = ... val rdd1 = rdd.map(m => m + 1)
我们知道Spark是借鉴参考了MapReduce的理念思想转化而来的。在MapReduce中,每一步的计算结果都要保存到磁盘上,在计算下一步的时候又要从磁盘中将上一步保存的结果读出来,这样反复的磁盘I/O读写,再加上磁盘读写速度低效,从而就导致了MapReduce的计算性能低下。
而Spark正是使用RDD的抽象加上内存计算,使得计算效率得到了大幅度的提升。Spark通过RDD之间的依赖关系构建有向无环图,只有在遇到Action算子的时候,才会真正触发计算,并且将计算结果优先存储在内存中,从而避免了MapReduce中哪些低效的操作。
2. 分区性
分区性是指RDD这种抽象数据集中的数据是分布在集群中不同的节点上的,正是有了分区的特性,才使得RDD可以被并行地操作。
比如下面代码中,通过第二个参数,我们创建了一个具有4个分区的RDD:
val rdd = sparkContext.parallelize(Seq(1, 2, 3, 4, 5), 4)
3. 并行操作
基于RDD的分区个数,Spark便可以创建对应分区个数的task来进行并行计算(Spark数据集每个分区对应一个task来处理)。
4. RDD内部结构
RDD有五个主要的属性:
- RDD分区列表
- 计算每个分片数据的函数(iterator()函数和compute()函数)
- 对其他RDD依赖关系
- 如果是key-value型的RDD,还会有一个Partitioner分区器(比如,HashPartitioner和RangePartitioner)
- 要计算的分片数据的首选位置(例如,HDFS文件块的位置)
5. RDD宽依赖、窄依赖
Spark是通过RDD的依赖关系来进行失败恢复的。
1. 窄依赖
下图中每个大的圆角矩形代表一个RDD,蓝色的小的圆角矩形代表RDD的分区。
在窄依赖中,子RDD中最多有一个partition依赖其父RDD的一个或多个partition。例如:map,flatMap,filter,mapPartitions,union等算子。
2. 宽依赖
在宽依赖中,子RDD中有多个partition依赖其父RDD的一个partition。例如:cogroup,join,groupyByKey,reduceByKey,combineByKey,distinct,repartition等算子。
宽窄依赖也是Spark划分Stage的标准。另外窄依赖是允许流水线式执行的,因为窄依赖中,子RDD中的分区和父RDD中的分区是一一对应的。
6. RDD的重用
对于被使用多次的RDD进行内存缓存,减少重复计算,提升计算效率。Spark RDD一共有7种缓存等级:
- MEMORY_ONLY:将RDD作为反序列的Java对象存储在JVM中,如果RDD 分区不能全部存储到内存中,那么某些分区将不能被缓存,这些分区每次在用到的时候都会被重新计算。
- MEMORY_AND_DISK:将RDD作为反序列的Java对象存储在JVM中,如果RDD 分区不能全部存储到内存中,那么某些分区将被存储到磁盘,这些分区在用到的时候会从磁盘读取。
- MEMORY_ONLY_SER:将RDD作为序列化的Java对象(每个分区为一个字节数组)进行存储。通常情况下,这种方式比存储反序列化的Java对象会更节省空间,特别是在使用更加高效的序列化器的时候。但是这种方式在读取数据的时候由于要将数据反序列化为Java对象,会使用较多的CPU资源。
- MEMORY_AND_DISK_SER:和MEMORY_ONLY_SER相似,只是将不能存在内存的分区保存到磁盘。
- DISK_ONLY:将RDD分区全都存储到磁盘。
- MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc:跟以上存储等级类似,只是将每个分区存储两份。
- OFF_HEAP:与MEMORY_ONLY_SER类似,只不过是将数据缓存在非堆内存中。这种方式的前提是,非堆内存off-memory必须被启用。
RDD重用代码示例:
val rdd = spark.sparkContext.parallelize(Seq(1, 2, ..., 4, 100000000), 4)
.persist(StorageLevel.MEMORY_AND_DISK)
val rdd1 = rdd.map(m => m + 1)
val rdd2 = rdd.filter(f => f % 2 == 0)
Dataset
Dataset在很多方面和RDD都是类似的,它是特定领域的强类型集合,可以使用函数或者关系型操作对其进行并行处理。我们可以认为RDD和Dataset中都是一行行的数据,但是RDD中每一个行都是一个对象,内部结构是无法被Spark知晓的。而Dataset就像是一张数据库中的表,Spark不仅知道Dataset中每行数据的情况,也知道每行数据中每一列的数据类型,正是基于这种结构,在查询Dataset中的数据时,Spark SQL才能通过查询优化器取出需要查询的列,而不是每次都把整行记录都读出来。
Dataset上的操作也是分为transformation操作(map、filter、select等)和action操作(count、show等)。同样的,Dataset也是lazy的,只有在遇到action操作的时候才会触发计算。当action操作被调用的时候,Spark的查询优化器会优化逻辑计划,并生成物理计划进行高效的执行。
1. Encoder
由于Dataset是一个强类型的数据集,那么在生成Dataset的时候,为了高效的支持特定领域的对象,必须要使用Encoder(org.apache.spark.sql.Encoder)。Encoder的作用就是将JVM对象类型转化成Spark SQL的内部类型,或者是将Spark SQL的内部类型转成JVM对象类型。例如,类Person有两个字段name(string)和age(int),encode的作用就是告知Spark在运行时生成代码将Person对象序列化成二进制结构,这种二进制结构通常具有更低的内存査勇,并且对数据处理的效率进行了优化。
Encoder的使用,其实就是导入隐式转换:
import spark.implicits._
val ds = Seq(1, 2, 3).toDS() //需要先导入隐式转换,编译才能通过。这里隐式类型其实就是spark.implicits.newIntEncoder
2. Dataset的创建
-
通过读取文件系统中的文件来创建
//spark是一个SparkSession实例 import spark.implicits._ val people = spark.read.parquet("...").as[Person] case class Person(name: String, age: String)
-
通过在已存在的Dataset上转换而来
val names = people.map(_.name)
-
通过Scala集合创建
import spark.implicits._ val ds: Dataset[Int] = Seq(1, 2, 3, 4, 5).toDS()
DataFrame
每个Dataset都有一个无类型的视图,被称为DataFrame,它其实就是一个Row类型的Dataset(Dataset[Row])。DataFrame同样可以认为是一张表,DataFrame并不存储每行(Row)中每一列的类型,那么在编译期就不能像Dataset那样可以检查每一列的类型信息,只能在运行时通过解析才能获取每一列的类型。
Dataset和DataFrame都可以像操作数据库表那样进行SQL查询。
RDD、Dataset和DataFrame三者区别
- 三者都是分区、不可变并能进行并行操作的数据集
- Dataset和DataFrame内部都是结构化的数据,而RDD是无结构的
- RDD和Dataset都是在编译期检查类型、语法错误,而DataFrame是在运行时解析之后检查
- 在对Dataset和DataFrame进行操作时,Spark会利用Spark SQL中的查询优化器进行优化,而Spark并不会对RDD进行优化