Hudi剖析|Apache Hudi Rollback實現分析

1. 介紹

     在發現有些commit出錯時,可使用Hudi提供的rollback回滾至指定的commit,這樣可防止出現錯誤的結果,並且當一次commit失敗時,也會進行rollback操作,保證一次commit的原子性。

2. 分析

     rollback(回滾)的入口在 HoodieWriteClient#rollback,其依賴 HoodieWriteClient#rollbackInternal方法完成實際的回滾,其核心代碼如下

 

protected void rollbackInternal(String commitToRollback) {

// 生成新的rollback時間

final String startRollbackTime = HoodieActiveTimeline.createNewInstantTime();

try {

HoodieTable<T> table = HoodieTable.getHoodieTable(

createMetaClient(true), config, jsc);

// 找出第一個與rollback commit相等的instant

Option<HoodieInstant> rollbackInstantOpt =

Option.fromJavaOptional(table.getActiveTimeline().getCommitsTimeline().getInstants()

.filter(instant -> HoodieActiveTimeline.EQUAL.test(instant.getTimestamp(), commitToRollback))

.findFirst());

// 存在

if (rollbackInstantOpt.isPresent()) {

// 進行回滾

List<HoodieRollbackStat> stats = doRollbackAndGetStats(rollbackInstantOpt.get());

// 結束回滾

finishRollback(context, stats, Collections.singletonList(commitToRollback), startRollbackTime);

}

} catch (IOException e) {

throw new HoodieRollbackException("Failed to rollback " + config.getBasePath() + " commits " + commitToRollback,

e);

}

}

 

        首先過濾出commit/delta_commit中是否存在待回滾instant的時間,如果存在,則進行回滾,回滾的核心方法爲 doRollbackAndGetStats,該方法在前一篇講解savepoint時已經分析過,該方法會調用 HoodieTable#rollback完成實際回滾動作,下面着重分析 HoodieTable#rollback方法,對於MOR和COW不同類型有不同實現,下面一一進行分析。

 

2.1 HoodieCopyOnWriteTable#rollback

對於COW類型而言, rollback核心代碼如下

public List<HoodieRollbackStat> rollback(JavaSparkContext jsc, HoodieInstant instant, boolean deleteInstants)

throws IOException {

long startTime = System.currentTimeMillis();

List<HoodieRollbackStat> stats = new ArrayList<>();

HoodieActiveTimeline activeTimeline = this.getActiveTimeline();



if (instant.isCompleted()) { // instant狀態爲completed

// 轉變至inflight狀態

instant = activeTimeline.revertToInflight(instant);

}



if (!instant.isRequested()) { // 不爲requested狀態

String commit = instant.getTimestamp();

// 生成回滾的請求

List<RollbackRequest> rollbackRequests = generateRollbackRequests(instant);

// 進行回滾

stats = new RollbackExecutor(metaClient, config).performRollback(jsc, instant, rollbackRequests);

}

// 刪除inflight和requested狀態的instant

deleteInflightAndRequestedInstant(deleteInstants, activeTimeline, instant);

return stats;

}

 

可以看到,進行回滾總體分爲四步:

1. 對於處理completed狀態的instant,首先會將其轉變至inflight狀態,而對於不處於requested狀態的instant(compaction會存在requested狀態);

2. 生成回滾請求;

3. 進行回滾;

4. 刪除instant。

 

2.1.1 轉變instant狀態

對於處於completed狀態的instant,將其轉變至 inflight狀態,其核心代碼如下

public HoodieInstant revertToInflight(HoodieInstant instant) {

// 獲取inflight狀態的instant

HoodieInstant inflight = HoodieTimeline.getInflightInstant(instant, metaClient.getTableType());

// 轉變至inflight,即文件名會變爲.inflight

revertCompleteToInflight(instant, inflight);

return inflight;

}

 

對於狀態轉變體現在文件名後綴的變化,即會變爲 .inflght狀態。

 

2.1.2 生成回滾請求

回滾請求由 generateRollbackRequests方法生成,其核心代碼如下

private List<RollbackRequest> generateRollbackRequests(HoodieInstant instantToRollback)

throws IOException {

// 獲取所有的分區路徑,對每個分區路徑生成DELETE_DATA_AND_LOG_FILES類型的RollbackRequest

return FSUtils.getAllPartitionPaths(this.metaClient.getFs(), this.getMetaClient().getBasePath(),

config.shouldAssumeDatePartitioning()).stream().map(partitionPath -> RollbackRequest.createRollbackRequestWithDeleteDataAndLogFilesAction(partitionPath, instantToRollback))

.collect(Collectors.toList());

}

 

會根據不同的分區路徑生成不同的RollbackRequest,該方法會生成會生成DELETEDATAANDLOGFILES類型,指定分區路徑的RollbackRequest。

 

2.1.3 進行回滾

通過 RollbackExecutor#performRollback進行回滾,其核心代碼如下

