对于开发来说,最具吸引力的是一组API可以使其提高生产力,易于使用,直观和富有表现力。 Apache Spark对开发人员的吸引力在于它对大量数据集操作十分简易,并且跨语言(Scala,Java,Python和R).
本文主要讲解Apache Spark 2.0中RDD,DataFrame和Dataset三种API;它们各自适合的使用场景;它们的性能和优化;列举使用DataFrame和Dataset代替RDD的场景。文章大部分聚焦DataFrame和Dataset,因为在Apache Spark 2.0中这两个API已经统一。
Apache Spark 2.0统一API的主要动机是:追求简化Spark。通过减少用户学习的概念和提供结构化的数据进行处理。除了结构化,Spark也提供higher-level抽象和API作为特定领域语言(DSL)。
RDD是Spark建立之初的主要面向用户的API。 RDD是不可变分布式弹性数据集,在Spark集群中可跨节点分区,并提供分布式low-level API来操作RDD,包括transformation和action。
在以下场景中,请考虑使用RDD:
你可能会问:RDD是不是成为“二等公民”了?会被弃用么?
答案当然是NO!
通过后面的描述你会得知:Spark用户可以在RDD,DataFrame和Dataset三种数据集之间无缝转换,而且DataFrame与Dataset建立在RDD之上。
DataFrame与RDD一样,都是不可变分布式弹性数据集。不同之处在于,DataFrame的数据集按列名存储,类似于关系型数据库中的表。 DataFrame的设计是为了让大数据处理起来更容易。DataFrame允许开发者把结构化数据集导入DataFrame,并做了higher-level的抽象; DataFrame提供特定领域的语言(DSL)API来操作你的数据集。
在我们之前的博文Apache Spark 2.0 webinar和subsequent blog,提及过在Spark 2.0种DataFrame API会与Dataset API整合。大部分Spark开发者对Dataset的high-level和type-safe API并没有啥概念。
在Spark 2.0中,Dataset具有两个完全不同的API特征:强类型(strongly-typed)API和弱类型(untyped)API,见下表。在概念上,将DataFrame视为对象Dataset[Row]的集合的别名,其中Row是一个弱类型JVM对象。相反,Dataset是强类型JVM对象的集合,由Scala的case class或者Java class定义。
强类型API和弱类型API
Language | Main Abstraction |
---|---|
Scala | Dataset[T] & DataFrame (alias for Dataset[Row]) |
Java | Dataset[T] |
Python* | DataFrame |
R* | DataFrame |
Note: Python和R没有编译时type-safety,所以只提供弱类型的API:DataFrame
对于Spark开发者而言,你将从Spark 2.0的DataFrame和Dataset统一的API获得以下好处:
1.静态类型(Static-typing)和运行时类型安全(runtime type-safety)
考虑静态类型和运行时类型安全是一个范围,对SQL的限制很少,制而Dataset限制很多。例如,Spark SQL查询语句,你直到运行时才能发现语法错误(syntax error),代价较大。然后DataFrame和Dataset在编译时就可捕捉到错误,节约开发时间和成本。
也就是说,如果在DataFrame中调用不是API部分的函数,编译器将捕获它。但是,它不会在运行时之前检测到不存在的列名。
在远端是Dataset,限制最大。由于Dataset API都表示为lambda函数和JVM类型对象,因此任何类型参数的不匹配在编译时将被检测到。此外,在使用Dataset时,也可以在编译时检测到分析错误,从而节省开发人员时间和成本。
所有这些都意味着你的Spark代码中的语法和分析错误是类型安全的一个范围,Dataset对开发人员来说是最有限制性的。
2.High-level抽象以及结构化和半结构化数据集的自定义视图
DataFrame是Dataset[Row]的集合,将结构化定制视图用于您的半结构化数据中。例如,有个海量IoT设备事件数据集,用JSON格式表示。JSON是一个半结构化数据格式,所以它很适合使用Dataset作为强类型化数据集[DeviceIoTData]的集合。
Copy
1 |
{"device_id": 198164, "device_name": "sensor-pad-198164owomcJZ", "ip": "80.55.20.25", "cca2": "PL", "cca3": "POL", "cn": "Poland", "latitude": 53.080000, "longitude": 18.620000, "scale": "Celsius", "temp": 21, "humidity": 65, "battery_level": 8, "c02_level": 1408, "lcd": "red", "timestamp" :1458081226051} |
您可以将用Scala为JSON数据DeviceIoTData定义case class。
Copy
1 |
case class DeviceIoTData (battery_level: Long, c02_level: Long, cca2: String, cca3: String, cn: String, device_id: Long, device_name: String, humidity: Long, ip: String, latitude: Double, lcd: String, longitude: Double, scale:String, temp: Long, timestamp: Long) |
紧接着,从JSON文件读取数据:
Copy
1 2 3 4 |
// read the json file and create the dataset from the // case class DeviceIoTData // ds is now a collection of JVM Scala objects DeviceIoTData val ds = spark.read.json("/databricks-public-datasets/data/iot/iot_devices.json").as[DeviceIoTData] |
这个时候有三个事情会发生:
我们大多数使用结构化数据的人都习惯于以柱状方式查看和处理数据,或者访问对象中的特定属性。将Dataset作为Dataset [ElementType]类型对象的集合,您可以无缝地获得强类型JVM对象的编译时安全性和自定义视图。您可以使用高级方法轻松地显示或处理上面代码中生成的强类型数据集[T]。
3.简单易用的API
虽然结构化数据会给Spark程序操作数据集带来挺多限制,但它引入了丰富的语义和一组简单的特定操作。大部分计算可以被Dataset的high-level API所支持。然而,大多数计算可以使用Dataset的高级API完成。例如,通过访问Dataset类型化对象的DeviceIoTData比使用RDD行的数据字段执行agg,select,sum,avg,map,filter或groupBy操作简单得多。
使用特定领域语言API进行计算比在RDD中更加简单(relation algebra type expressions)。例如,下面的语句用filter()和map()创建了另一个不可变Dataset
Copy
1 2 3 4 5 6 7 8 9 |
// Use filter(), map(), groupBy() country, and compute avg() // for temperatures and humidity. This operation results in // another immutable Dataset. The query is simpler to read, // and expressive val dsAvgTmp = ds.filter(d => {d.temp > 25}).map(d => (d.temp, d.humidity, d.cca3)).groupBy($"_3").avg() //display the resulting dataset display(dsAvgTmp) |
4.性能和优化
除了上述所有优点,您不能忽视使用DataFrames和Dataset API时的空间效率和性能提升,有两个原因。
首先,因为DataFrame和Dataset API构建在Spark SQL引擎之上,它使用Catalyst生成优化的逻辑和物理查询计划。在R,Java,Scala或Python DataFrame / Dataset API中,所有关系类型查询都采用相同的代码优化器,从而提供空间和速度效率。虽然为数据工程任务优化了Dataset[T]类型的API,但是未类型化的Dataset[Row](DataFrame的别名更快,适合于交互式分析。
其次,由于Spark作为编译器理解您的Dataset类型JVM对象,它使用Encoders将特定于类型的JVM对象映射到Tungsten内存管理。因此,Tungsten的Encoder可以有效地对JVM对象进行序列化/反序列化,以及生成可以以极高速度执行的紧凑字节码。
你可以无缝的把DataFrame或者Dataset转化成一个RDD,只需简单的调用 .rdd,例如
Copy
1 2 3 4 5 6 |
// select specific fields from the Dataset, apply a predicate
// using the where() method, convert to an RDD, and show first 10
// RDD rows
val deviceEventsDS = ds.select($"device_name", $"cca3", $"c02_level").where($"c02_level" > 1300)
// convert to RDDs and take the first 10 rows
val eventsRDD = deviceEventsDS.rdd.take(10)
|
总的来说,选择何时使用RDD或DataFrame和Dataset似乎是显而易见的。虽然前者为您提供低级功能和控制,后者允许自定义视图和结构,提供高级和域特定操作,节省空间,并以超高速执行。
我们从Spark早期版本中得到的经验 – 如何为开发人员简化Spark,如何优化和提高性能 – 我们决定将低级RDD API提升为高级抽象,如DataFrame和Dataset,以及在Catalyst优化器和Tungsten之上的库之间构建这种统一的数据抽象。
选择一个DataFrame、Dataset或RDD API,以满足您的需求和用例,不过我肯定大多数开发都工作在结构化或者半结构化的数据中。
您可以尝试在Databricks上的Apache Spark 2.0的预览版(注截止到我翻译完毕,已经推出了Spark 2.1版),并运行这个附带的notebook。如果你还没有注册,try Databricks now.
原文:A Tale of Three Apache Spark APIs: RDDs, DataFrames, and Datasets