SparkSql提供了對Hive的結構化查詢語言,在某些業務場景下,我們可能需要對sql語法進行擴展,在此以自定義merge語法說明其一般步驟。
Hive中parquet格式表的數據文件可能會包含大量碎片文件(每次執行insert時都會產生獨立的parquet文件),碎文件過多會影響hdfs讀寫效率,對錶中的文件合併的一般步驟是通過對rdd做repartition操作,再重新寫入,通過控制repartition的數量即可控制最後生成文件的數量。
一 環境準備:
IntelliJ IDEA開發環境、antlr4安裝包、aspectjweaver包
antlr4是一種開源語法解析器,sparksql內部即是通過antlr4生成的語法解析,下載antlr-4.5-complete.jar,修改.bash_profile文件,添加如下命令:
alias antlr4 = "java -Xms500m -cp /user/local/antlr-4.5-complete.jar org.antlr.v4.Tool"
aspectjweaver是用於做aop切面的jar包,後續會用到
二 修改sql語法:
在Idea中建立相關工程,首先將sparksql源碼中的sql語法文件拷貝過來,源文件位於sql/catalyst目錄下,以g4結尾。
添加MERGE關鍵字
MERGE: 'MERGE';實現MERGE命令:
MERGE TABLE tableIdentifier partitionSpec?
後面的partitionSpec可以指定表的分區信息,
修改好sql語法之後,執行以下命令,此時會生成相關的java文件(注意不要使用idea自帶的antlr編譯器,版本不兼容)
antlr4 -o /Users/admin/workspace/sparkparser/src/main/java/cn/tongdun/sparkparser/antlr4
-package cn.tongdun.sparkparser.antlr4
-visitor -no-listener
-lib /Users/admin/workspace/sparkparser/src/main/antlr4/cn/tongdun/sparkparser
Users/admin/workspace/sparkparser/src/main/antlr4/cn/tongdun/sparkparser/SparkParser.g4
三 實現自定義邏輯
首先實現visitor解析器:
class SparkSqlParserVisitor(conf: SQLConf) extends SparkParserBaseVisitor[AnyRef]{
override def visitSingleStatement(ctx: SparkParserParser.SingleStatementContext): LogicalPlan = withOrigin(ctx){
visit(ctx.statement).asInstanceOf[LogicalPlan]
}
override def visitMergeTable(ctx: SparkParserParser.MergeTableContext): LogicalPlan = withOrigin(ctx){
MergeTableCommand(ctx.tableIdentifier, ctx.partitionSpec)
}
}
上面的類是解析出sql的logicplan,visitMergeTable中即是解析merge語法的logicplan,如果相應的sql語法對應的visit方法沒有被重寫的話,則返回的是null
接下來要實現的是strategy:
object MergeTableStrategy extends Strategy with Serializable{
override def apply(plan: LogicalPlan): Seq[SparkPlan] = plan match {
case s:MergeTableCommand =>
MergeTablePlan(plan.output, s.tableIdentifier, s.partitionSpec) :: Nil
case _ => Nil
}
}
MergeTablePlan實現的是物理執行計劃即physicplan,內部的實現原理是讀取parquet文件,轉換成rdd,映射成dataframe,最後寫入目標路徑。實現對sql的解析入口:
object SparkParserFactory {
var sparkContext:SparkContext = null
def parserSql(sparkSession:SparkSession, sql:String): Dataset[_] ={
sparkContext = sparkSession.sparkContext
sparkSession.experimental.extraStrategies = MergeTableStrategy :: Nil
val parser = new SparkSqlParser(sparkSession)
val plan = parser.parse(sql)
if(plan!=null){
val execution = sparkSession.sessionState.executePlan(plan)
execution.assertAnalyzed()
val clazz = classOf[Dataset[_]]
val constructor = clazz.getDeclaredConstructor(classOf[SparkSession], classOf[QueryExecution], classOf[Encoder[_]])
val dataSet = constructor.newInstance(sparkSession, execution, RowEncoder.apply(execution.analyzed.schema))
return dataSet
}
null
}
}
這個方法嘗試對sql進行解析,如果解析對logicplan不爲null,則轉換成dataset,否則返回null。
最後實現將自定義sql嵌入到sparksql中,這裏就利用到aspect的原理,首先建立AspectSql.aj文件:
public aspect AspectSql {
private static final Logger logger = LoggerFactory.getLogger(AspectSql.class);
public pointcut sparkSqlMethod(String sql): execution(public Dataset org.apache.spark.sql.SparkSession.sql(java.lang.String)) && args(sql) ;
Dataset around(String sql): sparkSqlMethod(sql){
try{
logger.info("parser sql:"+sql);
SparkSession sparkSession = (SparkSession)thisJoinPoint.getThis();
Dataset dataset = SparkParserFactory.parserSql(sparkSession, sql);
if(dataset!=null){
return dataset;
}else{
logger.info("parser failed, execute as SparkSession.sql");
return proceed(sql);
}
}catch (Exception e){
throw new RuntimeException(e);
}
}
public pointcut getCreateMethod(): execution(public org.apache.spark.sql.SparkSession getOrCreate());
SparkSession around(): getCreateMethod(){
SparkSession sparkSession = proceed();
String jarPath = sparkSession.sparkContext().getConf().get("spark.merge.jar.path");
if(StringUtils.isNotEmpty(jarPath)){
sparkSession.sparkContext().addJar(jarPath);
}
sparkSession.exprimental.extraStrategies = MergeTableStrategy :: Nil
return sparkSession;
}
}
上面的aop定義了兩個切面:第一個是在sparksession啓動時倒入自定義的strategy,第二個是在執行sql時進行攔截,首先嚐試用自定義的sql解析器進行解析,如果解析失敗則執行sparksql原生的方法,這樣的好處是不需要重複實現相關的sql業務邏輯,用戶只需要實現自定義的邏輯即可。
在工程的resource目錄下建立META-INF目錄,創建aop.xml文件:
<?xml version="1.0" encoding="UTF-8" ?>
<aspectj>
<aspects>
<aspect name="cn.tongdun.sparkparser.AspectSql"/>
</aspects>
<weaver options="-XaddSerialVersionUID"></weaver>
</aspectj>
最後對整個工程打包。
四 部署測試
接下來將實現的自定義sql解析器嵌入到sparksql中,我們需要兩個jar包,一個是自定義sql解析器sparkparser.jar,另一個就是aspectweaver.jar,首先將sparkparser.jar上傳到hdfs上的某個位置,同時將sparkparser.jar和aspectweaver.jar放到本地的某個路徑上,啓動spark-shell,後面加上以下參數:
bin/spark-shell
--conf spark.driver.extraJavaOptions=-javaagent:/home/admin/aspectjweaver-1.8.10.jar
--conf spark.driver.extraClassPath=/home/admin/sparkparser-1.0.0.jar
--conf spark.merge.jar.path=hdfs://tdhdfs/user/admin/sparkparser-1.0.0.jar
本例中 sparkparser-1.0.0.jar和aspectweave-1.8.10r.jar分別都放在本地的/home/admin目錄下,同時將sparkparser-1.0.0.jar上傳到hdfs的/user/admin/路徑下
啓動spark-shell之後,即可以執行merge table的相關命令
按照以上的步驟可以繼續開發一些定製化的sql語法,進一步滿足實際業務需求。