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_ONLY
和 DELETE_DATA_AND_LOG_FILES
類型的rollback,會直接刪除對應commit的數據文件和日誌文件,而對於 APPEND_ROLLBACK_BLOCK
類型,則會寫入控制塊至文件中,在讀取時不讀取其前一個塊。