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、工具、项目,比如自定义的数据源。有需要可来这里逛逛,做一名合格的搬运工。
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方法的路径要写全且文件夹后面/*