Spark SQL 之自定义删除外部表

前言

Spark SQL 在删除外部表时, 本不能删除外部表的数据的. 本篇文章主要介绍如何修改 Spark SQL 源码实现在删除外部表的时候, 可以带额外选项来删除外部表的数据.

本文的环境是我一直使用的 spark 2.4.3 版本.

1. 修改 ANTLR4 语法文件
修改 SqlBase.g4 文件中 drop Table 相关语句, 添加 (WITH DATA)?, 修改完之后如下:
DROP TABLE (IF EXISTS)? tableIdentifier (WITH DATA)? PURGE? #dropTable
因为, 删除 external 表也不是必须的, 所以添加 WITH DATA 为可选项, 跟 IF EXISTS 类似.
2. 修改相关方法
2.1 修改 SparkSqlParser.scala 文件/**
* Create a [[DropTableCommand]] command.
*/
overridedefvisitDropTable(ctx:DropTableContext):LogicalPlan=withOrigin(ctx){
DropTableCommand(
visitTableIdentifier(ctx.tableIdentifier),
ctx.EXISTS!=null,
ctx.VIEW!=null,
ctx.PURGE!=null,
ctx.WITH()!=null&&ctx.DATA()!=null)
}
2.2 修改 DropTableCommand.scala 等相关文件
首先修改构造函数, 在最后一个参数后面添加 withData 方法, 默认为 false:caseclassDropTableCommand(
tableName:TableIdentifier,
ifExists:Boolean,
isView:Boolean,
purge:Boolean,
withData:Boolean=false// TODO 外部表是否需要删除表数据
)extendsRunnableCommand
DropTableCommand 本质上其实是用了 command 设计模式, 实际在运行时, 会调用其 run 方法, 修改 run 方法, 如下:overridedefrun(sparkSession:SparkSession):Seq[Row]={
val catalog=sparkSession.sessionState.catalog
val isTempView=catalog.isTemporaryTable(tableName)
if(!isTempView&&catalog.tableExists(tableName)){
// If the command DROP VIEW is to drop a table or DROP TABLE is to drop a view
// issue an exception.
catalog.getTableMetadata(tableName).tableType match{
caseCatalogTableType.VIEWif!isView=>
thrownewAnalysisException(
"Cannot drop a view with DROP TABLE. Please use DROP VIEW instead")
caseoifo!=CatalogTableType.VIEW&&isView=>
thrownewAnalysisException(
s"Cannot drop a table with DROP VIEW. Please use DROP TABLE instead")
case_=>
}
}
if(isTempView||catalog.tableExists(tableName)){
try{
sparkSession.sharedState.cacheManager.uncacheQuery(
sparkSession.table(tableName),cascade=!isTempView)
}catch{
caseNonFatal(e)=>log.warn(e.toString,e)
}
catalog.refreshTable(tableName)
log.warn(s"withData:${withData}")
catalog.dropTable(tableName,ifExists,purge,withData)
}elseif(ifExists){
// no-op
}else{
thrownewAnalysisException(s"Table or view not found: ${tableName.identifier}")
}
Seq.empty[Row]
}

在第 28 行, 为 catalog 对象的 dropTable 添加 withData 参数. 其中 catalog 是 org.apache.spark.sql.catalyst.catalog.SessionCatalog 的实例. 其子类并没有重写其 dropTable 方法, 故只需要修改其 dropTable 方法即可. 具体修改代码如下:/**

* Drop a table.
*
* If a database is specified in `name`, this will drop the table from that database.
* If no database is specified, this will first attempt to drop a temporary view with
* the same name, then, if that does not exist, drop the table from the current database.
*/
defdropTable(
name:TableIdentifier,
ignoreIfNotExists:Boolean,
purge:Boolean,
withData:Boolean=false// 外部表是否需要在 hdfs 上删除其对应的数据
):Unit=synchronized{
val db=formatDatabaseName(name.database.getOrElse(currentDb))
val table=formatTableName(name.table)
if(db==globalTempViewManager.database){
val viewExists=globalTempViewManager.remove(table)
if(!viewExists&&!ignoreIfNotExists){
thrownewNoSuchTableException(globalTempViewManager.database,table)
}
}else{
if(name.database.isDefined||!tempViews.contains(table)){
requireDbExists(db)
// When ignoreIfNotExists is false, no exception is issued when the table does not exist.
// Instead, log it as an error message.
if(tableExists(TableIdentifier(table,Option(db)))){
logError(s"withData :${withData}")
externalCatalog.dropTable(db,table,ignoreIfNotExists=true,purge=purge,withData)
}elseif(!ignoreIfNotExists){
thrownewNoSuchTableException(db=db,table=table)
}
}else{
tempViews.remove(table)
}
}
}

