自定義SparkSql語法的一般步驟

        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語法,進一步滿足實際業務需求。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章