SPARK中 DS V2 push down(下推)的一些说明

简介: SPARK中 DS V2 push down(下推)的一些说明

背景

本文基于 SPARK 3.3.0

在之前的文章 SPARK中的FileSourceStrategy,DataSourceStrategy以及DataSourceV2Strategy规则 我们有提到 DS V2 push down的功能,如JDBC 复杂下推,以及Parquet的聚合下推等等。其实这里面有个比较大的背景–就是TableCatalog类。


结论

先说结论,这些聚合下推的大前提是,在spark中已经配置了对应的catalog,如下:


spark.sql.catalog.h2=org.apache.spark.sql.execution.datasources.v2.jdbc.JDBCTableCatalog


分析

在Rule V2ScanRelationPushDown一系列的规则中,第一个规则createScanBuilder:

private def createScanBuilder(plan: LogicalPlan) = plan.transform {
    case r: DataSourceV2Relation =>
      ScanBuilderHolder(r.output, r, r.table.asReadable.newScanBuilder(r.options))
  }

只有是DataSourceV2Relation类型,也就是 DS v2,才会转换为 ScanBuilderHolder, 而后续的pushDownFilters,pushDownAggregates规则则是基于ScanBuilderHolder来做转换的(如果有遇到ScanBuilderHolder类型才会进行DS v2特有的规则转换),所以DataSourceV2Relation是从哪里来的是关键。

直接说重点:

在RULE ResolveRelations中会进行 UnresolvedRelation到DataSourceV2Relation或是UnresolvedCatalogRelation的转换:

