Spark数据源API:扩展我们的Spark SQL查询引擎

在上一篇文章Apache Spark作为分布式SQL引擎中 ,我们解释了如何使用SQL查询存储在Hadoop中的数据。 我们的引擎能够从分布式文件系统中读取CSV文件,能够自动从文件中发现模式,并通过Hive元存储将它们作为表公开。 所有这些都是为了能够将标准SQL客户端连接到我们的引擎并浏览我们的数据集而无需手动定义文件的架构,从而避免了ETL工作。

Spark提供了可以扩展的框架,我们将通过扩展其某些功能来进一步扩展其功能。

Spark数据源API

数据源API允许我们以任何格式管理结构化数据。 Spark已经内置了一些标准结构,例如Avro和Parquet,但是第三方通过扩展此API为CSV,JSON和其他应用创建了新的阅读器。 今天,我们将创建自己的。

扩展API有两个原因。

首先,我们需要一个能够读取传统格式并将当前数据源转换为更易于使用的新数据库的库。

其次,我们希望在所有使用我们的数据的应用程序中共享该库,从而避免了为实现同一目标而需要共享的应用程序的复杂包装。

资料来源

我们的数据源包含文件的集合,其中每个文件本身就是一个实体。 为了这个示例,我们定义了一种简单的格式,其中每个文件都是一个文本文件,其中包含用户信息,每个字段逐行。 让我们看一个文件的例子。

pepe
20
Miami
Cube

该文件代表一个名为“ pepe”的用户,该用户今年20岁,住在迈阿密,出生在古巴。

在现实世界中,格式可以像我们想要的那样复杂,但是我们将要解释的过程不会改变。

每个文件具有相同的格式,而我们有数百万个文件。 我们还希望公开它们以在SQL中进行查询。

我们的实施

为了扩展数据源API,我们需要从Spark框架中实现某些类,以便可以加载和使用我们的自定义阅读器。

让我们首先创建一个Spark应用程序作为示例的入口。 我们可以按照SBT,Scala和Spark的帖子进行操作。

创建应用程序后,我们要做的第一件事是链接正确的Spark库。 我们将在Spark 1.5.1上运行示例,并且sbt文件定义如下。

name := "spark-datasource" version := "1.0" scalaVersion := "2.11.7" libraryDependencies += "org.apache.spark" % "spark-core_2.11" % "1.5.1" libraryDependencies += "org.apache.spark" % "spark-sql_2.11" % "1.5.1"

创建架构

数据源API的起始扩展点是RelationProvider类。 RelationProvider类将用于创建我们数据的必要关系。

我们还需要混合使用SchemaRelationProvider特性,这允许我们创建所需的架构。

我们需要创建一个名为DefaultSource的类,Spark将在给定的包中查找它。 DefaultSource类将扩展RelationProvider并混合SchemaRelationProvider

到目前为止,我们的代码如下:

class DefaultSource extends RelationProvider with SchemaRelationProvider {   override def createRelation(sqlContext: SQLContext, parameters: Map[String, String])     : BaseRelation = {     createRelation(sqlContext, parameters, null)   }   override def createRelation(sqlContext: SQLContext, parameters: Map[String, String]     , schema: StructType)     : BaseRelation = {     parameters.getOrElse("path", sys.error("'path' must be specified for our data."))     return new LegacyRelation(parameters.get("path").get, schema)(sqlContext)   } }

在代码中,我们基本上是在创建一个LegacyRelation对象,该对象定义了我们要创建的Relation。 考虑一下诸如具有已知模式的元组集合的关系。

让我们看看如何实现Relation类。

class LegacyRelation(location: String, userSchema: StructType) (@transient val sqlContext: SQLContext)   extends BaseRelation        with Serializable {   override def schema: StructType = {     if (this.userSchema != null) {       return this.userSchema     }     else {       return StructType(Seq(StructField("name", StringType, true),                              StructField("age", IntegerType, true)))     }   } }

在这里,我们将覆盖架构函数,以便它返回所需的架构。 在此示例中,我们知道了数据的架构,但是在这里,我们可以做任何想要获得所需架构的事情。 如果数据是CSV,我们可以使用文件的标题来推断模式,或者执行我们需要的其他任何操作。

注意,我们只需要名称和年龄字段,而不是实体的全部内容。

下一步是测试我们是否获取了正确的架构,我们可以通过将以下代码添加到我们的应用程序中来进行此操作。

object app {   def main(args: Array[String]) {     val config = new SparkConf().setAppName("testing provider")     val sc = new SparkContext(config)     val sqlContext = new SQLContext(sc)        val df = sqlContext               .read               .format("com.nico.datasource.dat")               .load("/Users/anicolaspp/data/")                       df.printSchema()   } }

