Spark之SQL高级知识分享二(DataSource+Tuling+CustomDataSource)

1.DataSource

1.1传统的ETL数据操作弊端


如上图,传统的方式如MR、Hive、Spark core方式进行数据ETL操作有如下弊端:

  • 弊端一:若涉及数据格式的转换,则代码或sql相关的逻辑就得重写。
  • 弊端二:若数据的来源非常多样化,混杂,则实现起来就更加麻烦了。

基于这种情况,Spark1.2 诞生了External Data Sources,使用它我们可以非常的方便将外部数据源转换成DF或DS以及将相关数据保存到外External Data Sources。

1.2 Build-In DataSource Options

Spark内置了** Parquet、ORC、JSON、Hive Tables、JDBC To Other DataBases 、Avro**等外部数据源读取和保存操作。详情可参考官网:http://spark.apache.org/docs/latest/sql-data-sources.html,Spark默认的数据源是Parquet,parquet格式数据在读取性能上有良好表现。

1.3 DataSource Option

如下,标准的load、save数据的流程:

val peopleDF = spark.read.format("json").load("exampleData/people.json")
peopleDF.write.save.format("parquet").save("namesAndFavColors.parquet")
1.4 Some Notes
  • (重要)DataSource Parquet:具有分区自动探索能力,会根据路径下的的k=v名称子路径的值将分区数据信息带上,k为列,v为值,列的类型默认时自动推导的,可以设置。
  • DataSource Parquet:具有Schema自动合并能力。这种非常耗资源,1.5开始默认时关的。建议不要使用
  • (重要)DataSource Hive Table:想要Spark session能够操作Hive,必须开启支持hive操作.enableHiveSupport()
  • (重要)DataSource Hive Table:若我们没有设置hive-site.xml 则底层默认启动metastore_db 数据库
  • 重要)DataSource JDBC:读取还是用它自带的load,生产采坑,注意写最好使用foreachPartition加jdbc方式。

2.(重要)DataSource Tuling

2.1Caching Data In Memory

可通过df.cache()方式缓存数据,默认是MEMORY_AND_DISK级别。

2. 2 Other Configuration Options

一些有用的配置优化:
spark.sql.autoBroadcastJoinThreshold:默认10M,即表数据只有10M时就自动broadcast,这个值太小,具体根据业务进行优化。
spark.sql.shuffle.partitions:默认是200,join操作时ResultStage的parition数量(spark-sql可测试),这个值也是太小了。

2.3 Broadcast Hint for SQL Queries

主动的广播出去小表,注意注意,一定不要写错了,别把大表广播出去了。

import org.apache.spark.sql.functions.broadcast
broadcast(spark.table("src")).join(spark.table("records"), "key").show()

3.(重要)Custom Extral DataSource

3.1 third-party packages简介

官网这里提供了很多Spark第三方的jar、工具、项目,比如自定义的数据源。有需要可来这里逛逛,做一名合格的搬运工。
2

3.2 JDBC外部数据源实现源码解析

但是在生产上spark内置的以及第三方的外部数据源有时并不能满足我们的需求,这时我们就需要自己Custom Extral DataSource,可参考JDBC数据源的JDBCRelation.scala以及JdbcRelationProvider.scala的实现。

class JdbcRelationProvider extends CreatableRelationProvider
  with RelationProvider with DataSourceRegister {。。。。。}
  
private[sql] case class JDBCRelation(
    override val schema: StructType,
    parts: Array[Partition],
    jdbcOptions: JDBCOptions)(@transient val sparkSession: SparkSession)
  extends BaseRelation
  with PrunedFilteredScan
  with InsertableRelation {。。。。。。。}

