-
Notifications
You must be signed in to change notification settings - Fork 2.5k
[HUDI-2833][Design] Merge small archive files instead of expanding indefinitely. #4078
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
2846e88
8f8ae38
019e161
20a7639
65f3ac5
e0c979f
a6a0889
0cde37b
2d2ee77
9b9620a
1a6fc6b
209b558
1a236d6
0287faa
fbc011d
0a05511
c36aac5
74721ec
ffb67da
a9664ae
1012596
01ed38a
9680dd5
7ed3f01
894b96e
0329837
14a6337
5ba0b03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -249,6 +249,19 @@ public class HoodieCompactionConfig extends HoodieConfig { | |
| + "record size estimate compute dynamically based on commit metadata. " | ||
| + " This is critical in computing the insert parallelism and bin-packing inserts into small files."); | ||
|
|
||
| public static final ConfigProperty<String> MAX_ARCHIVE_FILES_TO_KEEP_PROP = ConfigProperty | ||
| .key("hoodie.max.archive.files") | ||
| .defaultValue("10") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's make this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, thing. Changed. |
||
| .withDocumentation("The numbers of kept archive files under archived."); | ||
|
|
||
| public static final ConfigProperty<String> AUTO_TRIM_ARCHIVE_FILES_DROP = ConfigProperty | ||
| .key("hoodie.auto.trim.archive.files") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this one can be:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed. |
||
| .defaultValue("false") | ||
| .withDocumentation("When enabled, Hoodie will keep the most recent " + MAX_ARCHIVE_FILES_TO_KEEP_PROP.key() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Appreciate it. Changed |
||
| + " archive files and delete older one which lose part of archived instants information."); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should these configs live in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Emmm, because all the archive related configs such as
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it. Ideally, the archive configs should not be in |
||
|
|
||
|
|
||
|
|
||
|
nsivabalan marked this conversation as resolved.
Outdated
|
||
| /** @deprecated Use {@link #CLEANER_POLICY} and its methods instead */ | ||
| @Deprecated | ||
| public static final String CLEANER_POLICY_PROP = CLEANER_POLICY.key(); | ||
|
|
@@ -541,6 +554,16 @@ public Builder archiveCommitsWith(int minToKeep, int maxToKeep) { | |
| return this; | ||
| } | ||
|
|
||
| public Builder maxArchiveFilesToKeep(int number) { | ||
| compactionConfig.setValue(MAX_ARCHIVE_FILES_TO_KEEP_PROP, String.valueOf(number)); | ||
| return this; | ||
| } | ||
|
|
||
| public Builder withAutoTrimArchiveFiles(boolean enable) { | ||
| compactionConfig.setValue(AUTO_TRIM_ARCHIVE_FILES_DROP, String.valueOf(enable)); | ||
| return this; | ||
| } | ||
|
|
||
| public Builder compactionSmallFileSize(long smallFileLimitBytes) { | ||
| compactionConfig.setValue(PARQUET_SMALL_FILE_LIMIT, String.valueOf(smallFileLimitBytes)); | ||
| return this; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ | |
| import org.apache.hudi.common.model.HoodieArchivedLogFile; | ||
| import org.apache.hudi.common.model.HoodieAvroPayload; | ||
| import org.apache.hudi.common.model.HoodieFailedWritesCleaningPolicy; | ||
| import org.apache.hudi.common.model.HoodieLogFile; | ||
| import org.apache.hudi.common.table.HoodieTableMetaClient; | ||
| import org.apache.hudi.common.table.log.HoodieLogFormat; | ||
| import org.apache.hudi.common.table.log.HoodieLogFormat.Writer; | ||
|
|
@@ -80,6 +81,7 @@ public class HoodieTimelineArchiveLog<T extends HoodieAvroPayload, I, K, O> { | |
| private Writer writer; | ||
| private final int maxInstantsToKeep; | ||
| private final int minInstantsToKeep; | ||
| private final int maxArchiveFilesToKeep; | ||
| private final HoodieTable<T, I, K, O> table; | ||
| private final HoodieTableMetaClient metaClient; | ||
|
|
||
|
|
@@ -90,6 +92,7 @@ public HoodieTimelineArchiveLog(HoodieWriteConfig config, HoodieTable<T, I, K, O | |
| this.archiveFilePath = HoodieArchivedTimeline.getArchiveLogPath(metaClient.getArchivePath()); | ||
| this.maxInstantsToKeep = config.getMaxCommitsToKeep(); | ||
| this.minInstantsToKeep = config.getMinCommitsToKeep(); | ||
| this.maxArchiveFilesToKeep = config.getMaxArchiveFilesToKeep(); | ||
| } | ||
|
|
||
| private Writer openWriter() { | ||
|
|
@@ -130,6 +133,9 @@ public boolean archiveIfRequired(HoodieEngineContext context) throws IOException | |
| archive(context, instantsToArchive); | ||
| LOG.info("Deleting archived instants " + instantsToArchive); | ||
| success = deleteArchivedInstants(instantsToArchive, context); | ||
| if (config.getAutoTrimArchiveFilesEnable()) { | ||
| trimArchiveFilesIfNecessary(context); | ||
| } | ||
| } else { | ||
| LOG.info("No Instants to archive"); | ||
| } | ||
|
|
@@ -140,6 +146,48 @@ public boolean archiveIfRequired(HoodieEngineContext context) throws IOException | |
| } | ||
| } | ||
|
|
||
| private void trimArchiveFilesIfNecessary(HoodieEngineContext context) throws IOException { | ||
| Stream<HoodieLogFile> allLogFiles = FSUtils.getAllLogFiles(metaClient.getFs(), | ||
| archiveFilePath.getParent(), | ||
| archiveFilePath.getName(), | ||
| HoodieArchivedLogFile.ARCHIVE_EXTENSION, | ||
| ""); | ||
| List<HoodieLogFile> sortedLogFilesList = allLogFiles.sorted(HoodieLogFile.getReverseLogFileComparator()).collect(Collectors.toList()); | ||
| if (!sortedLogFilesList.isEmpty()) { | ||
| List<String> skipped = sortedLogFilesList.stream().skip(maxArchiveFilesToKeep).map(HoodieLogFile::getPath).map(Path::toString).collect(Collectors.toList()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed. |
||
| if (!skipped.isEmpty()) { | ||
| LOG.info("Deleting archive files : " + skipped); | ||
| context.setJobStatus(this.getClass().getSimpleName(), "Delete archive files"); | ||
| Map<String, Boolean> result = deleteFilesParallelize(metaClient, skipped, context, true); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove local variable assignment since it's not used?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed. |
||
| } | ||
| } | ||
| } | ||
|
|
||
| private Map<String, Boolean> deleteFilesParallelize(HoodieTableMetaClient metaClient, List<String> paths, HoodieEngineContext context, boolean ignoreFailed) { | ||
|
|
||
| return FSUtils.parallelizeFilesProcess(context, | ||
| metaClient.getFs(), | ||
| config.getArchiveDeleteParallelism(), | ||
| pairOfSubPathAndConf -> { | ||
| Path file = new Path(pairOfSubPathAndConf.getKey()); | ||
| try { | ||
| FileSystem fs = metaClient.getFs(); | ||
|
yihua marked this conversation as resolved.
|
||
| if (fs.exists(file)) { | ||
| return fs.delete(file, false); | ||
| } | ||
| return true; | ||
| } catch (IOException e) { | ||
| if (!ignoreFailed) { | ||
| throw new HoodieIOException("Failed to delete : " + file, e); | ||
| } else { | ||
| LOG.warn("Ignore failed deleting : " + file); | ||
| return true; | ||
| } | ||
| } | ||
| }, | ||
| paths); | ||
| } | ||
|
|
||
| private Stream<HoodieInstant> getCleanInstantsToArchive() { | ||
| HoodieTimeline cleanAndRollbackTimeline = table.getActiveTimeline() | ||
| .getTimelineOfActions(CollectionUtils.createSet(HoodieTimeline.CLEAN_ACTION, HoodieTimeline.ROLLBACK_ACTION)).filterCompletedInstants(); | ||
|
|
@@ -234,22 +282,7 @@ private boolean deleteArchivedInstants(List<HoodieInstant> archivedInstants, Hoo | |
| }).map(Path::toString).collect(Collectors.toList()); | ||
|
|
||
| context.setJobStatus(this.getClass().getSimpleName(), "Delete archived instants"); | ||
| Map<String, Boolean> resultDeleteInstantFiles = FSUtils.parallelizeFilesProcess(context, | ||
| metaClient.getFs(), | ||
| config.getArchiveDeleteParallelism(), | ||
| pairOfSubPathAndConf -> { | ||
| Path commitFile = new Path(pairOfSubPathAndConf.getKey()); | ||
| try { | ||
| FileSystem fs = commitFile.getFileSystem(pairOfSubPathAndConf.getValue().get()); | ||
| if (fs.exists(commitFile)) { | ||
| return fs.delete(commitFile, false); | ||
| } | ||
| return true; | ||
| } catch (IOException e) { | ||
| throw new HoodieIOException("Failed to delete archived instant " + commitFile, e); | ||
| } | ||
| }, | ||
| instantFiles); | ||
| Map<String, Boolean> resultDeleteInstantFiles = deleteFilesParallelize(metaClient, instantFiles, context, false); | ||
|
|
||
| for (Map.Entry<String, Boolean> result : resultDeleteInstantFiles.entrySet()) { | ||
| LOG.info("Archived and deleted instant file " + result.getKey() + " : " + result.getValue()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,8 @@ | |
|
|
||
| package org.apache.hudi.io; | ||
|
|
||
| import org.apache.hadoop.fs.LocatedFileStatus; | ||
| import org.apache.hadoop.fs.RemoteIterator; | ||
| import org.apache.hudi.avro.model.HoodieActionInstant; | ||
| import org.apache.hudi.avro.model.HoodieCleanMetadata; | ||
| import org.apache.hudi.avro.model.HoodieCleanerPlan; | ||
|
|
@@ -59,6 +61,10 @@ | |
| import org.junit.jupiter.api.AfterEach; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.params.ParameterizedTest; | ||
|
|
||
| import static org.junit.jupiter.params.provider.Arguments.arguments; | ||
| import org.junit.jupiter.params.provider.Arguments; | ||
| import org.junit.jupiter.params.provider.MethodSource; | ||
| import org.junit.jupiter.params.provider.ValueSource; | ||
|
|
||
| import java.io.IOException; | ||
|
|
@@ -119,18 +125,46 @@ public void clean() throws IOException { | |
| } | ||
|
|
||
| private HoodieWriteConfig initTestTableAndGetWriteConfig(boolean enableMetadata, int minArchivalCommits, int maxArchivalCommits, int maxDeltaCommitsMetadataTable) throws Exception { | ||
| return initTestTableAndGetWriteConfig(enableMetadata, minArchivalCommits, maxArchivalCommits, maxDeltaCommitsMetadataTable, HoodieTableType.COPY_ON_WRITE); | ||
| return initTestTableAndGetWriteConfig(enableMetadata, minArchivalCommits, maxArchivalCommits, maxDeltaCommitsMetadataTable, HoodieTableType.COPY_ON_WRITE, false, 10); | ||
| } | ||
|
|
||
| private HoodieWriteConfig initTestTableAndGetWriteConfig(boolean enableMetadata, int minArchivalCommits, int maxArchivalCommits, int maxDeltaCommitsMetadataTable, | ||
| private HoodieWriteConfig initTestTableAndGetWriteConfig(boolean enableMetadata, | ||
| int minArchivalCommits, | ||
| int maxArchivalCommits, | ||
| int maxDeltaCommitsMetadataTable, | ||
| HoodieTableType tableType) throws Exception { | ||
| return initTestTableAndGetWriteConfig(enableMetadata, minArchivalCommits, maxArchivalCommits, maxDeltaCommitsMetadataTable, tableType, false, 10); | ||
| } | ||
|
|
||
| private HoodieWriteConfig initTestTableAndGetWriteConfig(boolean enableMetadata, | ||
| int minArchivalCommits, | ||
| int maxArchivalCommits, | ||
| int maxDeltaCommitsMetadataTable, | ||
| boolean enableArchiveTrim, | ||
| int archiveFilesToKeep) throws Exception { | ||
| return initTestTableAndGetWriteConfig(enableMetadata, minArchivalCommits, maxArchivalCommits, maxDeltaCommitsMetadataTable, HoodieTableType.COPY_ON_WRITE, enableArchiveTrim, archiveFilesToKeep); | ||
| } | ||
|
|
||
| private HoodieWriteConfig initTestTableAndGetWriteConfig(boolean enableMetadata, | ||
| int minArchivalCommits, | ||
| int maxArchivalCommits, | ||
| int maxDeltaCommitsMetadataTable, | ||
| HoodieTableType tableType, | ||
| boolean enableArchiveTrim, | ||
| int archiveFilesToKeep) throws Exception { | ||
| init(tableType); | ||
| HoodieWriteConfig writeConfig = HoodieWriteConfig.newBuilder().withPath(basePath) | ||
| .withSchema(HoodieTestDataGenerator.TRIP_EXAMPLE_SCHEMA).withParallelism(2, 2) | ||
| .withCompactionConfig(HoodieCompactionConfig.newBuilder().retainCommits(1).archiveCommitsWith(minArchivalCommits, maxArchivalCommits).build()) | ||
| .withCompactionConfig(HoodieCompactionConfig.newBuilder() | ||
| .retainCommits(1) | ||
| .withAutoTrimArchiveFiles(enableArchiveTrim) | ||
| .maxArchiveFilesToKeep(archiveFilesToKeep) | ||
| .archiveCommitsWith(minArchivalCommits, maxArchivalCommits) | ||
| .build()) | ||
| .withFileSystemViewConfig(FileSystemViewStorageConfig.newBuilder() | ||
| .withRemoteServerPort(timelineServicePort).build()) | ||
| .withMetadataConfig(HoodieMetadataConfig.newBuilder().enable(enableMetadata) | ||
| .withMetadataConfig(HoodieMetadataConfig.newBuilder() | ||
| .enable(enableMetadata) | ||
| .withMaxNumDeltaCommitsBeforeCompaction(maxDeltaCommitsMetadataTable).build()) | ||
| .forTable("test-trip-table").build(); | ||
| initWriteConfigAndMetatableWriter(writeConfig, enableMetadata); | ||
|
|
@@ -183,6 +217,41 @@ public void testArchiveTableWithArchival(boolean enableMetadata) throws Exceptio | |
| } | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @MethodSource("testArchiveTableWithArchivalTrim") | ||
| public void testArchiveTableWithArchivalCleanUp(boolean enableMetadata, boolean enableArchiveTrim, int archiveFilesToKeep) throws Exception { | ||
| HashSet<String> archiveFilesExisted = new HashSet<>(); | ||
| ArrayList<String> currentExistArchiveFiles = new ArrayList<>(); | ||
| HoodieWriteConfig writeConfig = initTestTableAndGetWriteConfig(enableMetadata, 2, 3, 2, enableArchiveTrim, archiveFilesToKeep); | ||
| String archivePath = metaClient.getArchivePath(); | ||
| for (int i = 1; i < 10; i++) { | ||
| testTable.doWriteOperation("0000000" + i, WriteOperationType.UPSERT, i == 1 ? Arrays.asList("p1", "p2") : Collections.emptyList(), Arrays.asList("p1", "p2"), 2); | ||
| // trigger archival | ||
| archiveAndGetCommitsList(writeConfig); | ||
| RemoteIterator<LocatedFileStatus> iter = metaClient.getFs().listFiles(new Path(archivePath), false); | ||
| ArrayList<String> files = new ArrayList<>(); | ||
| while (iter.hasNext()) { | ||
| files.add(iter.next().getPath().toString()); | ||
| } | ||
| archiveFilesExisted.addAll(files); | ||
| currentExistArchiveFiles = files; | ||
| } | ||
|
|
||
| assertEquals(archiveFilesToKeep, currentExistArchiveFiles.size()); | ||
|
|
||
|
yihua marked this conversation as resolved.
|
||
| if (enableArchiveTrim) { | ||
| // sort archive files path | ||
| List<String> sorted = archiveFilesExisted.stream().sorted().collect(Collectors.toList()); | ||
| List<String> archiveFilesDeleted = sorted.subList(0, 3 - archiveFilesToKeep); | ||
| List<String> archiveFilesKept = sorted.subList(3 - archiveFilesToKeep, sorted.size()); | ||
|
|
||
| // assert older archive files are deleted | ||
| assertFalse(currentExistArchiveFiles.containsAll(archiveFilesDeleted)); | ||
| // assert most recent archive files are preserved | ||
| assertTrue(currentExistArchiveFiles.containsAll(archiveFilesKept)); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a check when archive trim is disabled as well?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added. |
||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @ValueSource(booleans = {true, false}) | ||
| public void testNoArchivalUntilMaxArchiveConfigWithExtraInflightCommits(boolean enableMetadata) throws Exception { | ||
|
|
@@ -723,4 +792,14 @@ private HoodieInstant createRollbackMetadata(String rollbackTime, String commitT | |
| } | ||
| return new HoodieInstant(inflight, "rollback", rollbackTime); | ||
| } | ||
|
|
||
| private static Stream<Arguments> testArchiveTableWithArchivalTrim() { | ||
| // boolean enableMetadata, boolean enableArchiveTrim, int archiveFilesToKeep | ||
| return Stream.of( | ||
| arguments(false, true, 1), | ||
| arguments(false, false, 3), | ||
| arguments(true, true, 1), | ||
| arguments(true, false, 3) | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: after thinking about the naming again, let's use
hoodie.archiveprefix for the archive configs and update the variable naming accordingly.For this one, it can be
hoodie.archive.max.files.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking more like "hoodie.max.archive.files.to.retain" sort of.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This falls under archival timeline so better to have the same
hoodie.archiveprefix. Given that the writer archives instants instead of files, this shouldn't create confusion.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed.