Apache Hudi初探(十一)(与spark的结合)--hudi的markers机制

文章详细阐述了Hudi在使用SparkDataSourceV2时,为何在Compaction操作中会有deleteMarker操作。这涉及到Spark任务的执行流程,包括DataWritingSparkTask的run方法和WriterCommitMessage的处理。Hudi为了避免数据重复和提高写入效率,直接将数据写入目标目录并同时创建Marker文件。在作业完成后,根据Marker文件清理无效数据,并最终删除Marker目录。

背景

在之前的文章中hudi的Compaction操作中,completeTableService中其实会有deleteMarker的操作,那为什么会有这个操作呢?

分析

为什么会存在Marker文件

这得从Spark DataSource V2说起,引入了DataSource V2以后,hudi的写入文件主要就是V2TableWriteExec类:

  sparkContext.runJob(
    rdd,
    (context: TaskContext, iter: Iterator[InternalRow]) =>
      DataWritingSparkTask.run(writerFactory, context, iter, useCommitCoordinator),
    rdd.partitions.indices,
    (index, result: DataWritingSparkTaskResult) => {
      val commitMessage = result.writerCommitMessage
      messages(index) = commitMessage
      totalNumRowsAccumulator.add(result.numRows)
      batchWrite.onDataWriterCommit(commitMessage)
    }
  )

DataWritingSparkTask.run方法如下:

      while (iter.hasNext) {
        // Count is here.
        count += 1
        dataWriter.write(iter.next())
      }

      val msg = if (useCommitCoordinator) {
        val coordinator = SparkEnv.get.outputCommitCoordinator
        val commitAuthorized = coordinator.canCommit(stageId, stageAttempt, partId, attemptId)
        if (commitAuthorized) {
          logInfo(s"Commit authorized for partition $partId (task $taskId, attempt $attemptId, " +
            s"stage $stageId.$stageAttempt)")
          dataWriter.commit()
        } else {
          val message = s"Commit denied for partition $partId (task $taskId, attempt $attemptId, " +
            s"stage $stageId.$stageAttempt)"
          logInfo(message)
          // throwing CommitDeniedException will trigger the catch block for abort
          throw new CommitDeniedException(message, stageId, partId, attemptId)
        }

      } else {
        logInfo(s"Writer for partition ${context.partitionId()} is committing.")
        dataWriter.commit()
      }

之前的文章也说过,主要的就是以下三重曲:

  1. dataWriter.write
  2. dataWriter.commit/abort
  3. dataWriter.close

这就不得不提到dataWriter这个变量,在Spark原生的类中,该dataWriter对应的为SingleDirectoryDataWriter或者DynamicPartitionDataWriter
看这两个类的构造方法会有一个FileCommitProtocol类型的commiter,这个commiter,在以上write/commit/close等操作中扮演着重要的作用:
也就是说在task.write的时候,会先创建临时目录,
之后在task.commit的时候会把临时目录的文件真正的移到需要写入的目录下
那反观一下在hudi中,该dataWriter对应的是HoodieBulkInsertDataInternalWriter

this.bulkInsertWriterHelper = new BulkInsertDataInternalWriterHelper(hoodieTable,
        writeConfig, instantTime, taskPartitionId, taskId, 0, structType, populateMetaFields, arePartitionRecordsSorted);
  
@Override
  public void write(InternalRow record) throws IOException {
    bulkInsertWriterHelper.write(record);
  }

  @Override
  public WriterCommitMessage commit() throws IOException {
    return new HoodieWriterCommitMessage(bulkInsertWriterHelper.getWriteStatuses());
  }

真正进行写操作的是BulkInsertDataInternalWriterHelper,该类的写操作就是直接写真正需要写入的目录,而不是临时目录
那为什么这么做呢?这么做的优点和缺点是什么?
优点: 写数据直接写入目的目录,不需要二次拷贝,提高写入的效率
缺点: 如果spark存在speculative的情况下,会存在相同的数据写入到不同的文件中,造成数据重复不准确
所以说hudi引入了Markers的机制

marker文件什么时候被创建

