什么是Spark Datasource API
Spark Datasource API 是一套連接外部數據源和Spark引擎的框架
它主要是給Spark框架提供一種快速讀取外界數據的能力,它可以方便地把不同的數據格式通過DataSource API注冊成Spark的表,然后通過Spark SQL直接讀取。它可以充分利用Spark分布式的優點進行并發讀取,而且SparkSQL本身有一個很好的Catalyst優化引擎,能夠極大的加快任務的執行。
Spark Datasource API 同時提供了一套優化機制,如將列剪枝和過濾操作下推至數據源側,減少數據讀取數量,提高數據處理效率。
我們先看一下Spark DataSource API 典型的工作方式:
sparkSession //SparkSession
.read
.format("csv") //驅動類,類似JDBC的driver class
.option(Map(....)) //你需要額外傳遞給驅動類的參數
.load("hdfs:///...") //數據文件路徑
或者
CREATE TEMPORARY TABLE table_test USING druid OPTIONS (timeColumn "time",datasource "spark-druid-test ",zkHost "...")
CREATE TABLE spark_druid_test USING druid OPTIONS
(timeColumn "time",
datasource "spark-demo",
zkHost "node1:2015,node2:2015,node3:2015")
目前Spark DataSource的來源主要有三個:
- Spark 原生支持的DataSource,如常用的csv,orc,parquet;
- Spark Packages 網站中納入的第三方包;
- 隨其他項目一起發布的內嵌DataSource,如ES-Hadoop等。
Spark Datasource API 工作流程
讀流程:
sparkSession.read 返回DataFrameReader,它是DataSource 讀數據的入口,DataFrameReader中提供了讀取多種Spark內置DataSource 的方法,包括參數配置接口。
sparkSession通過調用不同DataSource的接口,實現DataSource的參數配置,最終通過load方法真正建立Spark與DataSource的連接。
load函數最重要的功能就是將baseRelation轉換成DataFrame,該功能是通過sparkSession的
def baseRelationToDataFrame(baseRelation: BaseRelation): DataFrame
接口實現的,其中的參數baseRelation
通過DataSource類的resolveRelation
方法提供。
resolveRelation中使用反射創建出對應DataSource實例,協同用戶指定的userSpecifiedSchema進行匹配,匹配成功返回對應的baseRelation:
- 如果是基于文件的,返回HadoopFsRelation實例
- 非文件的,返回如KafkaRelation或者JDBCRelation
上面提到baseRelationToDataFrame接受baseRelation參數返回DataFrame,是通過Dataset.ofRows(sparkSession,logicalPlan)方法實現的,其中的參數logicPlan是由LogicalRelation(baseRelation)得到。
寫流程
dataSet.write 返回DataFrameWriter類型對象, 它是DataSource寫數據的入口,與讀機制類似,DataFrameWriter提供了DataSource的接口和參數配置方法,底層落到save方法上,運行runCommand執行寫入過程,runCommand所需的LogicalPlan由對應的DataSource.planForWriting()提供。
如何擴展Spark Datasource API
所有DataSource的擴展都是基于spark\sql\core\src\main\scala\org\apache\spark\sql\sources\interfaces.scala提供的接口來實現:
- interface
一般來講,自定義數據源需要實現以下接口和功能:
- BaseRelation:代表了一個抽象的數據源,描述了數據源和Spark SQL交互
- 數據掃描接口(根據需要實現):
- TableScan:全表數據掃描
- PrunedScan:返回指定列數據,其他的列數據源不用返回
- PrunedFilteredScan:指定列的同時,附加一些過濾條件,只返回滿足過濾條件的數據
- RelationProvider: 根據用戶提供的參數返回一個BaseRelation
- 數據源RDD: 將DataSource的數據讀取后裝配成RDD
以JDBC為例看一下DataSource擴展的流程:
(1) 【JDBCRelation】
- JDBCRelation
JDBCRelation實現了BaseRelation、PrunedFilteredScan和InsertableRelation接口,在Spark層面描述了JDBC DataSource,以及數據讀取(buildScan)和寫入(insert)邏輯,它的全部工作就是重寫以上三個接口的方法,方法清單:
BaseRelation:sqlContext、needConversion、schema、unhandledFilters
PrunedFilteredScan:上文提到了,這個是提供列裁剪和過濾的讀取接口,只需要實現一個方法buildScan就好了,buildScan通過調用JDBCRDD.scanTable將從數據庫中讀出的數據裝配成RDD。
InsertableRelation:實現寫入接口insert,將DataFrame寫入DataSource,調用的是DataFrameWriter的jdbc方法。
- buildScan & insert
(2) 【JdbcRelationProvider】
- JdbcRelationProvider
JdbcRelationProvider實現了CreatableRelationProvider、RelationProvider、DataSourceRegister。重寫了shortName和兩個createRelation方法:
- DataSourceRegister:shortName方法比較簡單,就是為DataSource提供一個別名,這樣用戶在使用實現的DataSource API的時候,提供這個別名就可以了。
- RelationProvider:重寫createRelation方法,根據用戶提供的參數創建baseRelation
- CreatableRelationProvider:重寫createRelation方法,基于給定的DataFrame和用戶參數返回baseRelation,它描述了當數據已存在情況下的createRelation行為。支持寫入模式如append、overwrite。
(3) 【JDBCRDD】
一個JDBCRDD代表了關系數據庫中的一張表,在Spark的Driver和Executor端都必須能夠通過JDBC訪問這張表,其中Driver獲取schema信息,Executor獲取數據。
JDBCRDD重寫了RDD的getPartitions和compute方法,其中compute方法就是從關系表里讀出數據,使用JdbcUtils.resultSetToSparkInternalRows( )將數據轉換成SparkInternalRow格式。
JDBCRDD的伴生類中還有兩個非常重要的方法:resolveTable和scanTable。這兩個方法功能都比較清楚,前者是將表的schema信息以Spark 內部StructType的形式返回,后者其實是使用對應的參數創建了一個JDBCRDD的對象,對象中以RDD[InternalRow]形式映射了當前讀取的關系表數據。這兩個方法分別被JDBCRelation
中重寫的方法-schema
和buildScan
調用。
DataSource API 流程調用鏈
下圖是DataSource的讀寫流程調用鏈,以JDBC為例:
- read
- write
File Source 實現原理初探
Spark中內置的基于文件的數據源有以下幾種:
- text
- csv
- json
- parquet
- orc
它們都擴展了DataSource中的FileFormat特質,那么什么是FileFormat呢?
FileFormat有讀、寫兩方面的功能:
- 讀:將文件中的數據讀取成為Spark內部的InternalRow格式
- 寫:將Spark內部的InternalRow格式以對應的格式寫入文件
該特質有幾個主要的接口:
- inferSchema(自動推測模式),返回類型為
Option[StructType]
:
當option中設置inferSchema為true情況下,無需用戶編碼顯示指定模式,而是由系統自動推斷模式。但是當該文件格式不支持模式推測或者傳入的文件路徑非法時,該方法返回None,此時需要用戶顯示指定schema。基本思路就是將傳入的文件路徑使用baseRelationToDataFrame方法轉換成為DataFrame,然后取一行進行格式推測。 - prepareWrite,返回類型OutputWriterFactory:
這里通過參數spark.sql.sources.outputCommitterClass可以配置用戶自定義的output committer。 - supportBatch,是否支持批量列的讀入和寫出
- isSplitable,單個文件是否能被切分
- buildReader,返回一個能夠將單個文件讀成Iterator[InternalRow]的方法
DataSource 在匹配類型時,會通過反射得到DataSource類型(FileFormat),返回HadoopFsRelation的BaseRelation,后續通過DataFrameReader的load接口獲取DataFrame。
寫過程調用DataFrameWriter save方法,構造DataSource實例,通過className確定DataSource類型,然后調用dataSource.write(mode, df)寫操作。
接下來看一下datasource的寫方法:write方法通過providingClass.newInstance()
實例化分別匹配CreatableRelationProvider和FileFormat,如果是CreatableRelationProvider
(其實針對內置的datasource,就是JDBCRelationProvider),調用dataSource.createRelation,如果是FileFormat
類型,構造InsertIntoHadoopFsRelationCommand
,其實該實例是一個RunnableCommand
,得到該實例后,就通過調用sparkSession.sessionState.executePlan(plan).toRdd
來執行RunnableCommand
。