object ResolveRelations extends Rule[LogicalPlan] {
  ...
def apply(plan: LogicalPlan)
        : LogicalPlan = plan.resolveOperatorsUpWithPruning(AlwaysProcess.fn, ruleId) {
      case i @ InsertIntoStatement(table, _, _, _, _, _) if i.query.resolved =>
        val relation = table match {
          case u: UnresolvedRelation if !u.isStreaming =>
            lookupRelation(u).getOrElse(u)
          case other => other
        }

这里的lookupRelation会根据是否有对应的Catalog的注册来判断是DS V1还是DS V2:


private def lookupRelation(
        u: UnresolvedRelation,
        timeTravelSpec: Option[TimeTravelSpec] = None): Option[LogicalPlan] = {
      lookupTempView(u.multipartIdentifier, u.isStreaming, timeTravelSpec.isDefined).orElse {
        expandIdentifier(u.multipartIdentifier) match {
          case CatalogAndIdentifier(catalog, ident) =>
            val key = catalog.name +: ident.namespace :+ ident.name
            AnalysisContext.get.relationCache.get(key).map(_.transform {
              case multi: MultiInstanceRelation =>
                val newRelation = multi.newInstance()
                newRelation.copyTagsFrom(multi)
                newRelation
            }).orElse {
              val table = CatalogV2Util.loadTable(catalog, ident, timeTravelSpec)
              val loaded = createRelation(catalog, ident, table, u.options, u.isStreaming)
              loaded.foreach(AnalysisContext.get.relationCache.update(key, _))
              loaded
            }
          case _ => None
        }
      }
    }
    ...
private def expandIdentifier(nameParts: Seq[String]): Seq[String] = {
    if (!isResolvingView || isReferredTempViewName(nameParts)) return nameParts
    if (nameParts.length == 1) {
      AnalysisContext.get.catalogAndNamespace :+ nameParts.head
    } else if (catalogManager.isCatalogRegistered(nameParts.head)) {
      nameParts
    } else {
      AnalysisContext.get.catalogAndNamespace.head +: nameParts
    }
  }
object CatalogAndIdentifier {
    import org.apache.spark.sql.connector.catalog.CatalogV2Implicits.MultipartIdentifierHelper
    private val globalTempDB = SQLConf.get.getConf(StaticSQLConf.GLOBAL_TEMP_DATABASE)
    def unapply(nameParts: Seq[String]): Option[(CatalogPlugin, Identifier)] = {
      assert(nameParts.nonEmpty)
      if (nameParts.length == 1) {
        Some((currentCatalog, Identifier.of(catalogManager.currentNamespace, nameParts.head)))
      } else if (nameParts.head.equalsIgnoreCase(globalTempDB)) {
        // Conceptually global temp views are in a special reserved catalog. However, the v2 catalog
        // API does not support view yet, and we have to use v1 commands to deal with global temp
        // views. To simplify the implementation, we put global temp views in a special namespace
        // in the session catalog. The special namespace has higher priority during name resolution.
        // For example, if the name of a custom catalog is the same with `GLOBAL_TEMP_DATABASE`,
        // this custom catalog can't be accessed.
        Some((catalogManager.v2SessionCatalog, nameParts.asIdentifier))
      } else {
        try {
          Some((catalogManager.catalog(nameParts.head), nameParts.tail.asIdentifier))
        } catch {
          case _: CatalogNotFoundException =>
            Some((currentCatalog, nameParts.asIdentifier))
        }
      }
    }
  }


expandIdentifier方法结合CatalogAndIdentifier.unapply方法,判断:


1.如果没有指定catalog,则 默认catalog 为v2SessionCatalog,catalog的名称为"spark_catalog",这也是spark默认的sessionCatalog 名称,跳到步骤3

如以下SQL: select a from table

2.如果指定了catalog,且catalog已经注册了(如以spark.sql.catalog.h2=org.apache.spark.sql.execution.datasources.v2.jdbc.JDBCTableCatalog),则catalog为指定的(如为JDBCTableCatalog,catalog的名称为"h2",跳到步骤3

如以下SQL:select a from h2.table

3.调用CatalogV2Util.loadTable方法也就是对应的Catalog的loadTable方法来获取对应的Table:


V2SessionCatalog catalog返回是的V1Table

JDBCTableCatalog catalog 返回的是JDBCTable


这样在下一步的createRelation 方法中就会根据不同的case转换为不同的relation:


private def createRelation(
        catalog: CatalogPlugin,
        ident: Identifier,
        table: Option[Table],
        options: CaseInsensitiveStringMap,
        isStreaming: Boolean): Option[LogicalPlan] = {
      ...
      case v1Table: V1Table if CatalogV2Util.isSessionCatalog(catalog) =>
          if (isStreaming) {
            if (v1Table.v1Table.tableType == CatalogTableType.VIEW) {
              throw QueryCompilationErrors.permanentViewNotSupportedByStreamingReadingAPIError(
                ident.quoted)
            }
            SubqueryAlias(
              catalog.name +: ident.asMultipartIdentifier,
              UnresolvedCatalogRelation(v1Table.v1Table, options, isStreaming = true))
          } else {
            v1SessionCatalog.getRelation(v1Table.v1Table, options)
          }
      ...
      case table =>
        ...
         } else {
            SubqueryAlias(
              catalog.name +: ident.asMultipartIdentifier,
              DataSourceV2Relation.create(table, Some(catalog), Some(ident), options))


如果是V1Table,则会转换为UnresolvedCatalogRelation,继而在 Rule FindDataSourceTable中转为LogicalRelation,这里就会涉及lookupDataSource,也就是注册的datasource(如:“org.apache.spark.sql.execution.datasources.jdbc.JdbcRelationProvider” 或者 "org.apache.spark.sql.execution.datasources.v2.parquet.ParquetDataSourceV2”(目前没有进行cast匹配))发生作用了(在providingInstance()方法中实现)

如果是其他的,则会转换为DataSourceV2Relation,继而在Rule V2ScanRelationPushDown中做一系列的下推优化


所以说 对于JDBC的catalog来说,想要进行DS V2的优化,就得配置:

spark.sql.catalog.h2=org.apache.spark.sql.execution.datasources.v2.jdbc.JDBCTableCatalog

如果想要对于其他DS v2的优化,如Parquet,就得实现对应的TableCatalog,再进行配置:


spark.sql.catalog.parquet=org.apache.spark.sql.execution.datasources.v2.jdbc.xxxx

关于TableCatalog


目前 jdbc的datasource和TableCatalog 在spark都是已经实现了:

## datasource
org.apache.spark.sql.execution.datasources.jdbc.JdbcRelationProvider
## TableCatalog
org.apache.spark.sql.execution.datasources.v2.jdbc.JDBCTableCatalog

如果想实现其他的datasource以及catalog,可以参考JDBC的实现(目前的JDBC的source实现还是基于 DS V1,最好是基于DS V2实现,如参考:ParquetDataSourceV2)。


在SPARK-28396也有这方面的讨论。

更进一步DS V2 Push Down的特性,参考技术前沿|Spark 3.3.0 中 DS V2 Push-down 的重构与新特性

相关文章
|
SQL 分布式计算 大数据
SparkSQL DatasourceV2 之 Multiple Catalog
SparkSQL DatasourceV2作为Spark2.3引入的特性,在Spark 3.0 preview(2019/12/23)版本中又有了新的改进以更好的支持各类数据源。本文将从catalog角度,介绍新的数据源如何和Spark DatasourceV2进行集成。
SparkSQL DatasourceV2 之 Multiple Catalog
|
SQL 分布式计算 数据库
【大数据技术Spark】Spark SQL操作Dataframe、读写MySQL、Hive数据库实战(附源码)
【大数据技术Spark】Spark SQL操作Dataframe、读写MySQL、Hive数据库实战(附源码)
726 0
|
分布式计算 资源调度 Hadoop
十二、Spark的安装与部署详情(Local模式,Standalone模式,Spank on YARN模式)
十二、Spark的安装与部署详情(Local模式,Standalone模式,Spank on YARN模式)
1588 0
十二、Spark的安装与部署详情(Local模式,Standalone模式,Spank on YARN模式)
|
SQL 分布式计算 测试技术
扩展Spark Catalyst,打造自定义的Spark SQL引擎
在Spark2.2版本中,引入了新的扩展点,使得用户可以在Spark session中自定义自己的parser,analyzer,optimizer以及physical planning stragegy rule。
4675 0
|
SQL 缓存 分布式计算
SparkSQL与Hive metastore Parquet转换
Spark SQL为了更好的性能,在读写Hive metastore parquet格式的表时,会默认使用自己的Parquet SerDe,而不是采用Hive的SerDe进行序列化和反序列化
SparkSQL与Hive metastore Parquet转换
|
5月前
|
分布式计算 Java 流计算
Fluss on 鲲鹏 openEuler 大数据实战
本文介绍了基于华为鲲鹏ARM架构服务器与openEuler操作系统,构建包含HDFS、ZooKeeper、Flink、Fluss及Paimon的实时大数据环境的完整实战过程。涵盖了软硬件配置、组件部署、集群规划、环境变量设置、安全认证及启停脚本编写等内容,适用于企业级实时数据平台搭建与运维场景。
713 0
Fluss on 鲲鹏 openEuler 大数据实战
|
12月前
|
存储 SQL 缓存
Flink 2.0 存算分离状态存储 — ForSt DB 
本文整理自阿里云技术专家兰兆千在Flink Forward Asia 2024上的分享,主要介绍Flink 2.0的存算分离架构、全新状态存储内核ForSt DB及工作进展与未来展望。Flink 2.0通过存算分离解决了本地磁盘瓶颈、检查点资源尖峰和作业恢复速度慢等问题,提升了云原生部署能力。ForSt DB作为嵌入式Key-value存储内核,支持远端读写、批量并发优化和快速检查点等功能。性能测试表明,ForSt在异步访问和本地缓存支持下表现卓越。未来,Flink将继续完善SQL Operator的异步优化,并引入更多流特性支持。
1188 88
Flink 2.0 存算分离状态存储 — ForSt DB 
|
存储 消息中间件 分布式计算
某全球领先网络解决方案提供商:统一 Trino、Pinot、Iceberg 及 Kyuubi,探索 Apache Doris 的改造实践
某全球领先网络解决方案提供商早期架构面临架构复杂、数据冗余存储、运维困难、资源利用率低、数据时效性差等问题。因此,引入 Apache Doris 替换了 Trino、Pinot 、 Iceberg 及 Kyuubi 技术栈,依赖于 Doris 的实时数据湖能力及高性能 OLAP 分析能力,统一数据湖仓及查询分析引擎,显著提升了查询性能及系统稳定性,同时实现资源成本降低 30%。
374 8
某全球领先网络解决方案提供商:统一 Trino、Pinot、Iceberg 及 Kyuubi,探索 Apache Doris 的改造实践
|
消息中间件 分布式计算 大数据
大数据-128 - Flink 并行度设置 细节详解 全局、作业、算子、Slot
大数据-128 - Flink 并行度设置 细节详解 全局、作业、算子、Slot
924 0

热门文章

最新文章