这段代码从中创建一个SparkContext和一个SQLContext。 使用SQLContext,我们通过传递包名称来设置格式(请记住,Spark将在此包中查看DefaultSource类)。 然后,使用提供程序将指定路径中的数据加载到DataFrame中。

df.printSchema()

将打印我们定义的模式,输出应如下所示。

root  |-- name: string (nullable = true)  |-- age: integer (nullable = true)

至此,我们仅创建了所需的架构,但是没有任何内容说明如何准备数据以及如何将其结构化为定义的架构。

将数据读入我们的模式

为了从我们的数据源读取,我们的LegacyRelation类需要混合使用TableScan特征。 TableScan有一种我们需要使用以下签名实现的方法:

def buildScan(): RDD[Row]

方法buildScan应该返回数据源中的所有行。 在我们的特定情况下,每一行将是每个文件的选定内容。 让我们看一下buildScan的实现。

override def buildScan(): RDD[Row] = {     val rdd = sqlContext                 .sparkContext                 .wholeTextFiles(location)                 .map(x => x._2)        val rows = rdd.map(file => {       val lines = file.split("\n")       Row.fromSeq(Seq(lines(0), lines(1)))     })     rows   }

在这里,我们使用了WholeTextFiles方法,该方法读取整个文件(每个文件是一个实体),读取前两行(我们想要的唯一字段),并从每行创建一行。 结果是行的集合,其中仅使用我们关心的文件部分来创建每一行。

这足以修改我们的应用程序,以便打印出我们数据源的内容。 现在,该应用程序如下所示。

object app {   def main(args: Array[String]) {     val config = new SparkConf().setAppName("testing provider")     val sc = new SparkContext(config)     val sqlContext = new SQLContext(sc)        val df = sqlContext               .read               .format("com.nico.datasource.dat")               .load("/Users/anicolaspp/data/")                       df.show()   } }

即使我们正在将所需的格式读入数据帧,也没有有关数据字段类型的信息。 我们的架构定义支持不同的数据类型,但我们不强制执行它们。

让我们修改buildScan方法,以便在创建每一行时推断类型信息。

override def buildScan(): RDD[Row] = {     val schemaFields = schema.fields     val rdd = sqlContext                 .sparkContext                 .wholeTextFiles(location)                 .map(x => x._2)          val rows = rdd.map(file => {       val lines = file.split("\n")              val typedValues = lines.zipWithIndex.map {         case (value, index) => {           val dataType = schemaFields(index).dataType           castValue(value, dataType)         }     nbsp;  }       Row.fromSeq(typedValues)     })          rows   }       private def castValue(value: String, toType: DataType) = toType match {     case _: StringType      => value     case _: IntegerType     => value.toInt   }

在这里,唯一的变化是,我们将从文件中读取的每个值都转换为从schema.fields对象推断出的正确类型。 在我们的特殊情况下,我们只对名称是String且age integer感兴趣,但是在这一点上,我们可能很有创造力。

现在,我们最终的LegacyRelation类如下所示。