在写入真正文件的同时,会在 .hoodie/.temp/instantTime目录下创建maker文件,比如.hoodie/.temp/202307237055/f1.parquet.marker.CREATE,
具体的写入marker文件的在HoodieRowCreateHandle的构造方法中:

HoodiePartitionMetadata partitionMetadata =
          new HoodiePartitionMetadata(
              fs,
              instantTime,
              new Path(writeConfig.getBasePath()),
              FSUtils.getPartitionPath(writeConfig.getBasePath(), partitionPath),
              table.getPartitionMetafileFormat());
      partitionMetadata.trySave(taskPartitionId);

      createMarkerFile(partitionPath, fileName, instantTime, table, writeConfig);

该HoodieRowCreateHandle会在BulkInsertDataInternalWriterHelper.write的方法中被调用。

无效数据文件什么时候被清理

因为存在了marker文件,所以在写入完后需要清理无效的数据文件(会在job运行完清理),该清理在V2TableWriteExec中的batchWrite.commit方法中,也就是HoodieDataSourceInternalBatchWrite.commit:

@Override
  public void commit(WriterCommitMessage[] messages) {
    List<HoodieWriteStat> writeStatList = Arrays.stream(messages).map(m -> (HoodieWriterCommitMessage) m)
        .flatMap(m -> m.getWriteStatuses().stream().map(HoodieInternalWriteStatus::getStat)).collect(Collectors.toList());
    dataSourceInternalWriterHelper.commit(writeStatList);
  }

数据流如下:

HoodieDataSourceInternalBatchWrite.commit
      ||
      \/
dataSourceInternalWriterHelper.commit
      ||
      \/
SparkRDDWriteClient.commitStats
      ||
      \/

SparkRDDWriteClient.commit
      ||
      \/

SparkRDDWriteClient.finalizeWrite
      ||
      \/

HoodieTable.finalizeWrite
      ||
      \/

HoodieTable.reconcileAgainstMarkers
      ||
      \/

HoodieTable.getInvalidDataPaths
      ||
      \/

markers.createdAndMergedDataPaths

reconcileAgainstMarkers方法中会根据marker文件删除无效的数据文件
注意一点
虽然说在Executor端写入了多个重复数据的文件,但是因为在只有一个真正的文件会被Driver认可,所以通过最终返回的被driver认可的文件和marker文件求交集就能删除掉其他废弃的文件。具体的和driver交互是否能被认可的代码在DataWritingSparkTask中:

