Spark lazy list files 的实现

背景

对于 Spark partition table, 在生成 HadoopFsRelation 时,如果 partitionKeyFilters 或者 subqueryFilters 非空的时候,HadoopFsRelation 的 location: FileIndex 属性为 LazyFileIndex, 在最终 FileSourceScanExec 调用 listFiles 之前才将 LazyFileIndex 转换成 InMemoryFileIndex。
但是如果 Spark partition table 的 partition filter 含有 subquery, 此时 Spark 认为无法下推,所以会跳过使用 LazyFileIndex, 在 listFiles 时 prunePartitions 也不会过滤掉任务分区,导致做了很多没用的操作。

如果分区过滤条件是 subquery, 默认会把所有的分区全部拿回来,然后再进行分区过滤。

生成 HadoopFsRelation

在 DataSourceStrategy 的 FindDataSourceTable Rule 会尝试解析 'UnresolvedCatalogRelation 中的 CatalogTable。在 DataSource.resolveRelation() 方法尝试将 table Node 转换成 HadoopFsRelation。 如果 DataSource 传入的 table 是分区表,fileCatalog 使用 CatalogFileIndex 以便于后面分区裁剪。否则,直接遍历访问DataSource 中传入的Paths, 生成 InMemoryFileIndex

// DataSource.resolveRelation()
      case (format: FileFormat, _) =>
        val useCatalogFileIndex = sparkSession.sqlContext.conf.manageFilesourcePartitions &&
          catalogTable.isDefined && catalogTable.get.tracksPartitionsInCatalog &&
          catalogTable.get.partitionColumnNames.nonEmpty
        val (fileCatalog, dataSchema, partitionSchema) = if (useCatalogFileIndex) {
          val defaultTableSize = sparkSession.sessionState.conf.defaultSizeInBytes
          val index = new CatalogFileIndex(
            sparkSession,
            catalogTable.get,
            catalogTable.get.stats.map(_.sizeInBytes.toLong).getOrElse(defaultTableSize))
          (index, catalogTable.get.dataSchema, catalogTable.get.partitionSchema)
        } else {
          val globbedPaths = checkAndGlobPathIfNecessary(
            checkEmptyGlobPath = true, checkFilesExist = checkFilesExist)
          val index = createInMemoryFileIndex(globbedPaths)
          val (resultDataSchema, resultPartitionSchema) =
            getOrInferFileFormatSchema(format, () => index)
          (index, resultDataSchema, resultPartitionSchema)
        }

        HadoopFsRelation(
          fileCatalog,
          partitionSchema = partitionSchema,
          dataSchema = dataSchema.asNullable,
          bucketSpec = bucketSpec,
          format,
          caseInsensitiveOptions)(sparkSession)

对 HadoopFsRelation 进行分区裁剪

  • 社区版本 code 在 PruneFileSourcePartitions rule 中对 HadoopFsRelation 进行分区裁剪。
  • 因为分区表生成的是 CatalogFileIndex, 通过 Plan 中的带有分区字段的过滤条件, 用来进行分区裁剪 val prunedFileIndex = catalogFileIndex.filterPartitions(partitionKeyFilters)
  • 如果过滤条件中含有 subquery, 这种过滤条件不能下推到 Hive metastore,导致返回非常多 partitions(对应多个rootPaths)。
  • 将裁减后的分区条件包装成 InMemoryFileIndex 。InMemoryFileIndex 实例化的时候执行 refresh0() 方法,获取 rootPaths 下所有的文件信息。因为 files 过多会导致 plan 解析变慢,同时占用大量 Driver 内存。
// InMemoryFileIndex
  private def refresh0(): Unit = {
    val files = listLeafFiles(rootPaths)
    cachedLeafFiles =
      new mutable.LinkedHashMap[Path, FileStatus]() ++= files.map(f => f.getPath -> f)
    cachedLeafDirToChildrenFiles = files.toArray.groupBy(_.getPath.getParent)
    cachedPartitionSpec = null
  }

LazyFileIndex 优化

  • Carmel 版本进行了优化,CatalogFileIndex.lazyFilterPartitions(filters: Seq[Expression]) 返回的是 PartitioningAwareFileIndex 子类 LazyFileIndex ,这个类不会发生实际的 HDFS 遍历。
  • 在 FileSourceStrategy 中会将 HadoopFsRelation 转换为 FileSourceScanExec 。在 FileSourceScanExec 中有三个 lazy 变量: selectedPartitions, dynamicallySelectedPartitions, inputRDD
    • selectedPartitions : 通过 InMemoryFileIndex.listFiles() 返回选中的分区
    • dynamicallySelectedPartitions : 使用不能下推的分区过滤条件对 selectedPartitions 再过滤一次
    • inputRDD : 处理最终确定的分区,生成 FileScanRDD
  • 我们在访问 selectedPartitions 的时候,自动将 LazyFileIndex 替换成 InMemoryFileIndex 并进行 HDFS 遍历。

FileIndex 类继承关系

FileIndex
    CatalogFileIndex
        def lazyFilterPartitions(filters: Seq[Expression]): PartitioningAwareFileIndex

    PartitioningAwareFileIndex
        LazyFileIndex
            def createFileIndex(predicates: Seq[Expression]): InMemoryFileIndex

        InMemoryFileIndex
            def listFiles(partitionFilters: Seq[Expression], dataFilters: Seq[Expression]): Seq[PartitionDirectory]

        MetadataLogFileIndex // 非重点,先忽略

一些讨论

  • 优化点是对文件目录的遍历放到了 plan analyze 之后, 需关注文件在 hdfs 上的遍历耗时,还有遍历的文件结果内存占用。
  • TODO …
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值