Hudi事务机制

随着大数据业务的快速发展,大数据架构也在不断演进,当前业内主推的是数据湖技术栈与数据仓库(MPP)相整合的湖仓一体架构,该架构可以兼顾海量结构化/非结构化的数据存储、数据加工和分析(离线/实时/交互式)的业务需求。

Hudi作为数据湖的典型实现,最初主要是为了解决业务数据经常需要修改、而传统数仓的更新效率低下的问题而设计的,是位于计算引擎(Spark/Flink/Presto等)和大数据文件系统(HDFS/S3)之间的一个存储引擎层,可见高效的事务操作是Hudi的一个重要特性。

前面的文章中我们讲解了时间线Timeline的原理,时间线是一系列Hudi表的操作记录,同时Timeline也是Hudi实现事务机制的基础。

下面从事务的角度来介绍Hudi表的一次数据变更的基本过程,并说明Hudi是如何以时间线为基础来实现表操作的事务特性的:

1、预写时间线:

初始化Hudi表相关对象,然后在时间线上创建一个REQUESTED类型的时间线实例,并在持久化为一个.hoodie目录下以.requested结尾的文件。

2、分析待操作的数据记录:

1)根据记录的主键和数据表的索引信息获取本次变更所涉及的记录所在的分区、文件组和文件片等存储地址信息,可避免file listing操作所带来的性能问题;

2)将时间线上本次操作的Instant实例的状态改为INFLIGHT,并在.hoodie目录中写入一个.inflight结尾的文件。

3、执行数据写入(持久性):

接下来根据记录的操作类型和记录所在的文件组将待操作的记录分为多个bucket,并根据bucket将记录的并发写入磁盘,这里体现了Hudi表事务操作的持久性;如果写入过程发生异常,则本次事件则停留在INFLIGHT状态。

当所有记录均写入成功后更新Hudi表的索引信息,索引是Hudi表实现快速读写的重要依据,也是Hudi与传统数仓的一个区别,因此记录数据写入完成后会执行更新索引的操作:

 protected List<WriteStatus> updateIndex(List<WriteStatus> writeStatuses, HoodieWriteMetadata<List<WriteStatus>> result) {
    Instant indexStartTime = Instant.now();
    // Update the index back
    List<WriteStatus> statuses = HoodieList.getList(
        table.getIndex().updateLocation(HoodieList.of(writeStatuses), context, table));
    result.setIndexUpdateDuration(Duration.between(indexStartTime, Instant.now()));
    result.setWriteStatuses(statuses);
    return statuses;
  }

4、提交事务(隔离性):

接下来开始执行事务的commit操作,首先检测是否有写入冲突,如果发现写入冲突,例如当前表正在执行clustering操作,则本次操作异常返回;如果没有并发冲突,则将本次事件的instant状态改为COMPLETED并持久到时间线目录中,产生一个以.commit结尾的文件。

在Timeline的章节中我们提到过,在执行commit操作后,Hudi中会将此次写入的文件片信息同步到Hudi表的文件系统对象中的哈希表partitionToFileGroupsMap中,接下来当读取该表的数据时应用就可以获取到相应文件组中的最新的FileSlice的数据;另外,如果发生了写入失败的情况则不会继续执行commit操作,本次写入的文件片的存储信息将不会更新到表视图对象中,即这些异常的文件片对读写客户端将是不可见的,因此Hudi事务操作隔离性的体现。

5、事务回滚(原子性,一致性):

在Hudi表的写入过程中,如果发生异常或者检测到并发冲突的问题,则本次的写入操作将需要执行回滚,这里体现了Hudi表事务操作的原子性。

由于Hudi表事务隔离性的保障,当发生写入异常后无需立即执行回滚,而是在下次执行写入操作时根据异常事件的清理策略来获取需要进行回滚的instant。

1)获取待回滚的Instant:

protected List<String> getInstantsToRollback(HoodieTableMetaClient metaClient, HoodieFailedWritesCleaningPolicy cleaningPolicy, Option<String> curInstantTime) {
    Stream<HoodieInstant> inflightInstantsStream = getInflightTimelineExcludeCompactionAndClustering(metaClient)
        .getReverseOrderedInstants();
    if (cleaningPolicy.isEager()) {
      return inflightInstantsStream.map(HoodieInstant::getTimestamp).filter(entry -> {
        if (curInstantTime.isPresent()) {
          return !entry.equals(curInstantTime.get());
        } else {
          return true;
        }
      }).collect(Collectors.toList());
    } else if (cleaningPolicy.isLazy()) {
      return inflightInstantsStream.filter(instant -> {
        try {
          return heartbeatClient.isHeartbeatExpired(instant.getTimestamp());
        } catch (IOException io) {
          throw new HoodieException("Failed to check heartbeat for instant " + instant, io);
        }
      }).map(HoodieInstant::getTimestamp).collect(Collectors.toList());
    } else if (cleaningPolicy.isNever()) {
      return Collections.EMPTY_LIST;
    } else {
      throw new IllegalArgumentException("Invalid Failed Writes Cleaning Policy " + config.getFailedWritesCleanPolicy());
}
...
public enum HoodieFailedWritesCleaningPolicy {
  // performs cleaning of failed writes inline every write operation
  EAGER,
  // performs cleaning of failed writes lazily during clean
  LAZY,
  // Does not clean failed writes
  NEVER;
...
//获取未执行完成的且非压缩操作的instant
public HoodieTimeline filterPendingExcludingCompaction() {
    return new HoodieDefaultTimeline(instants.stream().filter(instant -> (!instant.isCompleted())
            && (!instant.getAction().equals(HoodieTimeline.COMPACTION_ACTION))), details);
}
  • 如果失败instant清理策略为EAGER(默认值):则将所有未完成的instant标记为待回滚事件;
  • 如果清理策略为LAZY:该策略一般为并发写入场景下,如果检测到了未完成的instant并且该instant的心跳已超时,则标记该instant为待回滚。

2)获取待回滚的instant集合后,针对这些instant执行实际的回滚操作,主要包括:删除本次写入产生的索引、数据文件片、以及Timeline上的instant文件等,清理掉本次操作前期写入的所有记录,这体现了事务的原子性和一致性:

protected Boolean rollbackFailedWrites(boolean skipLocking) {
    HoodieTable<T, I, K, O> table = createTable(config, hadoopConf);
    List<String> instantsToRollback = getInstantsToRollback(table.getMetaClient(), config.getFailedWritesCleanPolicy(), Option.empty());
    Map<String, Option<HoodiePendingRollbackInfo>> pendingRollbacks = getPendingRollbackInfos(table.getMetaClient());
    instantsToRollback.forEach(entry -> pendingRollbacks.putIfAbsent(entry, Option.empty()));
    rollbackFailedWrites(pendingRollbacks, skipLocking);
    return true;
}
...
try {
      List<HoodieRollbackStat> stats = executeRollback(hoodieRollbackPlan);
      LOG.info("Rolled back inflight instant " + instantTimeToRollback);
      if (!isPendingCompaction) {
        //删除回滚记录的索引:
        rollBackIndex();
      }
...
// For Requested State (like failure during index lookup), there is nothing to do rollback other than
    // deleting the timeline file
    if (!resolvedInstant.isRequested()) {
      // delete all the data files for this commit
      //删除写入数据的文件片:
      LOG.info("Clean out all base files generated for commit: " + resolvedInstant);
      stats = executeRollback(resolvedInstant, hoodieRollbackPlan);
}
...
//删除时间线目录中本次操作涉及的instant文件:
deleteInflightAndRequestedInstant(deleteInstants, table.getActiveTimeline(), resolvedInstant);

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值