从Spark1.3.0版本开始推出DataFrame,DataFrame让Spark具备了处理大规模结构化数据的能力,在比原有的RDD转化方式易用的前提下,计算性能更快。发布DataFrame之后开发者收到了很多反馈,其中一个主要的是大家反映缺乏编译时类型安全。为了解决这个问题,Spark采用新的 Dataset API (DataFrame API的类型扩展)。
从Spark1.6.0版本开始引入Dataset,Dataset API扩展DataFrame API,支持静态类型和运行已经存在的Scala或Java语言的用户自定义函数。对比传统的RDD API,Dataset API提供更好的内存管理,特别是在长任务中有更好的性能提升。
Dataset 效率的提升主要是得益于Tungsten项目,Tungsten项目是Spark内核级别的优化。Tungsten做的优化就是由Spark自己来管理内存而不是使用JVM,这样可以避免JVM GC带来的性能损失;内存中的Java对象被存储成Spark自己的二进制格式,更加紧凑,节省内存空间,而且能更好的估计数据量大小和内存使用情况;计算直接发生在二进制格式上,省去了序列化和反序列化时间。
Dataset 是一个强类型的、不可变的对象集合。DataSets的API核心是一个新的编码器,编码器的作用是将JVM的对象与表结构进行转换。表结构采用的Tungsten的二进制格式存储,允许操作序列化数据,提高内存利用率。Spark1.6附带支持各种类型的自动编码器,包括原始类型(如字符串、整数、长整数),Scala case类,和Javabean。
Dataset的API与RDD的API类似,它也提供了许多相同的转换操作(如map、flatMap、filter)。例如下面的代码,读取一个文本文件并进行分词操作:
RDDs val lines = sc.textFile("/wikipedia") val words = lines .flatMap(_.split(" ")) .filter(_ != "") |
Datasets val lines = sqlContext.read.text("/wikipedia").as[String] val words = lines .flatMap(_.split(" ")) .filter(_ != "") |
虽然这个高级代码可能看起来类似,但是Dataset能提供一个完整的关系执行引擎。例如,如果你现在想要执行一个聚合(如计算每个单词出现的次数),该操作可以简单有效地表达如下:
RDDs
val counts = words
.groupBy(_.toLowerCase)
.map(w => (w._1, w._2.size))
Datasets
val counts = words
.groupBy(_.toLowerCase)
.count()
Dataset可以利用内置的聚合函数计算,这种计算不仅可以用更少的代码,而且它也将执行更快。你可以看到在下面的图中, Dataset的实现比RDD的实现跑快得多。相比之下,如果使用RDD也要得到相同的性能,需要用户手动地考虑如何优化所表达计算的方式。
Dataset—API的另一个好处是减少内存的使用。因为是采用Spark自己的数据结构,Spark能够解析在DataSets中结构化的数据,并在内存中优化结构,将DataSets中的数据缓存起来。同比原生的RDD,要节省相当多的内存空间。在下面的例子中,我们比较缓存几百万字符串在内存中使用Dataset和RDD。在这两种情况下,缓存数据对于后续的查询可能会导致显著的性能改进,然而对于Dataset,由于数据编码器提供更多的信息关于数据的存储,使得优化缓存可以少用4.5倍空间。
Spark采用的是一种快速序列化与编码器,该编码器是高度优化和使用运行中的代码生成来构建自定义序列化和反序列化的字节码。结果,他们可以显著快于Java或Kryo序列化。
除了速度,序列化编码后的数据大小也可以小得多(减少2倍),可以减少网络传输的成本。此外,序列化数据采用的Tungsten 的二进制格式,这意味着许多操作可以一次完成,而不需要实现一个对象。Spark内置支持自动生成编码器的有多种原始类型(如字符串、整数、长),Scala case类,和Javabean。
该编码器功能强大,支持半结构化的格式(例如JSON)。例如,以下关于大学信息的数据集:
{"name": "UC Berkeley", "yearFounded": 1868, numStudents: 37581}
{"name": "MIT", "yearFounded": 1860, numStudents: 11318}
...
可以代替手动提取字段和指定类型,通过一个简单地定义,定义一个类来指定输出的数据结果,并和输入数据进行映射。其中列是按照名称自动排列,并保存类型。
case class University(name: String, numStudents: Long, yearFounded: Long)
val schools = sqlContext.read.json("/schools.json").as[University]
schools.map(s => s"${s.name} is ${2015 - s.yearFounded} years old")
编码器可以自动检查你的数据是否与格式匹配,并且提供有用的错误信息,当你在使用TBs数据时。举个例子,如果我们尝试使用的一个数据类型,如果太小,这样转换为一个对象将导致截断(即numStudents大于一个字节,最大值为255)分析器将发出一个AnalysisException。
case class University(numStudents: Byte)
val schools = sqlContext.read.json("/schools.json").as[University]
org.apache.spark.sql.AnalysisException: Cannot upcast `yearFounded` from bigint to smallint as it may truncate
当数据执行映射操作时,编码器将自动处理复杂类型,包括嵌套类、数组和maps。
Dataset的API的另一个目标是提供一个统一的接口,可用在Scala和Java中。这个统一接口对Java用户来说是个好消息,因为它确保他们不会落后于Scala的API接口,使得一些代码示例和库不再需要处理两种不同类型的输入。对于Java用户唯一的区别是他们需要指定要使用的编码器,因为编译器不提供类型信息。例如,如果想要处理json数据使用Java可以做到如下:
public class University implements Serializable {
private String name;
private long numStudents;
private long yearFounded;
public void setName(String name) {...}
public String getName() {...}
public void setNumStudents(long numStudents) {...}
public long getNumStudents() {...}
public void setYearFounded(long yearFounded) {...}
public long getYearFounded() {...}
}
class BuildString implements MapFunction<University, String> {
public String call(University u) throws Exception {
return u.getName() + " is " + (2015 - u.getYearFounded()) + " years old.";
}
}
Dataset<University> schools = context.read().json("/schools.json").as(Encoders.bean(University.class));
Dataset<String> strings = schools.map(new BuildString(), Encoders.STRING());
Datasets是一个新的API,对于RDD和现有Spark项目能很容易的进行交互操作。在Datasets 上简单地调用rdd()方法将会得到一个RDD。从长远来看,我们希望Datasets 可以成为处理结构化数据的常用方法,进一步我们可能集合api。
展望Spark2.0,我们计划一些激动人心的Datasets 的改进,具体来说:
1)性能优化,在很多情况下, Dataset的当前实现API尚未利用的附加信息,可能会慢于RDD。接下来的几个版本中,我们将致力于改善性能。
2)定制编码器,目前自动编码器适用于各种类型,我们想打开自定义对象的API。
3)Python支持。
4)统一Datasets与DataFrames,由于兼容性,目前DataFrames与Datasets数据不能共享一个共同的父类。Spark2.0,我们将能够统一这些抽象对象。
转载请注明出处:
http://blog.csdn.net/sunbow0