// useCommitCoordinator 默认都是true
  val msg = if (useCommitCoordinator) {
  val coordinator = SparkEnv.get.outputCommitCoordinator
  val commitAuthorized = coordinator.canCommit(stageId, stageAttempt, partId, attemptId)
  if (commitAuthorized) {
    logInfo(s"Commit authorized for partition $partId (task $taskId, attempt $attemptId, " +
      s"stage $stageId.$stageAttempt)")
    dataWriter.commit()
       

makers目录什么时候被清理

一个job完成以后,我们可以得到真正的写入的文件,这个时候,Marker目录的意义就没有多大了,所以得进行清除
marker被清理的调用链有很多,比如说SparkRDDWriteClient.commitStats中就有清理:

SparkRDDWriteClient.commitStats

      ||
      \/
SparkRDDWriteClient.postCommit

      ||
      \/
WriteMarkers.quietDeleteMarkerDir

quietDeleteMarkerDir就会直接删除marker目录

更多关于hudi marker的问题,可以参考Apache Hudi内核之文件标记机制深入解析

<think>好的,用户想要下载特定版本的Hudi Spark bundle JAR包,具体是hudi-spark3.1-bundle_2.12-0.12.0.jar。我需要根据提供的引用信息来指导他如何找到或下载这个文件。 首先,看用户提供的引用内容。引用[1]和[4]提到了编译命令,其中使用了-Dspark3.1和-Dscala-2.12参数,这可能意味着Hudi的构建参数需要指定Spark和Scala的版本。但用户现在需要的是直接下载JAR包,而不是从源码编译。不过这些参数可以帮助确定正确的版本组合。 引用[2]和[3]是Maven仓库的URL示例。比如,引用[2]指向hudi-flink1.15-bundle的0.12.1版本,而引用[3]是Flink Hive连接器的例子。这些例子展示了Maven仓库的结构,用户可能需要按照类似的路径找到Spark bundle的JAR。 根据Maven仓库的通用结构,HudiSpark bundle包应该位于org/apache/hudi目录下。具体路径可能为:org/apache/hudi/hudi-spark3.1-bundle_2.12/0.12.0/,其中版本号是0.12.0。因此,完整的URL可能类似于: https://repo.maven.apache.org/maven2/org/apache/hudi/hudi-spark3.1-bundle_2.12/0.12.0/hudi-spark3.1-bundle_2.12-0.12.0.jar 需要确认这个URL是否正确。检查用户提供的引用,发现引用[2]中的hudi-flink1.15-bundle的路径结构是org/apache/hudi/hudi-flink1.15-bundle/0.12.1/,因此推断Spark bundle的路径应该类似,替换对应的模块名和版本即可。 另外,用户可能没有找到0.12.0版本,因为引用[1]和[4]中的版本是0.12.1,但用户需要的是0.12.0。需要确认该版本是否存在。在Maven仓库中,可以访问https://repo.maven.apache.org/maven2/org/apache/hudi/hudi-spark3.1-bundle_2.12/查看是否有0.12.0目录。如果存在,用户就可以下载对应的JAR。 如果用户无法通过直接访问URL下载,可能需要使用Maven或Gradle依赖配置,或者在构建工具中添加对应的仓库地址。但根据用户的问题,直接下载JAR包更直接,所以提供具体的URL路径和步骤是关键。 总结步骤: 1. 访问Maven中央仓库的Hudi Spark bundle路径。 2. 检查是否存在0.12.0版本。 3. 提供直接下载链接,如果存在的话。 4. 如果不存在,建议用户检查版本号是否正确,或考虑使用其他版本。 可能的问题:用户提供的版本是否在Maven仓库中可用?需要确认0.12.0是否被发布。例如,引用中提到的0.12.1存在,但0.12.0可能也存在,需要检查。</think>### 分步解答 要下载 `hudi-spark3.1-bundle_2.12-0.12.0.jar`,需通过以下步骤操作: 1. **确认Maven仓库路径** Apache Hudi的官方组件在Maven中央仓库的路径为: `org/apache/hudi/{模块名}/{版本号}` 根据文件名拆解: - 模块名:`hudi-spark3.1-bundle_2.12` - 版本号:`0.12.0` 完整路径应为: $$https://repo.maven.apache.org/maven2/org/apache/hudi/hudi-spark3.1-bundle_2.12/0.12.0/$$ 2. **直接访问JAR文件** 在路径末尾添加文件名即可下载: ```text https://repo.maven.apache.org/maven2/org/apache/hudi/hudi-spark3.1-bundle_2.12/0.12.0/hudi-spark3.1-bundle_2.12-0.12.0.jar ``` 若链接有效,浏览器会自动下载文件[^2]。 3. **验证版本可用性** 若链接返回404错误,说明该版本可能未发布或路径有误。可检查: - 版本号是否存在:访问父目录 $$https://repo.maven.apache.org/maven2/org/apache/hudi/hudi-spark3.1-bundle_2.12/$$ 查看是否有`0.12.0`文件夹。 - 确认命名规范是否Hudi官方一致(例如Spark 3.1Scala 2.12的组合是否支持)[^4]。 4. **替代下载方式** 若需通过构建工具下载,在`pom.xml`中添加依赖: ```xml <dependency> <groupId>org.apache.hudi</groupId> <artifactId>hudi-spark3.1-bundle_2.12</artifactId> <version>0.12.0</version> </dependency> ``` 执行 `mvn clean install` 自动从仓库拉取[^1]。 --- ### 关键提示 - **版本兼容性**:Hudi Bundle的版本需Spark、Scala版本严格匹配(例如`spark3.1`对应Spark 3.1.x,`_2.12`对应Scala 2.12)[^4]。 - **镜像仓库**:若中央仓库下载慢,可改用国内镜像(如阿里云Maven仓库)。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值