對於DELETEDATAFILES_ONLY類型的rollback,會調用 deleteCleanedFiles來刪除數據文件,其核心代碼如下

private Map<FileStatus, Boolean> deleteCleanedFiles(HoodieTableMetaClient metaClient, HoodieWriteConfig config,

Map<FileStatus, Boolean> results, String commit, String partitionPath) throws IOException {

FileSystem fs = metaClient.getFs();

PathFilter filter = (path) -> {

if (path.toString().contains(".parquet")) { // 數據文件

String fileCommitTime = FSUtils.getCommitTime(path.getName());

// 與rollback時間相等

return commit.equals(fileCommitTime);

}

return false;

};

// 過濾出與rollback時間相等的所有parquet文件

FileStatus[] toBeDeleted = fs.listStatus(FSUtils.getPartitionPath(config.getBasePath(), partitionPath), filter);

for (FileStatus file : toBeDeleted) { // 逐一刪除

boolean success = fs.delete(file.getPath(), false);

results.put(file, success);

}

return results;

}

 

首先會過濾指定分區下所有與rollback時間相等的parquet文件,然後逐一刪除。

對於DELETEDATAANDLOGFILES類型的rollback,會調用同名的 deleteCleanedFiles來刪除文件,其核心代碼如下

private Map<FileStatus, Boolean> deleteCleanedFiles(HoodieTableMetaClient metaClient, HoodieWriteConfig config,

Map<FileStatus, Boolean> results, String partitionPath, PathFilter filter) throws IOException {

FileSystem fs = metaClient.getFs();

// 過濾出與rollback時間相等的所有parquet和log文件

FileStatus[] toBeDeleted = fs.listStatus(FSUtils.getPartitionPath(config.getBasePath(), partitionPath), filter);

for (FileStatus file : toBeDeleted) { // 逐一刪除

boolean success = fs.delete(file.getPath(), false);

results.put(file, success);

}

return results;

}

 

首先會過濾指定分區下所有與rollback時間相等的parquet/log文件,然後逐一刪除。

對於APPENDROLLBACKBLOCK類型的rollback,會生成日誌文件控制塊並寫入指定的文件中,在讀取時,將不會讀取該控制塊的前一個塊。

 

2.1.4 刪除instant

在完成回滾後,還需要調用 deleteInflightAndRequestedInstant來刪除instant,其核心代碼如下

protected void deleteInflightAndRequestedInstant(boolean deleteInstant, HoodieActiveTimeline activeTimeline,

HoodieInstant instantToBeDeleted) {

// 刪除marker下的目錄

deleteMarkerDir(instantToBeDeleted.getTimestamp());



if (deleteInstant) { // 刪除instant

// 刪除處於pending狀態的instant

activeTimeline.deletePending(instantToBeDeleted);

if (instantToBeDeleted.isInflight() && !metaClient.getTimelineLayoutVersion().isNullVersion()) {

// 刪除處於requested狀態的instant

instantToBeDeleted = new HoodieInstant(State.REQUESTED, instantToBeDeleted.getAction(),

instantToBeDeleted.getTimestamp());

activeTimeline.deletePending(instantToBeDeleted);

}

}

}

 

刪除instant主要是刪除處於inflight和requested狀態的在元數據目錄下的文件。

 

2.2 HoodieMergeOnReadTable#rollback

對於MOR而言, rollback核心代碼如下

public List<HoodieRollbackStat> rollback(JavaSparkContext jsc, HoodieInstant instant,

boolean deleteInstants) throws IOException {

long startTime = System.currentTimeMillis();

// rollback時間

String commit = instant.getTimestamp();



if (instant.isCompleted()) { // instant狀態爲completed

// // 轉變至inflight狀態

instant = this.getActiveTimeline().revertToInflight(instant);

}



List<HoodieRollbackStat> allRollbackStats = new ArrayList<>();



// 不爲requested狀態

if (!instant.isRequested()) {

// 生成rollback請求

List<RollbackRequest> rollbackRequests = generateRollbackRequests(jsc, instant);

// 進行回滾

allRollbackStats = new RollbackExecutor(metaClient, config).performRollback(jsc, instant, rollbackRequests);

}



// 刪除inflight和requested狀態的instant

deleteInflightAndRequestedInstant(deleteInstants, this.getActiveTimeline(), instant);



return allRollbackStats;

}

可以看到其流程與COW相同,不再贅述。

3. 總結

      對於rollback而言,其主要分爲四步:轉變instant狀態;2. 生成回滾請求;3. 進行回滾;4. 刪除instant。而回滾時會分爲三種情況,對於 DELETE_DATA_FILES_ONLYDELETE_DATA_AND_LOG_FILES類型的rollback,會直接刪除對應commit的數據文件和日誌文件,而對於 APPEND_ROLLBACK_BLOCK類型,則會寫入控制塊至文件中,在讀取時不讀取其前一個塊。

發佈了65 篇原創文章 · 獲贊 8 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章