为防止在 test 中有很多的测试类在调用该方法, 在编译时报错, 新添加的 withData 给默认值, 为 false, 保证该方法默认行为跟之前未修改前一致.

withData 参数继续传递给 externalCatalog.dropTable 方法, 其中, externalCatalog 是 org.apache.spark.sql.catalyst.catalog.ExternalCatalog 类型变量, ExternalCatalog 是一个 trait,ExternalCatalog 实现类关系如下:


首先修改 ExternalCatalog 的 dropTable 方法, 如下:defdropTable(

db:String,

table:String,

ignoreIfNotExists:Boolean,

purge:Boolean,

withData:Boolean=false):Unit

参数加载最后, 给默认值 false.

org.apache.spark.sql.catalyst.catalog.ExternalCatalogWithListener 是一个包装类, 其内部在原来 ExternalCatalog 的行为之外添加了监听的行为. 先修改这个包装类的 dropTable, 如下:overridedefdropTable(

db:String,
table:String,
ignoreIfNotExists:Boolean,
purge:Boolean,
withData:Boolean):Unit={
postToAll(DropTablePreEvent(db,table))
delegate.dropTable(db,table,ignoreIfNotExists,purge,withData)
postToAll(DropTableEvent(db,table))
}

其中, delegate 就是真正执行 dropTable 操作的 ExternalCatalog 对象.

catlog 有两个来源, 分别是 in-memory 和 hive, in-memory 的实现类是 org.apache.spark.sql.catalyst.catalog.InMemoryCatalog, 只需要添加 方法参数列表即可, 在方法内部不需要做任何操作.

hive 的实现类是 org.apache.spark.sql.hive.HiveExternalCatalog, 其 dropTable 方法如下:overridedefdropTable(

db:String,
table:String,
ignoreIfNotExists:Boolean,
purge:Boolean,
withData:Boolean):Unit=withClient{
requireDbExists(db)
val tableLocation:URI=client.getTable(db,table).location
client.dropTable(db,table,ignoreIfNotExists,purge)
val path:Path=newPath(tableLocation)
val fileSystem:FileSystem=FileSystem.get(hadoopConf)
val fileExists:Boolean=fileSystem.exists(path)
logWarning(s"withData:${withData}, ${path} exists : ${fileExists}")
if(withData&&fileExists){
fileSystem.delete(path,true)
}
}

3. 打包编译

在生产环境编译, 编译命令如下:

./dev/make-distribution.sh --name 2.6.0-cdh5.14.0 --tgz --mvn /opt/soft/apache-maven-3.6.1/bin/mvn -Pyarn -Phadoop-2.6 -Phive -Phive-thriftserver -Dhadoop.version=2.6.0-cdh5.14.0 -X

注: 由于编译的是 cdh 版本, 一些 jar 包不在中央仓库, 在 pom.xml 文件中, 添加 cloudera maven 源:

cloudera

https://repository.cloudera.com/artifactory/cloudera-repos

为了加快 maven 编译的速度, 在 make-distribution.sh 文件中, 修改了编译的并行度, 在 171 行, 把 1C 改为 4C, 具体修改如下:

BUILD_COMMAND=("$MVN" -T 4C clean package -DskipTests $@)

执行编译结束之后, 在项目的根目录下, 会有 spark-2.4.3-bin-2.6.0-cdh5.14.0.tgz 这个压缩包, 这就是 binary 文件, 可以解压到指定目录进行相应配置了.

4. 配置 spark

把原来集群中 spark 的配置以及相关 jar 包拷贝到新的 spark 相应目录.

5. 测试

5.1 创建外部表spark sql

spark-sql>usetest;
spark-sql>create external table ext1 location'/user/hive/warehouse/test.db/ext1'asselect*fromperson;
spark-sql>select*fromext1;
123
2zhangsan4
3lisi5
4wangwu6
5rose7
6nose8
7info9
8test10

查看 hdfs 上对应目录是否有数据[root@xxx~]#hdfs dfs-ls-R/user/hive/warehouse/test.db/ext1

-rwxr-xr-x3root supergroup762020-02-2715:58/user/hive/warehouse/test.db/ext1/part-00000-aae237ac-4a0b-425c-a0f1-5d54d1e88957-c000

5.2 删除表

spark-sql> drop table if exists ext1 with data;

5.3 验证表元数据已删除成功spark-sql>show tables;

test personfalse

没有 ext 表, 说明已删除成功.

5.4 验证 hdfs 上数据已删除成功[root@xxx~]#hdfs dfs-ls-R/user/hive/warehouse/test.db/ext1

ls:`/user/hive/warehouse/test.db/ext1': No such file or directory

该目录已不存在, 说明 hdfs 上数据已删除成功.

总结

本文具体介绍了如何修改 spark sql 的源码, 在删除 external 表时可选择地删除 hdfs 上的底层数据.