RDD DataFrame DataSet
https://databricks.com/wp-content/uploads/2016/06/Unified-Apache-Spark-2.0-API-1.png
https://www.infoq.cn/article/three-apache-spark-apis-rdds-dataframes-and-datasets
https://databricks.com/blog/2016/07/14/a-tale-of-three-apache-spark-apis-rdds-dataframes-and-datasets.html
http://dblab.xmu.edu.cn/blog/985-2/#more-985
RDD
- 本质: Immutable的弹性可伸缩分布式对象集合(Resilient Distributed DataSet),可以分成多个区,每个分区为一个数据集片段,不同分区可以保存在集群不同节点上。Spark 计算操作的基本数据结构。
- 两类操作:
- 转换 Transformation: RDD -> RDD 惰性操作,延迟计算,粗粒度操作,反映RDD之间的相互依赖关系
- map, flatMap, filter, groupByKey, union, join, …
- 行动 Action:RDD -> 输出结果, 立即计算
- reduce, count, collect, foreach, saveAs…
- 转换 Transformation: RDD -> RDD 惰性操作,延迟计算,粗粒度操作,反映RDD之间的相互依赖关系
- 创建方式:
读入外部数据源, sparkcontext parallelize方法,RDD -> RDD, DataFrame、Dataset -> RDD
- 读入外部数据源
- json, csv, hive, jdbc, odbc, mongo-spark-connector…
- 通过并行集合(数组)sparkcontext parallelize 创建RDD
- RDD -> RDD
- DataFrame/Dataset转RDD:
scala val rdd1=testDF.rdd val rdd2=testDS.rdd
```scala
// I 读取外部源
scala> val linesRDD = sc.textFile("hdfs://host:9000/test/word.txt")
// scala> val linesRDD = sc.textFile("/user/hadoop/word.txt")
// II sparkcontext(sc) parallelize 方法创建
scala>val list = List(1,2,3,4,5)
list: List[Int] = List(1, 2, 3, 4, 5)
scala>val rdd = sc.parallelize(list) //rdd: org.apache.spark.rdd.RDD[Int]
// Again
scala> val list = List("Hadoop","Spark","Hive","Spark") //list: List[String] = List(Hadoop, Spark, Hive, Spark)
scala> val rdd = sc.parallelize(list) //rdd: org.apache.spark.rdd.RDD[String]
// pairRDD
scala> val pairRDD = rdd.map(word => (word,1)) //pairRDD: org.apache.spark.rdd.RDD[(String, Int)]
scala> pairRDD.foreach(print) // (Hadoop,1)(Spark,1)(Hive,1)(Spark,1)
scala> pairRDD.reduceByKey((a,b)=>a+b).foreach(print) //(Spark,2)(Hive,1)(Hadoop,1)
scala> pairRDD.groupByKey().foreach(print) //(Spark,CompactBuffer(1, 1))(Hive,CompactBuffer(1))(Hadoop,CompactBuffer(1))
scala> pairRDD.keys
scala> pairRDD.values
scala> pairRDD.sortByKey()
scala> pairRDD.mapValues(x => x+1)
```
- 注意:
1. 如果使用了本地文件系统的路径,那么,必须要保证在所有的worker节点上,也都能够采用相同的路径访问到该文件,比如,可以把该文件拷贝到每个worker节点上,或者也可以使用网络挂载共享文件系统。
2. textFile()方法的输入参数,可以是文件名,也可以是目录,也可以是压缩文件等。比如,textFile(“/my/directory”), textFile(“/my/directory/*.txt”), and textFile(“/my/directory/*.gz”).
3. textFile()方法也可以接受第2个输入参数(可选),用来指定分区的数目。默认情况下,Spark会为HDFS的每个block创建一个分区(HDFS中每个block默认是128MB)。你也可以提供一个比block数量更大的值作为分区数目,但是,你不能提供一个小于block数量的值作为分区数目。
-
DAG(有向无环图):RDD 转换和行动等操作构成的依赖关系图
- Job 作业,DAG解析时遇到Action操作即分隔为job.
- Stage 阶段或任务的集合, 1 job = n stage, 1 stage = m tasks.
- DAG中进行反向解析,遇到宽依赖就断开,切分成stage,遇到窄依赖就把当前的RDD加入到当前的stage中;将窄依赖尽量划分在同一个stage中,可以实现流水线计算.
- Task 任务
- 共享变量
- 广播变量(只读): 阶段内任务见共享的公共数据,自动广播;跨越多个阶段的任务见共享数据, 手动广播, 在Executor中只保留一份;
scala> val broadcastVar = sc.broadcast(Array(1, 2, 3)) // broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0) scala> broadcastVar.value // res0: Array[Int] = Array(1, 2, 3)
- 累加器: val accum = SparkContext.longAccumulator()或者SparkContext.doubleAccumulator()来创建,任务只能对其做累加,只有任务控制节点(Driver Program)可以使用value方法来读取累加器的值.
- 宽窄依赖
- 父RDD每一个分区只对应到子RDD的一个分区,为窄依赖(1to1, Mto1),否则宽依赖(1toN)。窄依赖适合分区间的并行计算。
-
特点:
- 数据只读,不可修改,如果需要修改数据,必须从父RDD转换到子RDD,由此在不同RDD之间建立了血缘关系。
- 高效的容错和可用性:传统集群部分节点间采用数据复制等冗余容错,开销大。RDD通过父子依赖关系(粗粒度的转换操作)重新计算得到丢失的分区来实现容错,可并行进行。
- 中间结果持久化到内存。
- 存放的数据可以是Java对象,避免了不必要的对象序列化和反序列化开销。
-
示例
[root@localhost ~]# spark-shell Spark context Web UI available at http://host:4040 Spark context available as 'sc' (master = spark://host:7077, app id = app-20190123110129-0006). Spark session available as 'spark'. Welcome to ____ __ / __/__ ___ _____/ /__ _\ \/ _ \/ _ `/ __/ '_/ /___/ .__/\_,_/_/ /_/\_\ version 2.4.0 /_/ Using Scala version 2.11.12 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_191) Type in expressions to have them evaluated. Type :help for more information. scala> val textFile = spark.read.textFile("hdfs://host:9000/test/SparkReadMe.md") textFile: org.apache.spark.sql.Dataset[String] = [value: string] // tansformation 算子 scala> val linesWithSpark = textFile.filter(line => line.contains("Spark")) linesWithSpark: org.apache.spark.sql.Dataset[String] = [value: string] // Action 算子 scala> linesWithSpark.count() res0: Long = 20
DataFrame
DataFrame = RDD + Schema
- 和RDD的区别。
- RDD是分布式的 Java对象的集合,比如,RDD[Person]是以Person为类型参数,但是,Person类的内部结构对于RDD而言却是不可知的。
- DataFrame是一种以RDD为基础的分布式数据集,也就是分布式的Row对象的集合(每个Row对象代表一行记录, 所有行的数据类型等Schema相同),提供了详细的结构信息,也就是我们经常说的模式(schema),Spark SQL可以清楚地知道该数据集中包含哪些列、每列的名称和类型。
DataFrame创建
外部数据源, RDD -> DataFrame, Dataset -> DataFrame
-
读入外部数据源
- json, csv, hive, jdbc, odbc, mongo-spark-connector…
- Json Schema 自动推断:
JSON 是一种可读性良好的重要结构化数据格式,许多原始数据往往以JSON的形式存在。然而JSON数据的体积却过于庞大,不利于批量数据分析。因此一个常见的数据处理步骤就是将JSON转换为ORC、Parquet等高效的列式存储格式。然而,不同版本的JSON数据往往具有不同的schema(例如新版本的Twitter API返回的数据可能比老版本的API返回的数据多出若干列)。人工合并整个JSON数据集所有记录的schema是一件十分枯燥繁琐的任务。Spark SQL在处理JSON数据时可以自动扫描整个数据集,得到所有记录中出现的数据列的全集,推导出完整的schema。(对于同名但不同类型的列,Spark SQL会尝试规约出一个公共类型。)
示例:
import spark.implicits._ val spark=SparkSession.builder().getOrCreate() // support flat, non-flat, mixed, sparse type json val path = "src/main/resources/data/some.json" // {"name":"Michael"} // {"name":"Andy", "age":30} // {"name":"Justin", "age":19} val peopleDF = spark.read.format("json").option("inferSchema", "true").option("multiLine", true).load(path) // OR Equivalent to // val people = spark.read.json(path) // DF peopleDF.printSchema() peopleDF.select("name").show() peopleDF.select($"name", $"age" + 1).show() peopleDF.filter($"age" > 21).show() peopleDF.groupBy("age").count().show() peopleDF.createOrReplaceTempView("people") // sparkSQL spark.sql("SELECT * FROM people").show()
上图展示了Spark SQL对三条不规整的个人信息JSON记录进行整理和schema推导的过程。第2条记录跟第1条记录类似,但多出了一个age字段,第3条与前两条也很类似,但是身高字段的类型是double而不是int。对此,Spark SQL的JSON数据源作出的处理是,将出现的所有列都纳入最终的schema中,对于名称相同但类型不同的列,取所有类型的公共父类型(例如int和 double的公共父类型为double)。通过这样的处理,我们最终就得到了右下方的DataFrame。 -
RDD -> DataFrame
-
反射推断
- 示例
import spark.implicits._ // case class for pattern match case class Person(name: String, age: Long) // people.txt // Michael, 29 // Andy, 30 // Justin, 19 // Create an RDD of Person objects from a text file, convert it to a Dataframe val peopleRDD = spark.sparkContext .textFile("examples/src/main/resources/people.txt") .map(_.split(",")) .map(attributes => Person(attributes(0), attributes(1).trim.toInt)) // inference from peopleRDD 通过反射将类属性映射为DataFrame的Schema val peopleDF = peopleRDD.toDF() // people.toDS() to dataset
如果,RDD[T] 中 T 的类型不是 case class, 需要手动指明 DataFrame 的列名称:
import spark.implicits._ val rdd = spark.sparkContext .textFile("examples/src/main/resources/people.txt") .map(_.split(",")) val peopleRDD = rdd.map {line=> (line._1,line._2.trim.toInt)} val peopleDF = peopleRDD.toDF("name","age")
-
手动构造Schema, 然后应用于已知的的RDD上: RDD->rowRDD + strucType(StructFileds)->df
- 示例
import spark.implicits._ // $example on:programmatic_schema$ // Create an RDD val peopleRDD = spark.sparkContext.textFile("examples/src/main/resources/people.txt") // The schema is encoded in a string val schemaString = "name age" // Generate the schema based on the string of schema val fields = schemaString.split(" ") .map(fieldName => StructField(fieldName, StringType, nullable = true)) val schema = StructType(fields) // Convert records of the RDD (people) to Rows val rowRDD = peopleRDD .map(_.split(",")) .map(attributes => Row(attributes(0), attributes(1).trim.toString)) // Apply the schema to the RDD val peopleDF = spark.createDataFrame(rowRDD, schema)
-
Dataset -> DataFrame
把case class封装成Row
import spark.implicits._ val testDF = testDS.toDF
-
Dataset
- 结合了RDD DataFrame 的优点,类型安全(type-safe),面向对象(object-oriented)。
- DataFrame 其实是 DataSet[Row]
- 支持结构化和非结构化的数据
- 因为序列化是通过Tungsten进行的,它使用了off heap数据序列化,不需要垃圾回收器来摧毁对象
Dataset 创建
基本的数据类型转换, RDD -> Dataset, DataFrame -> Dataset
- 基本的数据类型转换, Encoders
import spark.implicits._
// $example on:create_ds$
// Encoders are created for case classes
val caseClassDS = Seq(Person("Andy", 32)).toDS()
caseClassDS.show()
// +----+---+
// |name|age|
// +----+---+
// |Andy| 32|
// +----+---+
// Encoders for most common types are automatically provided by importing spark.implicits._
val primitiveDS = Seq(1, 2, 3).toDS()
primitiveDS.map(_ + 1).collect() // Returns: Array(2, 3, 4)
- RDD[T : case class] -> Dataset
val peopleDS = spark.sparkContext
.textFile("examples/src/main/resources/people.txt")
.map(_.split(","))
.map(attributes => Person(attributes(0), attributes(1).trim.toInt)).toDS()
- DataFrame -> Dataset
// DataFrames can be converted to a Dataset by providing a class. Mapping will be done by name
val path = "examples/src/main/resources/people.json"
val peopleDS = spark.read.json(path).as[Person]
peopleDS.show()
// +----+-------+
// | age| name|
// +----+-------+
// |null|Michael|
// | 30| Andy|
// | 19| Justin|
// +----+-------+
转化:
RDD、DataFrame、Dataset三者有许多共性,有各自适用的场景常常需要在三者之间转换
- DataFrame/Dataset转RDD:
这个转换很简单
val rdd1=testDF.rdd
val rdd2=testDS.rdd
- RDD转DataFrame:
import spark.implicits._
val testDF = rdd.map {line=>
(line._1,line._2)
}.toDF("col1","col2")
一般用元组把一行的数据写在一起,然后在toDF中指定字段名
- RDD转Dataset:
import spark.implicits._
case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型
val testDS = rdd.map {line=>
Coltest(line._1,line._2)
}.toDS
可以注意到,定义每一行的类型(case class)时,已经给出了字段名和类型,后面只要往case class里面添加值即可
- Dataset转DataFrame:
这个也很简单,因为只是把case class封装成Row
import spark.implicits._
val testDF = testDS.toDF
- DataFrame转Dataset:
import spark.implicits._
case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型
val testDS = testDF.as[Coltest]
这种方法就是在给出每一列的类型后,使用as方法,转成Dataset,这在数据类型是DataFrame又需要针对各个字段处理时极为方便
特别注意:
在使用一些特殊的操作时,一定要加上 import spark.implicits._ 不然toDF、toDS无法使用
<全文完>