如下是列举了一些常见的trait:

  • abstract class BaseRelation:外部数据源基类,保存了定义好的的Schema
  • trait TableScan { def buildScan(): RDD[Row]}:读取数据构建RDD[ROW],类似于:select * from
  • trait PrunedScan{def buildScan(requiredColumns: Array[String]): RDD[Row]} :同上,但是能够传入一个列名列表参数,类似于:select a,b,c from
  • trait PrunedFilteredScan { def buildScan(requiredColumns: Array[String], filters: Array[Filter]):RDD[Row]:同上,但是能够传入一个列名列表参数以及过滤条件列表参数,类似于:select a,b,c from where a!=0
  • trait CatalystScan {def buildScan(requiredColumns: Seq[Attribute], filters: Seq[Expression]):RDD[Row]}:用于实验的,不建议用,效果完全同PrunedFilteredScan
  • trait InsertableRelation {def insert(data: DataFrame, overwrite: Boolean): Unit}:存储数据。写数据的
  • trait RelationProvider {def createRelation(sqlContext: SQLContext, parameters: Map[String, String]): BaseRelation}:用于创建relation,parameters即是设置的参数列表,如设置jdbc的链接信息等
  • trait SchemaRelationProvider { def createRelation(sqlContext: SQLContext,parameters: Map[String, String],schema: StructType): BaseRelation}:同RelationProvider创建Relation,但是它多传了一个Schema
  • trait DataSourceRegister { def shortName(): String}:注册一个外部数据源字符串名字,如parquet、jdbc等。
3.3 (重要)Custom Extral Text DataSource 编程

之前提到过有两种方式将RDD转成DF,但是这种方式在遇到格式变化时都是比较麻烦的。因此在这里自定义Text外部数据源:

package com.wsk.spark.sql.textdatasource

import org.apache.spark.sql.types.{DataType, LongType, StringType}

/**
  * 转换类型
  */
object Utils {

  def castTo(value:String, dataType:DataType) ={
    dataType match {
      case _:LongType => value.toLong
      case _:StringType => value
    }
  }

}


package com.wsk.spark.sql.textdatasource

import org.apache.spark.internal.Logging
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{Row, SQLContext}
import org.apache.spark.sql.sources._
import org.apache.spark.sql.types.{LongType, StringType, StructField, StructType}

/**
  * 读取外部数据以及保存
  */
class TextDatasourceRelation(override val sqlContext: SQLContext,
                             path:String,
                             userSchema:StructType)
  extends BaseRelation with TableScan
//  with PrunedScan with PrunedFilteredScan
    with Logging{

  override def schema: StructType = {
    if(userSchema != null){
      userSchema
    } else {
      StructType(
        StructField("id",LongType,false) ::
          StructField("name",StringType,false) ::
          StructField("gender",StringType,false) ::
          StructField("salary",LongType,false) ::
          StructField("comm",LongType,false) :: Nil
      )
    }
  }

  // select * from xxx
  override def buildScan(): RDD[Row] = {
    logError("this is ruozedata custom buildScan...")

    var rdd = sqlContext.sparkContext.wholeTextFiles(path).map(x => {
      println(x)
      x._2
    })
    val schemaField = schema.fields

    rdd.foreach(println)
    // rdd + schemaField

    val l = rdd.count()
    // rdd + schemaField
    val rows = rdd.map(fileContent => {
      val lines = fileContent.split("\n")
      val data = lines.map(_.split(",").map(x=>x.trim)).toSeq

      val result = data.map(x => x.zipWithIndex.map{
        case  (value, index) => {

          val columnName = schemaField(index).name

          Utils.castTo(if(columnName.equalsIgnoreCase("gender")) {
            if(value == "0") {
              "男"
            } else if(value == "1"){
              "女"
            } else {
              "未知"
            }
          } else {
            value
          }, schemaField(index).dataType)
        }
        })
      result.map(x => Row.fromSeq(x))
    })

    rows.flatMap(x=>x)
}

//  override def buildScan(requiredColumns: Array[String]): RDD[Row] = ???
//
//  override def buildScan(requiredColumns: Array[String], filters: Array[Filter]): RDD[Row] = ???
}

package com.wsk.spark.sql.textdatasource

import org.apache.spark.sql.SQLContext
import org.apache.spark.sql.sources.{BaseRelation, RelationProvider, SchemaRelationProvider}
import org.apache.spark.sql.types.StructType

/**
  * 注意这类名字必须是DefaultSource,不然会报错
  *
  */
class DefaultSource extends RelationProvider with SchemaRelationProvider {
  override def createRelation(
                               sqlContext: SQLContext,
                               parameters: Map[String, String],
                               schema: StructType): BaseRelation = {
    val path = parameters.get("path")

    path match {
      case Some(p) => new TextDatasourceRelation(sqlContext, p, schema)
      case _ => throw new IllegalArgumentException("path is required...")
    }

  }

  override def createRelation(sqlContext: SQLContext, parameters: Map[String, String]): BaseRelation = {
    createRelation(sqlContext, parameters, null)
  }
}


package com.wsk.spark.sql.textdatasource

import org.apache.spark.sql.SparkSession

/**
  * 测试
  */
object TextApp {

  def main(args: Array[String]): Unit = {

    val spark = SparkSession.builder().appName("TextApp").master("local[2]").getOrCreate()
    val df = spark.read.format("com.wsk.spark.sql.textdatasource").option("path","D://wskspace/workspace1/data/spark-train/data/textdatasource/*").load()
    df.show()

    spark.stop()
  }

}

  • 注意1: 我这里测试时wholeTextFiles方法的路径要写全且文件夹后面/*
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值