class LegacyRelation(location: String, userSchema: StructType)   (@transient val sqlContext: SQLContext)   extends BaseRelation       with TableScan with Serializable {   override def schema: StructType = {     if (this.userSchema != null) {       return this.userSchema     }     else {       return StructType(Seq(StructField("name", StringType, true),                              StructField("age", IntegerType, true)))     }   }   private def castValue(value: String, toType: DataType) = toType match {     case _: StringType      => value     case _: IntegerType     => value.toInt   }   override def buildScan(): RDD[Row] = {     val schemaFields = schema.fields     val rdd = sqlContext               .sparkContext               .wholeTextFiles(location)               .map(x => x._2)                    val rows = rdd.map(file => {       val lines = file.split("\n")       val typedValues = lines.zipWithIndex.map{         case (value, index) => {           val dataType = schemaFields(index).dataType           castValue(value, dataType)         }       }       Row.fromSeq(typedValues)     })     rows   }

现在,我们可以将数据加载到DataFrame中并注册,以供SQL客户端使用,如我们在上一篇文章中所解释的。 我们的应用程序就像我们展示的一样简单。

object app {   def main(args: Array[String]) {     val config = new SparkConf().setAppName("testing provider")     val sc = new SparkContext(config)     val sqlContext = new SQLContext(sc)     val df = sqlContext               .read               .format("com.nico.datasource.dat")               .load("/Users/anicolaspp/data/")        df.registerTempTable("users")     sqlContext.sql("select name from users").show()   } }

我们已经展示了足够多的内容,可以将自定义格式读入数据框中,因此我们可以利用DataFrame API的优势,但是可以做更多的事情。

数据源API不仅提供读取数据的功能,而且还以自定义格式编写数据。 如果我们要将数据集从一种格式转换为另一种格式,则此功能非常强大。 让我们看看如何将这些功能添加到我们现有的驱动程序中。

编写格式化程序

假设我们要保存数据,以便可以从其他标准系统读取数据。 我们将加载自定义数据源,并从中创建类似CSV的输出。

为了支持来自API的保存调用,我们的DefaultSource类必须与CreatableRelationProvider特性混合使用。 这个特征有一个我们需要实现的名为createRelation的方法,让我们来看一下。

override def createRelation(sqlContext: SQLContext, mode: SaveMode,      parameters: Map[String, String], data: DataFrame): BaseRelation = {          saveAsCsvFile(data, parameters.get("path").get)     createRelation(sqlContext, parameters, data.schema)   }      def saveAsCsvFile(data: DataFrame, path: String) = {     val dataCustomRDD = data.rdd.map(row => {       val values = row.toSeq.map(value => value.toString)       values.mkString(",")     })     dataCustomRDD.saveAsTextFile(path)   }

基本上,我们将数据帧保存为类似CSV的文件,然后返回与已知架构的关系。

saveAsCsvFile方法正在创建一个RDD [String],我们的数据格式为CSV,然后将其保存到给定的路径。 为简单起见,我们没有在输出文件中包含标头,但请记住,我们可以做任何需要以所需格式输出数据的操作。

我们的DefaultSource类的整个代码如下。

class DefaultSource extends RelationProvider      with SchemaRelationProvider      with CreatableRelationProvider {   override def createRelation(sqlContext: SQLContext,      parameters: Map[String, String]): BaseRelation = {                  createRelation(sqlContext, parameters, null)   }   override def createRelation(sqlContext: SQLContext,      parameters: Map[String, String], schema: StructType): BaseRelation = {              parameters.getOrElse("path", sys.error("'path' must be specified for CSV data."))         return new LegacyRelation(parameters.get("path").get, schema)(sqlContext)   }   def saveAsCsvFile(data: DataFrame, path: String) = {     val dataCustomRDD = data.rdd.map(row => {       val values = row.toSeq.map(value => value.toString)       values.mkString(",")     })     dataCustomRDD.saveAsTextFile(path)   }   override def createRelation(sqlContext: SQLContext, mode: SaveMode,      parameters: Map[String, String], data: DataFrame): BaseRelation = {              saveAsCsvFile(data, parameters.get("path").get)         createRelation(sqlContext, parameters, data.schema)   } }

为了将原始数据保存为类似CSV的格式,我们对应用进行了如下修改。

object app {   def main(args: Array[String]) {     val config = new SparkConf().setAppName("testing provider")     val sc = new SparkContext(config)     val sqlContext = new SQLContext(sc)          val df = sqlContext               .read               .format("com.nico.datasource.dat")               .load("/Users/anicolaspp/data/")             df.write       .format("com.nico.datasource.dat")       .save("/Users/anicolaspp/data/output")   } }

请注意,每次读取/写入数据时,都需要指定DefaultSource类所在的包名称。

现在,我们可以打包我们的库,并将其包含在我们需要使用我们描述的数据源的任何项目中。 正在创建许多其他库来支持我们可以想象的所有可能的格式,现在您可以创建自己的库来为社区做出贡献或仅用于您自己的项目中。

结局

我们已经看到了如何使用Spark Data Source API将自定义格式的数据加载到数据帧中。 我们还回顾了该过程中涉及的类,特别是Spark如何使用包中的DefaultSource来执行所需的操作。 我们还实现了一个输出格式化程序,以便可以根据需要保存数据帧。

我们可以使用Data Source API进行更多操作,但是根据我的经验,找到正确的文档非常困难。 我相信可以创建更好的文档,特别是对于扩展API时非常有用的API部分。

即使我们的示例显示了如何扩展数据源API以支持简单格式,也可以对其进行修改以读取和写入更复杂的类型,例如二进制编码的实体。

将我们自己的数据类型集成到Spark中的能力使其成为在那里进行数据处理的顶级框架之一。

在Hadoop世界中,我们可以找到许多共享目标和功能的工具,但是没有一个工具比Spark具有更高的灵活性和多功能性。 这使得Spark在该领域非常理想。 如果我们对能够在无限环境下工作的处理框架感兴趣,那么Apache Spark是您的最佳选择。

翻译自: https://www.javacodegeeks.com/2016/03/spark-data-source-api-extending-spark-sql-query-engine.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值