-
Notifications
You must be signed in to change notification settings - Fork 3.8k
fix: Stop kill task if segment IDs that share load spec of killable segment could not be determined #19737
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
fix: Stop kill task if segment IDs that share load spec of killable segment could not be determined #19737
Changes from all commits
626a66d
392b190
8f6c820
4249ea9
02612d9
cb45985
8cadbbe
0f2ee05
0dd7e69
3728d11
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 |
|---|---|---|
|
|
@@ -48,10 +48,10 @@ | |
| import org.apache.druid.java.util.common.StringUtils; | ||
| import org.apache.druid.java.util.common.logger.Logger; | ||
| import org.apache.druid.server.coordination.BroadcastDatasourceLoadingSpec; | ||
| import org.apache.druid.server.http.DataSegmentPlus; | ||
| import org.apache.druid.server.lookup.cache.LookupLoadingSpec; | ||
| import org.apache.druid.server.security.ResourceAction; | ||
| import org.apache.druid.timeline.DataSegment; | ||
| import org.apache.druid.timeline.SegmentId; | ||
| import org.apache.druid.utils.CollectionUtils; | ||
| import org.joda.time.DateTime; | ||
| import org.joda.time.Interval; | ||
|
|
@@ -67,10 +67,10 @@ | |
| import java.util.NavigableMap; | ||
| import java.util.Set; | ||
| import java.util.TreeMap; | ||
| import java.util.function.Function; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| /** | ||
| * <p/> | ||
| * The client representation of this task is {@link ClientKillUnusedSegmentsTaskQuery}. | ||
| * JSON serialization fields of this class must correspond to those of {@link | ||
| * ClientKillUnusedSegmentsTaskQuery}, except for {@link #id} and {@link #context} fields. | ||
|
|
@@ -87,6 +87,10 @@ | |
| * <li> Filter the set of unreferenced segments using load specs from the set of used segments. </li> | ||
| * <li> Kill the filtered set of segments from deep storage. </li> | ||
| * </ol> | ||
| * Note: When {@link Tasks#USE_CONCURRENT_LOCKS} is true, keep a large buffer | ||
| * period before killing segments after they have been marked as unused. | ||
| * Otherwise, there may be a potential data loss if a concurrent APPEND job | ||
| * upgrades one of the segments that are being killed. | ||
| */ | ||
| public class KillUnusedSegmentsTask extends AbstractFixedIntervalTask | ||
| { | ||
|
|
@@ -211,7 +215,7 @@ public TaskStatus runTask(TaskToolbox toolbox) throws Exception | |
| // List unused segments | ||
| int nextBatchSize = computeNextBatchSize(numSegmentsKilled); | ||
| @Nullable Integer numTotalBatches = getNumTotalBatches(); | ||
| List<DataSegment> unusedSegments; | ||
| List<DataSegmentPlus> unusedSegmentsPlus; | ||
| logInfo( | ||
| "Starting kill for datasource[%s] in interval[%s] and versions[%s] with batchSize[%d], up to limit[%d]" | ||
| + " segments before maxUsedStatusLastUpdatedTime[%s] will be deleted%s", | ||
|
|
@@ -236,12 +240,25 @@ public TaskStatus runTask(TaskToolbox toolbox) throws Exception | |
| break; | ||
| } | ||
|
|
||
| unusedSegments = fetchNextBatchOfUnusedSegments(toolbox, nextBatchSize); | ||
| unusedSegmentsPlus = fetchNextBatchOfUnusedSegments(toolbox, nextBatchSize); | ||
| if (unusedSegmentsPlus.isEmpty()) { | ||
| // No more segments eligible for kill, do not proceed further | ||
| break; | ||
| } | ||
|
|
||
| // Fetch locks each time as a revokal could have occurred in between batches | ||
| final NavigableMap<DateTime, List<TaskLock>> taskLockMap | ||
| = getNonRevokedTaskLockMap(toolbox.getTaskActionClient()); | ||
|
|
||
| final Set<DataSegment> unusedSegments = unusedSegmentsPlus.stream() | ||
| .map(DataSegmentPlus::getDataSegment) | ||
| .collect(Collectors.toSet()); | ||
| final Map<String, DataSegmentPlus> unusedIdToSegmentPlus = CollectionUtils.toMap( | ||
| unusedSegmentsPlus, | ||
| segment -> segment.getDataSegment().getId().toString(), | ||
| Function.identity() | ||
| ); | ||
|
|
||
| if (!TaskLocks.isLockCoversSegments(taskLockMap, unusedSegments)) { | ||
|
Comment on lines
+253
to
262
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: perhaps we could introduce a thin wrapper for
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. The |
||
| throw new ISE( | ||
| "Locks[%s] for task[%s] can't cover segments[%s]", | ||
|
|
@@ -251,62 +268,46 @@ public TaskStatus runTask(TaskToolbox toolbox) throws Exception | |
| ); | ||
| } | ||
|
|
||
| // Kill segments. Order is important here: | ||
| // Retrieve the segment upgrade infos for the batch _before_ the segments are nuked | ||
| // We then want the nuke action to clean up the metadata records _before_ the segments are removed from storage. | ||
| // This helps maintain that we will always have a storage segment if the metadata segment is present. | ||
| // Determine the subset of segments to be killed from deep storage based on loadspecs. | ||
| // If the segment nuke throws an exception, then the segment cleanup is abandoned. | ||
|
|
||
| // Determine upgraded segment ids before nuking | ||
| final Set<String> segmentIds = unusedSegments.stream() | ||
| .map(DataSegment::getId) | ||
| .map(SegmentId::toString) | ||
| .collect(Collectors.toSet()); | ||
| final Map<String, String> upgradedFromSegmentIds = new HashMap<>(); | ||
| try { | ||
| upgradedFromSegmentIds.putAll( | ||
| taskActionClient.submit( | ||
| new RetrieveUpgradedFromSegmentIdsAction(getDataSource(), segmentIds) | ||
| ).getUpgradedFromSegmentIds() | ||
| ); | ||
| } | ||
| catch (Exception e) { | ||
| LOG.warn( | ||
| e, | ||
| "Could not retrieve parent segment ids using task action[retrieveUpgradedFromSegmentIds]." | ||
| + " Overlord may be on an older version." | ||
| ); | ||
| } | ||
| // Kill segments - order of steps 1, 2, 3, 4 must remain the same | ||
|
|
||
| // Nuke Segments | ||
| taskActionClient.submit(new SegmentNukeAction(new HashSet<>(unusedSegments))); | ||
| emitMetric(toolbox.getEmitter(), TaskMetrics.SEGMENTS_DELETED_FROM_METADATA_STORE, unusedSegments.size()); | ||
| // 1. Determine parent segment ids of killable unused segments | ||
| final Map<String, String> upgradedFromSegmentIds | ||
| = fetchParentIdsForSegments(toolbox, unusedIdToSegmentPlus); | ||
|
|
||
| // Determine segments to be killed | ||
| final List<DataSegment> segmentsToBeKilled | ||
| = getKillableSegments(unusedSegments, upgradedFromSegmentIds, usedSegmentLoadSpecs, taskActionClient); | ||
| // 2. Identify killable segments whose load specs are not shared with any other segment | ||
| final List<DataSegment> segmentsToKillFromDeepStore = getKillableSegments( | ||
| unusedIdToSegmentPlus, | ||
| upgradedFromSegmentIds, | ||
| usedSegmentLoadSpecs, | ||
| taskActionClient | ||
| ); | ||
|
|
||
| // 2a. Track segments that cannot be removed from deep store yet | ||
| final Set<DataSegment> segmentsNotKilled = new HashSet<>(unusedSegments); | ||
| segmentsToBeKilled.forEach(segmentsNotKilled::remove); | ||
|
|
||
| segmentsToKillFromDeepStore.forEach(segmentsNotKilled::remove); | ||
| if (!segmentsNotKilled.isEmpty()) { | ||
| LOG.warn( | ||
| "Skipping kill of [%d] segments from deep storage as their load specs are used by other segments.", | ||
| segmentsNotKilled.size() | ||
| "Skipping kill of [%d] segments of datasource[%s] from deep storage" | ||
|
abhishekrb19 marked this conversation as resolved.
|
||
| + " as their load specs are shared by other segments.", | ||
|
abhishekrb19 marked this conversation as resolved.
|
||
| segmentsNotKilled.size(), getDataSource() | ||
| ); | ||
| } | ||
|
|
||
| toolbox.getDataSegmentKiller().kill(segmentsToBeKilled); | ||
| emitMetric(toolbox.getEmitter(), TaskMetrics.SEGMENTS_DELETED_FROM_DEEPSTORE, segmentsToBeKilled.size()); | ||
| // 3. Nuke all eligible unused segments | ||
|
Member
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. [P1] Close the sharing race before deleting deep storage The new relationship lookup is only a preflight before
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. It is very unlikely for this to happen. It had been called out here as well. Essentially, the kill buffer period protects us against 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. Adding a comment in javadoc. |
||
| taskActionClient.submit(new SegmentNukeAction(unusedSegments)); | ||
| emitMetric(toolbox.getEmitter(), TaskMetrics.SEGMENTS_DELETED_FROM_METADATA_STORE, unusedIdToSegmentPlus.size()); | ||
|
|
||
| // 4. Delete deep store files only for segments which do not share load specs with other segments | ||
| toolbox.getDataSegmentKiller().kill(segmentsToKillFromDeepStore); | ||
| emitMetric(toolbox.getEmitter(), TaskMetrics.SEGMENTS_DELETED_FROM_DEEPSTORE, segmentsToKillFromDeepStore.size()); | ||
|
|
||
| numBatchesProcessed++; | ||
| numSegmentsKilled += segmentsToBeKilled.size(); | ||
| numSegmentsKilled += segmentsToKillFromDeepStore.size(); | ||
|
|
||
| logInfo("Processed [%d] batches for kill task[%s].", numBatchesProcessed, getId()); | ||
|
|
||
| nextBatchSize = computeNextBatchSize(numSegmentsKilled); | ||
| } while (!unusedSegments.isEmpty() && (null == numTotalBatches || numBatchesProcessed < numTotalBatches)); | ||
| } while (!unusedSegmentsPlus.isEmpty() && (null == numTotalBatches || numBatchesProcessed < numTotalBatches)); | ||
|
|
||
| final String taskId = getId(); | ||
| logInfo( | ||
|
|
@@ -342,7 +343,7 @@ int computeNextBatchSize(int numSegmentsKilled) | |
| /** | ||
| * Fetches the next batch of unused segments that are eligible for kill. | ||
| */ | ||
| protected List<DataSegment> fetchNextBatchOfUnusedSegments(TaskToolbox toolbox, int nextBatchSize) throws IOException | ||
| protected List<DataSegmentPlus> fetchNextBatchOfUnusedSegments(TaskToolbox toolbox, int nextBatchSize) throws IOException | ||
| { | ||
| return toolbox.getTaskActionClient().submit( | ||
| new RetrieveUnusedSegmentsAction( | ||
|
|
@@ -352,7 +353,44 @@ protected List<DataSegment> fetchNextBatchOfUnusedSegments(TaskToolbox toolbox, | |
| nextBatchSize, | ||
| maxUsedStatusLastUpdatedTime | ||
| ) | ||
| ); | ||
| ) | ||
| .stream() | ||
| .map(segment -> new DataSegmentPlus(segment, null, null, null, null, null, null, null)) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| /** | ||
| * Fetches the parent IDs (if any) for the given unused segments. | ||
| * | ||
| * @param unusedIdToSegmentPlus Map containing unused segments whose parent IDs | ||
| * need to be fetched | ||
| * @return Map from segment ID to the segment ID from which | ||
| * it was upgraded. If an input segment was not upgraded from any other segment, | ||
| * it does not have an entry in the map. | ||
| */ | ||
| protected Map<String, String> fetchParentIdsForSegments( | ||
| TaskToolbox toolbox, | ||
| Map<String, DataSegmentPlus> unusedIdToSegmentPlus | ||
| ) | ||
| { | ||
| try { | ||
| return toolbox.getTaskActionClient().submit( | ||
| new RetrieveUpgradedFromSegmentIdsAction(getDataSource(), unusedIdToSegmentPlus.keySet()) | ||
| ).getUpgradedFromSegmentIds(); | ||
| } | ||
| catch (Exception e) { | ||
| // Do not proceed with killing these segments as we cannot be sure if their | ||
| // load spec is shared by any other segment or not. If load spec is shared, | ||
| // segment files cannot be deleted from deep store. If load spec is not | ||
| // shared, segments cannot be deleted from metadata store as that would | ||
| // leave deep store files orphaned, and they would never be cleaned up. | ||
| throw new ISE( | ||
| e, | ||
| "Could not retrieve parent segment ids using task action[retrieveUpgradedFromSegmentIds]." | ||
| + " Stopping kill task to avoid data loss in case the segment files" | ||
| + " are shared by other segments." | ||
|
Comment on lines
+388
to
+391
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. What do you think about adding a unit test 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. Thanks for the suggestion! Added some tests. |
||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -387,21 +425,20 @@ private NavigableMap<DateTime, List<TaskLock>> getNonRevokedTaskLockMap(TaskActi | |
| * @return list of segments to kill from deep storage | ||
| */ | ||
| private List<DataSegment> getKillableSegments( | ||
| List<DataSegment> unusedSegments, | ||
| Map<String, DataSegmentPlus> unusedSegments, | ||
| Map<String, String> upgradedFromSegmentIds, | ||
| Set<Map<String, Object>> usedSegmentLoadSpecs, | ||
| TaskActionClient taskActionClient | ||
| ) | ||
| { | ||
|
|
||
| // Determine parentId for each unused segment | ||
| // Determine parentId (or self, if no parent) for each unused segment | ||
| final Map<String, Set<DataSegment>> parentIdToUnusedSegments = new HashMap<>(); | ||
| for (DataSegment segment : unusedSegments) { | ||
| final String segmentId = segment.getId().toString(); | ||
| for (Map.Entry<String, DataSegmentPlus> entry : unusedSegments.entrySet()) { | ||
| final String segmentId = entry.getKey(); | ||
| parentIdToUnusedSegments.computeIfAbsent( | ||
| upgradedFromSegmentIds.getOrDefault(segmentId, segmentId), | ||
| k -> new HashSet<>() | ||
| ).add(segment); | ||
| ).add(entry.getValue().getDataSegment()); | ||
| } | ||
|
|
||
| // Check if the parent or any of its children exist in metadata store | ||
|
|
@@ -411,10 +448,11 @@ private List<DataSegment> getKillableSegments( | |
| ); | ||
| if (response != null && response.getUpgradedToSegmentIds() != null) { | ||
| response.getUpgradedToSegmentIds().forEach((parent, children) -> { | ||
| if (!CollectionUtils.isNullOrEmpty(children)) { | ||
| // Do not kill segment if its parent or any of its siblings still exist in metadata store | ||
| if (!unusedSegments.keySet().containsAll(children)) { | ||
| // Do not kill segment if its load spec is shared by another segment | ||
| // which is not being killed. | ||
| LOG.info( | ||
| "Skipping kill of segments[%s] as its load spec is also used by segment IDs[%s].", | ||
| "Skipping kill of segments[%s] as its load spec is shared by segment IDs[%s].", | ||
| parentIdToUnusedSegments.get(parent), children | ||
| ); | ||
| parentIdToUnusedSegments.remove(parent); | ||
|
|
@@ -423,10 +461,14 @@ private List<DataSegment> getKillableSegments( | |
| } | ||
| } | ||
| catch (Exception e) { | ||
| LOG.warn( | ||
| // Do not proceed with the kill of any segment as we cannot be sure if their | ||
| // load specs are shared by any other segment | ||
| throw new ISE( | ||
| e, | ||
| "Could not retrieve referenced ids using task action[retrieveUpgradedToSegmentIds]." | ||
| + " Overlord may be on an older version." | ||
| "Could not perform task action[retrieveUpgradedToSegmentIds] to retrieve" | ||
| + " segment IDs which share load specs with segments being killed." | ||
| + " Stopping kill task to avoid data loss in case the segment files" | ||
| + " are shared by other segments." | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -449,7 +491,7 @@ private boolean isSegmentLoadSpecPresentIn( | |
| { | ||
| boolean isPresent = usedSegmentLoadSpecs.contains(segment.getLoadSpec()); | ||
| if (isPresent) { | ||
| LOG.info("Skipping kill of segment[%s] as its load spec is also used by other segments.", segment); | ||
| LOG.info("Skipping kill of segment[%s] as its load spec is shared by other 'used' segments.", segment); | ||
| } | ||
| return isPresent; | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.