时间线Timeline是Hudi的特有概念,表示数据湖的表在执行一系列事务操作过程中的事件信息,包含了每次事务操作的类型、时间戳和执行状态,同时也对于表的写入并发控制和保障事务的ACID特性提供了依据。
每张Hudi表的时间线数据也会序列化在该表的元数据目录.hoodie中:
分类:
随着对数据表的操作越来越多,时间线目录中文件会越来越多,当需要读取事件文件信息时就需要扫描较多的磁盘文件,这样就会影响到表的读写性能,因此需要将过期的事件文件进行归档并将归档结果放在.hoodie/archived目录下:
时间线主要分为活动时间线和归档时间线两类,如下为时间线的类关系图:
在时间线中,每个事务会实例化为一个Instant对象,该对象的构造方法如下,由状态、事件类型和时间戳三个部分组成:
public HoodieInstant(State state, String action, String timestamp) {
this.state = state;
this.action = action;
this.timestamp = timestamp;
}
- state:事件实例的执行状态,该字段为一个枚举类型:
public enum State { // Requested State (valid state for Compaction) REQUESTED, // Inflight instant INFLIGHT, // Committed instant COMPLETED, // Invalid instant INVALID }
- action:事件本身的类型,主要包括如下几种:
String COMMIT_ACTION = "commit"; //COW表的写入事务或MOR表压缩的提交 String DELTA_COMMIT_ACTION = "deltacommit"; //MOR的一次写入事务 String CLEAN_ACTION = "clean"; //清理过期的数据文件 String ROLLBACK_ACTION = "rollback"; //回滚异常的事务 String SAVEPOINT_ACTION = "savepoint"; String REPLACE_COMMIT_ACTION = "replacecommit"; //clustering操作 String COMPACTION_ACTION = "compaction"; //MOR表压缩 String RESTORE_ACTION = "restore"; //主动回撤已执行的事务 String INDEXING_ACTION = "indexing"; //创建表索引
- timestamp:事务发生时的时间戳
加载时间线:
应用程序在加载Hudi表对象时会先构建该表的元数据客户端HoodieTableMetaClient实例,该实例主要用于读写Hudi表的存储数据,并且在实例化该实例时会将该表中的活跃时间线从文件系统拉取到内存中:
private HoodieTableMetaClient(Configuration conf, String basePath, boolean loadActiveTimelineOnLoad,
ConsistencyGuardConfig consistencyGuardConfig, Option<TimelineLayoutVersion> layoutVersion,
String payloadClassName, FileSystemRetryConfig fileSystemRetryConfig) {
LOG.info("Loading HoodieTableMetaClient from " + basePath);
this.consistencyGuardConfig = consistencyGuardConfig;
this.fileSystemRetryConfig = fileSystemRetryConfig;
this.hadoopConf = new SerializableConfiguration(conf);
this.basePath = new SerializablePath(new CachingPath(basePath));
this.metaPath = new SerializablePath(new CachingPath(basePath, METAFOLDER_NAME));
this.fs = getFs();
...
if (loadActiveTimelineOnLoad) {
LOG.info("Loading Active commit timeline for " + basePath);
getActiveTimeline(); //加载活跃时间线
}
}
...
protected HoodieActiveTimeline(HoodieTableMetaClient metaClient, Set<String> includedExtensions,
boolean applyLayoutFilters) {
try { //从文件系统扫描instant数据
this.setInstants(metaClient.scanHoodieInstantsFromFileSystem(includedExtensions, applyLayoutFilters));
...
在取得该表活跃时间线上所有的Instant对象后,接下来则会利用这些Instant信息在内存中初始化Hudi表的文件系统视图HoodieTableFileSystemView对象,并且在该对象的初始化过程中主要提取了如下信息:
1、表服务相关元数据:
1)replacedFileGroupsMap :已完成的clustering的文件组id和instant信息
2)fileIdToPendingCompaction:待压缩的文件组id和压缩instant的信息
3)fgInpendingClustering:待执行clustering文件组id和instant信息
protected void init(HoodieTableMetaClient metaClient, HoodieTimeline visibleActiveTimeline) {
this.metaClient = metaClient;
refreshTimeline(visibleActiveTimeline);
resetFileGroupsReplaced(visibleCommitsAndCompactionTimeline);
this.bootstrapIndex = BootstrapIndex.getBootstrapIndex(metaClient);
// Load Pending Compaction Operations
resetPendingCompactionOperations(CompactionUtils.getAllPendingCompactionOperations(metaClient).values().stream()
.map(e -> Pair.of(e.getKey(), CompactionOperation.convertFromAvroRecordInstance(e.getValue()))));
resetBootstrapBaseFileMapping(Stream.empty());
resetFileGroupsInPendingClustering(ClusteringUtils.getAllFileGroupsInPendingClusteringPlans(metaClient));
}
2、数据文件信息:
初始化partitionToFileGroupsMap对象,用于存储Hudi表的分区及分区下的文件组的分布信息,从而减少读写过程中的flile listing。
注意:如上对象均采用了ConcurrentHashMap的数据结构,后续需要获取Hudi表的元数据信息时就可以直接从哈希表中读取,进而提升了访问元数据的性能。
同步时间线的变化:
时间线作为元数据,它是随着业务数据或表服务而变化的,每次数据表执行过一次事务的commit操作后,hudi表就会根据.hoodie目录下发生变化的instant文件的信息来同步更新HoodieTableFileSystemView对象:
public void sync() {
HoodieTimeline oldTimeline = getTimeline();
HoodieTimeline newTimeline = metaClient.reloadActiveTimeline().filterCompletedAndCompactionInstants();
try {
writeLock.lock();
runSync(oldTimeline, newTimeline);
} finally {
writeLock.unlock();
}
}
1)获取当前Hudi表内存中的时间线对象;
2)重新扫描当前的instant文件重新加载ActiveTimeline;
3)对比前两步发生变化的instant数据,执行在runSync方法进行表视图同步:
diffResult.getNewlySeenInstants().stream()
.filter(instant -> instant.isCompleted() || instant.getAction().equals(HoodieTimeline.COMPACTION_ACTION))
.forEach(instant -> {
try {
if (instant.getAction().equals(HoodieTimeline.COMMIT_ACTION)
|| instant.getAction().equals(HoodieTimeline.DELTA_COMMIT_ACTION)) {
addCommitInstant(timeline, instant);
} else if (instant.getAction().equals(HoodieTimeline.RESTORE_ACTION)) {
addRestoreInstant(timeline, instant);
} else if (instant.getAction().equals(HoodieTimeline.CLEAN_ACTION)) {
addCleanInstant(timeline, instant);
} else if (instant.getAction().equals(HoodieTimeline.COMPACTION_ACTION)) {
addPendingCompactionInstant(timeline, instant);
} else if (instant.getAction().equals(HoodieTimeline.ROLLBACK_ACTION)) {
addRollbackInstant(timeline, instant);
} else if (instant.getAction().equals(HoodieTimeline.REPLACE_COMMIT_ACTION)) {
addReplaceInstant(timeline, instant);
}
} catch (IOException ioe) {
throw new HoodieException(ioe);
}
});
根据不同的事件的类型来更新表的元数据信息:
1)commit或者delta_commit:将由于业务侧写入操作或者压缩操作产生的文件片变更信息更新至partitionToFileGroupsMap;
2)restore:更新表中相关分区的文件组元数据信息partitionToFileGroupsMap;并且如果需要回退的FileSlice之前执行过clustering操作,还需要将该instant从fgIdToReplaceInstants中删除掉;
3)clean:将clean操作中删除的文件片信息同步至partitionToFileGroupsMap中;
4)compaction:表示发起了压缩调度后,将添加这次压缩的计划信息更新至fgIdToPendingCompaction中,用于后续压缩执行时查询待压缩的计划信息。
5)clustering:将已完成clustering的instant和文件组Id记入fgIdToReplaceInstants,可用于在读取表数据时跳过执行过clustering的文件片。