This is an automated email from the ASF dual-hosted git repository.
kfaraz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new be27640963c perf: Improve efficiency of embedded kill tasks (#19772)
be27640963c is described below
commit be27640963ce7aae29659146d01b2ec987146c68
Author: Kashif Faraz <[email protected]>
AuthorDate: Fri Aug 7 09:35:43 2026 +0530
perf: Improve efficiency of embedded kill tasks (#19772)
Description
-----------
The `UnusedSegmentsKiller` currenly has some limitations:
- Wasted cycles: The kill queue contains jobs for all intervals which have
an unused segment,
regardless of whether the unused segment is eligible for kill or not.
- Slow progress: For intervals with a large number of killable unused
segments, a single
embedded kill task is launched which would kill only 1000 segments in one
go. In such cases,
the `UnusedSegmentsKiller` would take very long to clear a backlog of
killable unused segments.
- Many metadata queries: When the kill queue is rebuilt, heavy metadata
queries are fired for
each datasource in the DB, even if they don't have any unused segments,
killable or otherwise.
Changes
---------
- While rebuilding the kill queue, fire a single metadata query to retrieve
the intervals containing
killable unused segments of all datasources.
- This avoids firing multiple queries, one for each datasource in the DB.
- It also ensures that every embedded kill task actually kills atleast 1
unused segment.
- In the above query, scan upto a maximum of 200k eligible unused segments.
This would also
be the maximum number of segments killed when the queue has been fully
processed.
- This number is large enough to make meaningful progress in each cycle
of the
`UnusedSegmentsKiller` and small enough to keep the query time within ~5s
(tried on MySQL 8
with 17M unused segments).
- Pass in the max segments to kill in each `KillCandidate`.
- Allow each kill task to kill multiple batches of segments, upto a maximum
of 10 (previously 1).
---
docs/operations/metrics.md | 1 +
.../indexing/common/actions/SegmentNukeAction.java | 13 ++-
.../druid/indexing/common/actions/TaskLocks.java | 56 ++++++++++
.../common/task/KillUnusedSegmentsTask.java | 49 ++++++---
.../overlord/duty/UnusedSegmentsKiller.java | 121 +++++++++++++--------
.../overlord/supervisor/SupervisorManager.java | 22 +---
.../common/task/KillUnusedSegmentsTaskTest.java | 2 +-
.../overlord/duty/UnusedSegmentsKillerTest.java | 107 ++++++++++++------
.../http/OverlordDataSourcesResourceTest.java | 3 +-
.../TestIndexerMetadataStorageCoordinator.java | 11 ++
.../apache/druid/timeline/DatasourceInterval.java | 29 +++++
.../IndexerMetadataStorageCoordinator.java | 11 ++
.../IndexerSQLMetadataStorageCoordinator.java | 13 +++
.../metadata/SegmentsMetadataManagerConfig.java | 2 +-
.../druid/metadata/SqlSegmentsMetadataQuery.java | 95 +++++++++++++++-
.../druid/metadata/UnusedSegmentKillerConfig.java | 39 ++++++-
.../IndexerSQLMetadataStorageCoordinatorTest.java | 35 +++++-
17 files changed, 484 insertions(+), 125 deletions(-)
diff --git a/docs/operations/metrics.md b/docs/operations/metrics.md
index 3943b59f076..c05f6cc91f9 100644
--- a/docs/operations/metrics.md
+++ b/docs/operations/metrics.md
@@ -418,6 +418,7 @@ These metrics are emitted only if [auto-kill of unused
segments](../data-managem
|`segment/killed/metadataStore/count`|Number of segments permanently deleted
from the metadata store.|`taskId`, `groupId`, `taskType`(=`kill`), `dataSource`|
|`segment/killed/deepStorage/count`|Number of segments permanently deleted
from the deep storage.|`taskId`, `groupId`, `taskType`(=`kill`), `dataSource`|
|`segment/kill/unusedIntervals/count`|Number of intervals containing unused
segments for a given datasource.|`dataSource`|
+|`segment/kill/eligibleSegments/count`|Number of unused segments in an
interval that are eligible for kill.|`dataSource`, `interval`|
|`segment/kill/skippedIntervals/count`|Number of intervals that were skipped
for kill due to being already locked by another task.|`taskId`, `groupId`,
`taskType`(=`kill`), `dataSource`|
|`segment/kill/queueReset/time`|Time taken in milliseconds to reset the kill
queue.||
|`segment/kill/queueProcess/time`|Time taken in milliseconds to fully process
the kill queue.||
diff --git
a/indexing-service/src/main/java/org/apache/druid/indexing/common/actions/SegmentNukeAction.java
b/indexing-service/src/main/java/org/apache/druid/indexing/common/actions/SegmentNukeAction.java
index 6c73f664e3f..b9e6f035a80 100644
---
a/indexing-service/src/main/java/org/apache/druid/indexing/common/actions/SegmentNukeAction.java
+++
b/indexing-service/src/main/java/org/apache/druid/indexing/common/actions/SegmentNukeAction.java
@@ -39,8 +39,10 @@ import java.util.stream.Collectors;
/**
* Permanently deletes unused segments from the metadata store.
+ * {@link #perform(Task, TaskActionToolbox)} returns the number of segments
+ * deleted from the metadata store, but older versions return a null value.
*/
-public class SegmentNukeAction implements TaskAction<Void>
+public class SegmentNukeAction implements TaskAction<Integer>
{
private static final Logger log = new Logger(SegmentNukeAction.class);
@@ -61,19 +63,20 @@ public class SegmentNukeAction implements TaskAction<Void>
}
@Override
- public TypeReference<Void> getReturnTypeReference()
+ public TypeReference<Integer> getReturnTypeReference()
{
return new TypeReference<>() {};
}
@Override
- public Void perform(Task task, TaskActionToolbox toolbox)
+ public Integer perform(Task task, TaskActionToolbox toolbox)
{
TaskLocks.checkLockCoversSegments(task, toolbox.getTaskLockbox(),
segments);
+ final int numDeletedSegments;
try {
final Set<Interval> intervals =
segments.stream().map(DataSegment::getInterval).collect(Collectors.toSet());
- int numDeletedSegments = toolbox.getTaskLockbox().doInCriticalSection(
+ numDeletedSegments = toolbox.getTaskLockbox().doInCriticalSection(
task,
intervals,
CriticalAction.<Integer>builder().onValidLocks(
@@ -106,7 +109,7 @@ public class SegmentNukeAction implements TaskAction<Void>
toolbox.getEmitter().emit(metricBuilder.setMetric("segment/nuked/bytes",
segment.getSize()));
}
- return null;
+ return numDeletedSegments;
}
@Override
diff --git
a/indexing-service/src/main/java/org/apache/druid/indexing/common/actions/TaskLocks.java
b/indexing-service/src/main/java/org/apache/druid/indexing/common/actions/TaskLocks.java
index 82c8990c748..3646d4a9048 100644
---
a/indexing-service/src/main/java/org/apache/druid/indexing/common/actions/TaskLocks.java
+++
b/indexing-service/src/main/java/org/apache/druid/indexing/common/actions/TaskLocks.java
@@ -33,6 +33,7 @@ import org.apache.druid.indexing.overlord.GlobalTaskLockbox;
import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.metadata.ReplaceTaskLock;
+import org.apache.druid.query.QueryContexts;
import org.apache.druid.timeline.DataSegment;
import org.joda.time.DateTime;
import org.joda.time.Interval;
@@ -170,6 +171,61 @@ public class TaskLocks
}
}
+ /**
+ * Checks if an append task with the given context should use concurrent
locks.
+ */
+ public static boolean shouldUseConcurrentLocksForAppend(Map<String, Object>
context, boolean defaultValue)
+ {
+ return shouldUseConcurrentLocks(context, defaultValue,
TaskLockType.APPEND);
+ }
+
+ /**
+ * Checks if a replace task with the given context should use concurrent
locks.
+ */
+ public static boolean shouldUseConcurrentLocksForReplace(Map<String, Object>
context, boolean defaultValue)
+ {
+ return shouldUseConcurrentLocks(context, defaultValue,
TaskLockType.REPLACE);
+ }
+
+ /**
+ * Checks if an append or replace task with the given context should use
+ * concurrent locks.
+ *
+ * @param context Context used by the task.
+ * @param defaultValue Default behaviour in case the context does
not
+ * explicitly specify any value for {@code
useConcurrentLocks}
+ * or {@code taskLockType}.
+ * @param validConcurrentLockType Expected concurrent lock type (APPEND or
REPLACE)
+ * for the given task.
+ */
+ private static boolean shouldUseConcurrentLocks(
+ Map<String, Object> context,
+ boolean defaultValue,
+ TaskLockType validConcurrentLockType
+ )
+ {
+ if (context == null) {
+ return defaultValue;
+ }
+ Boolean useConcurrentLocks = QueryContexts.getAsBoolean(
+ Tasks.USE_CONCURRENT_LOCKS,
+ context.get(Tasks.USE_CONCURRENT_LOCKS)
+ );
+ if (useConcurrentLocks != null) {
+ return useConcurrentLocks;
+ }
+ TaskLockType taskLockType = QueryContexts.getAsEnum(
+ Tasks.TASK_LOCK_TYPE,
+ context.get(Tasks.TASK_LOCK_TYPE),
+ TaskLockType.class
+ );
+ if (taskLockType != null) {
+ return taskLockType == validConcurrentLockType;
+ }
+
+ return defaultValue;
+ }
+
/**
* Finds locks of type {@link TaskLockType#REPLACE} for each of the given
segments
* that have an interval completely covering the interval of the respective
segments.
diff --git
a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/KillUnusedSegmentsTask.java
b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/KillUnusedSegmentsTask.java
index 21b9e3f6a79..e31ac044d89 100644
---
a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/KillUnusedSegmentsTask.java
+++
b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/KillUnusedSegmentsTask.java
@@ -47,6 +47,7 @@ import org.apache.druid.indexing.overlord.Segments;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.query.QueryContexts;
import org.apache.druid.server.coordination.BroadcastDatasourceLoadingSpec;
import org.apache.druid.server.http.DataSegmentPlus;
import org.apache.druid.server.lookup.cache.LookupLoadingSpec;
@@ -87,10 +88,16 @@ import java.util.stream.Collectors;
* <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.
+ * <p>
+ * <b>Use concurrent locks:</b> A kill task using REPLACE locks (enabled by
+ * {@link Tasks#USE_CONCURRENT_LOCKS}) is affected by other tasks in the
following
+ * manner:
+ * <ul>
+ * <li>Not affected by tasks using EXCLUSIVE, SHARED or another REPLACE lock
+ * since they are mutually exclusive with this REPLACE lock.</li>
+ * <li>Not affected by APPEND tasks since they can only upgrade used segments
+ * and do not modify unused segments.</li>
+ * </ul>
*/
public class KillUnusedSegmentsTask extends AbstractFixedIntervalTask
{
@@ -210,6 +217,7 @@ public class KillUnusedSegmentsTask extends
AbstractFixedIntervalTask
{
// Track stats for reporting
int numSegmentsKilled = 0;
+ int totalSegmentsDeletedFromMetadataStore = 0;
int numBatchesProcessed = 0;
// List unused segments
@@ -294,14 +302,23 @@ public class KillUnusedSegmentsTask extends
AbstractFixedIntervalTask
}
// 3. Nuke all eligible unused segments
- taskActionClient.submit(new SegmentNukeAction(unusedSegments));
- emitMetric(toolbox.getEmitter(),
TaskMetrics.SEGMENTS_DELETED_FROM_METADATA_STORE, unusedIdToSegmentPlus.size());
+ final Number nukeResult = taskActionClient.submit(new
SegmentNukeAction(unusedSegments));
+
+ // Older versions of the Overlord return a null value from the
SegmentNukeAction
+ final int numSegmentsDeletedFromMetadataStore =
+ nukeResult == null ? unusedSegments.size() : nukeResult.intValue();
+ emitMetric(
+ toolbox.getEmitter(),
+ TaskMetrics.SEGMENTS_DELETED_FROM_METADATA_STORE,
+ numSegmentsDeletedFromMetadataStore
+ );
// 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++;
+ totalSegmentsDeletedFromMetadataStore +=
numSegmentsDeletedFromMetadataStore;
numSegmentsKilled += segmentsToKillFromDeepStore.size();
logInfo("Processed [%d] batches for kill task[%s].",
numBatchesProcessed, getId());
@@ -312,8 +329,10 @@ public class KillUnusedSegmentsTask extends
AbstractFixedIntervalTask
final String taskId = getId();
logInfo(
"Finished kill task[%s] for dataSource[%s] and interval[%s]."
- + " Deleted total [%d] unused segments in [%d] batches.",
- taskId, getDataSource(), getInterval(), numSegmentsKilled,
numBatchesProcessed
+ + " Deleted [%d] unused segments from metadata store and files for"
+ + " [%d] segments from deep store in [%d] batches.",
+ taskId, getDataSource(), getInterval(),
+ totalSegmentsDeletedFromMetadataStore, numSegmentsKilled,
numBatchesProcessed
);
final KillTaskReport.Stats stats =
@@ -512,13 +531,13 @@ public class KillUnusedSegmentsTask extends
AbstractFixedIntervalTask
public boolean isReady(TaskActionClient taskActionClient) throws Exception
{
final boolean useConcurrentLocks = Boolean.TRUE.equals(
- getContextValue(
+ QueryContexts.getAsBoolean(
Tasks.USE_CONCURRENT_LOCKS,
- Tasks.DEFAULT_USE_CONCURRENT_LOCKS
+ getContextValue(Tasks.USE_CONCURRENT_LOCKS)
)
);
- TaskLockType actualLockType = determineLockType(useConcurrentLocks);
+ final TaskLockType actualLockType = determineLockType(useConcurrentLocks);
final TaskLock lock = taskActionClient.submit(
new TimeChunkLockTryAcquireAction(
@@ -539,9 +558,13 @@ public class KillUnusedSegmentsTask extends
AbstractFixedIntervalTask
if (useConcurrentLocks) {
actualLockType = TaskLockType.REPLACE;
} else {
- actualLockType = getContextValue(Tasks.TASK_LOCK_TYPE,
TaskLockType.EXCLUSIVE);
+ actualLockType = QueryContexts.getAsEnum(
+ Tasks.TASK_LOCK_TYPE,
+ getContextValue(Tasks.TASK_LOCK_TYPE),
+ TaskLockType.class,
+ TaskLockType.EXCLUSIVE
+ );
}
return actualLockType;
}
-
}
diff --git
a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/duty/UnusedSegmentsKiller.java
b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/duty/UnusedSegmentsKiller.java
index dd23add68d7..a5f8ffa9dc9 100644
---
a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/duty/UnusedSegmentsKiller.java
+++
b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/duty/UnusedSegmentsKiller.java
@@ -34,6 +34,7 @@ import org.apache.druid.indexing.common.task.TaskMetrics;
import org.apache.druid.indexing.common.task.Tasks;
import org.apache.druid.indexing.overlord.GlobalTaskLockbox;
import org.apache.druid.indexing.overlord.IndexerMetadataStorageCoordinator;
+import org.apache.druid.indexing.overlord.config.DefaultTaskConfig;
import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.java.util.common.Stopwatch;
import org.apache.druid.java.util.common.concurrent.ScheduledExecutorFactory;
@@ -46,15 +47,14 @@ import org.apache.druid.metadata.UnusedSegmentKillerConfig;
import org.apache.druid.query.DruidMetrics;
import org.apache.druid.segment.loading.DataSegmentKiller;
import org.apache.druid.server.http.DataSegmentPlus;
+import org.apache.druid.timeline.DatasourceInterval;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Interval;
-import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
@@ -63,11 +63,12 @@ import java.util.concurrent.atomic.AtomicReference;
/**
* {@link OverlordDuty} to delete unused segments from metadata store and the
* deep storage. Launches {@link EmbeddedKillTask}s to clean unused segments
- * of a single datasource-interval.
+ * of a single datasource-interval. These tasks use EXCLUSIVE locks by default.
*
- * @see SegmentsMetadataManagerConfig to enable the cleanup
- * @see org.apache.druid.server.coordinator.duty.KillUnusedSegments for legacy
- * mode of killing unused segments via Coordinator duties
+ * @see SegmentsMetadataManagerConfig#getKillUnused() Config to enable the
cleanup
+ * @see org.apache.druid.server.coordinator.duty.KillUnusedSegments
+ * Legacy mode of killing segments via Coordinator duties
+ * @see KillUnusedSegmentsTask Behaviour of kill task with concurrent locks
*/
public class UnusedSegmentsKiller implements OverlordDuty
{
@@ -76,8 +77,14 @@ public class UnusedSegmentsKiller implements OverlordDuty
private static final String TASK_ID_PREFIX = "overlord-issued";
private static final int INITIAL_KILL_QUEUE_SIZE = 1000;
- private static final int MAX_INTERVALS_TO_KILL_IN_DATASOURCE = 10_000;
- private static final int MAX_SEGMENTS_TO_KILL_IN_INTERVAL = 1000;
+ private static final int MAX_INTERVALS_TO_KILL = 10_000;
+ private static final int MAX_SEGMENTS_TO_KILL_IN_BATCH = 1000;
+
+ /**
+ * Keep max segments to kill in a single kill task small so that the
EXCLUSIVE
+ * lock on the underlying interval is not held for too long.
+ */
+ private static final int MAX_SEGMENTS_TO_KILL_IN_TASK = 10 *
MAX_SEGMENTS_TO_KILL_IN_BATCH;
/**
* Period after which the queue is reset even if there are existing jobs in
queue.
@@ -85,9 +92,11 @@ public class UnusedSegmentsKiller implements OverlordDuty
private static final Duration QUEUE_RESET_PERIOD = Duration.standardDays(1);
/**
- * Duration for which a kill task is allowed to run.
+ * Duration for which a kill task is allowed to run. Assuming that processing
+ * each batch of segments takes around 30s, each kill task (upto 10 batches)
+ * should normally finish in 5 minutes.
*/
- private static final Duration MAX_TASK_DURATION =
Duration.standardMinutes(10);
+ private static final Duration MAX_TASK_DURATION =
Duration.standardMinutes(30);
private final ServiceEmitter emitter;
private final GlobalTaskLockbox taskLockbox;
@@ -116,6 +125,7 @@ public class UnusedSegmentsKiller implements OverlordDuty
@Inject
public UnusedSegmentsKiller(
SegmentsMetadataManagerConfig config,
+ DefaultTaskConfig defaultTaskConfig,
TaskActionClientFactory taskActionClientFactory,
IndexerMetadataStorageCoordinator storageCoordinator,
@IndexingService DruidLeaderSelector leaderSelector,
@@ -139,7 +149,7 @@ public class UnusedSegmentsKiller implements OverlordDuty
this.killQueue = new PriorityBlockingQueue<>(
INITIAL_KILL_QUEUE_SIZE,
Ordering.from(Comparators.intervalsByEndThenStart())
- .onResultOf(candidate -> candidate.interval)
+ .onResultOf(KillCandidate::interval)
);
} else {
this.exec = null;
@@ -193,7 +203,7 @@ public class UnusedSegmentsKiller implements OverlordDuty
// Schedule the first run after some delay since the segment metadata
cache
// might take time for the sync to finish if this Overlord has just
started.
final long periodMillis =
killConfig.getDutyPeriod().toStandardDuration().getMillis();
- final long initialDelayMillis = periodMillis / 4;
+ final long initialDelayMillis = periodMillis / 2;
log.info(
"Unused segment killer is enabled and will start after [%d] millis."
@@ -245,22 +255,53 @@ public class UnusedSegmentsKiller implements OverlordDuty
return;
}
- final Set<String> dataSources =
storageCoordinator.retrieveAllDatasourceNames();
-
final Map<String, Integer> dataSourceToIntervalCounts = new HashMap<>();
- for (String dataSource : dataSources) {
- storageCoordinator.retrieveSomeUnusedSegmentIntervals(dataSource,
MAX_INTERVALS_TO_KILL_IN_DATASOURCE).forEach(
- interval -> {
- dataSourceToIntervalCounts.merge(dataSource, 1, Integer::sum);
- killQueue.offer(new KillCandidate(dataSource, interval));
- }
+
+ // Identify intervals with unused segments which are eligible for kill
+ final Map<DatasourceInterval, Integer> eligibleIntervals =
+ storageCoordinator.retrieveSomeUnusedSegmentIntervals(
+ DateTimes.nowUtc().minus(killConfig.getBufferPeriod()),
+ MAX_INTERVALS_TO_KILL,
+ killConfig.getMaxSegmentsToKill()
+ );
+
+ // Add kill candidates to the queue
+ eligibleIntervals.forEach((entry, numEligibleSegments) -> {
+ dataSourceToIntervalCounts.merge(entry.dataSource(), 1, Integer::sum);
+
+ // Queue multiple candidates for the same datasource-interval if the
+ // number of segments to kill is large to avoid holding a lock for too
long
+ int remainingSegmentsToKill = numEligibleSegments;
+ while (remainingSegmentsToKill > 0) {
+ int numSegmentsToKill
+ = Math.min(remainingSegmentsToKill,
MAX_SEGMENTS_TO_KILL_IN_TASK);
+ remainingSegmentsToKill -= numSegmentsToKill;
+
+ // If only a few segments remain, add them to the same candidate
+ if (remainingSegmentsToKill < MAX_SEGMENTS_TO_KILL_IN_BATCH) {
+ numSegmentsToKill += remainingSegmentsToKill;
+ remainingSegmentsToKill = 0;
+ }
+
+ killQueue.offer(
+ new KillCandidate(entry.dataSource(), entry.interval(),
numSegmentsToKill)
+ );
+ }
+
+ emitMetric(
+ Metric.ELIGIBLE_UNUSED_SEGMENTS,
+ numEligibleSegments,
+ Map.of(
+ DruidMetrics.DATASOURCE, entry.dataSource(),
+ DruidMetrics.INTERVAL, entry.interval().toString()
+ )
);
- }
+ });
lastResetTime.set(DateTimes.nowUtc());
log.info(
"Queued [%d] kill jobs for [%d] datasources in [%d] millis.",
- killQueue.size(), dataSources.size(), resetDuration.millisElapsed()
+ killQueue.size(), dataSourceToIntervalCounts.size(),
resetDuration.millisElapsed()
);
dataSourceToIntervalCounts.forEach(
(dataSource, intervalCount) -> emitMetric(
@@ -306,8 +347,8 @@ public class UnusedSegmentsKiller implements OverlordDuty
final String taskId = IdUtils.newTaskId(
TASK_ID_PREFIX,
KillUnusedSegmentsTask.TYPE,
- candidate.dataSource,
- candidate.interval
+ candidate.dataSource(),
+ candidate.interval()
);
final Future<?> taskFuture = exec.submit(() -> {
@@ -339,6 +380,7 @@ public class UnusedSegmentsKiller implements OverlordDuty
final ServiceMetricEvent.Builder metricBuilder = new
ServiceMetricEvent.Builder();
IndexTaskUtils.setTaskDimensions(metricBuilder, killTask);
+ metricBuilder.setDimension(DruidMetrics.INTERVAL, candidate.interval());
try {
taskLockbox.add(killTask);
@@ -361,7 +403,7 @@ public class UnusedSegmentsKiller implements OverlordDuty
}
finally {
cleanupLocksSilently(killTask);
- emitMetric(Metric.PROCESSED_KILL_JOBS, 1L,
Map.of(DruidMetrics.DATASOURCE, candidate.dataSource));
+ emitMetric(Metric.PROCESSED_KILL_JOBS, 1L,
Map.of(DruidMetrics.DATASOURCE, candidate.dataSource()));
}
}
@@ -387,16 +429,9 @@ public class UnusedSegmentsKiller implements OverlordDuty
/**
* Represents a single candidate interval that contains unused segments.
*/
- private static class KillCandidate
+ private record KillCandidate(String dataSource, Interval interval, int
numSegmentsToKill)
{
- private final String dataSource;
- private final Interval interval;
- private KillCandidate(String dataSource, Interval interval)
- {
- this.dataSource = dataSource;
- this.interval = interval;
- }
}
/**
@@ -436,33 +471,24 @@ public class UnusedSegmentsKiller implements OverlordDuty
{
super(
taskId,
- candidate.dataSource,
- candidate.interval,
+ candidate.dataSource(),
+ candidate.interval(),
null,
Map.of(Tasks.PRIORITY_KEY,
Tasks.DEFAULT_EMBEDDED_KILL_TASK_PRIORITY),
- null,
- null,
+ MAX_SEGMENTS_TO_KILL_IN_BATCH,
+ candidate.numSegmentsToKill(),
maxUpdatedTimeOfEligibleSegment
);
}
- @Nullable
- @Override
- protected Integer getNumTotalBatches()
- {
- // Do everything in a single batch so that locks are not held for very
long
- return 1;
- }
-
@Override
protected List<DataSegmentPlus> fetchNextBatchOfUnusedSegments(TaskToolbox
toolbox, int nextBatchSize)
{
- // Kill only 1000 segments in the batch so that locks are not held for
very long
return storageCoordinator.retrieveUnusedSegmentsWithExactInterval(
getDataSource(),
getInterval(),
getMaxUsedStatusLastUpdatedTime(),
- MAX_SEGMENTS_TO_KILL_IN_INTERVAL
+ nextBatchSize
);
}
@@ -503,5 +529,6 @@ public class UnusedSegmentsKiller implements OverlordDuty
public static final String SKIPPED_INTERVALS =
"segment/kill/skippedIntervals/count";
public static final String UNUSED_SEGMENT_INTERVALS =
"segment/kill/unusedIntervals/count";
+ public static final String ELIGIBLE_UNUSED_SEGMENTS =
"segment/kill/eligibleSegments/count";
}
}
diff --git
a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorManager.java
b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorManager.java
index 7c75e5a9642..d99fcfa9e68 100644
---
a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorManager.java
+++
b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorManager.java
@@ -32,7 +32,7 @@ import org.apache.druid.error.DruidException;
import org.apache.druid.error.InvalidInput;
import org.apache.druid.error.NotFound;
import org.apache.druid.guice.annotations.Json;
-import org.apache.druid.indexing.common.TaskLockType;
+import org.apache.druid.indexing.common.actions.TaskLocks;
import org.apache.druid.indexing.common.task.Tasks;
import org.apache.druid.indexing.overlord.DataSourceMetadata;
import
org.apache.druid.indexing.overlord.supervisor.autoscaler.SupervisorTaskAutoScaler;
@@ -49,7 +49,6 @@ import org.apache.druid.java.util.emitter.EmittingLogger;
import org.apache.druid.metadata.MetadataSupervisorManager;
import org.apache.druid.metadata.PendingSegmentRecord;
import org.apache.druid.query.DefaultQueryMetrics;
-import org.apache.druid.query.QueryContexts;
import org.apache.druid.segment.incremental.ParseExceptionReport;
import org.apache.druid.server.metrics.SupervisorStatsProvider;
@@ -778,22 +777,9 @@ public class SupervisorManager implements
SupervisorStatsProvider
*/
private static boolean specHasConcurrentLocks(SeekableStreamSupervisorSpec
spec)
{
- Map<String, Object> context = spec.getContext();
- if (context == null) {
- return Tasks.DEFAULT_USE_CONCURRENT_LOCKS;
- }
- Boolean useConcurrentLocks = QueryContexts.getAsBoolean(
- Tasks.USE_CONCURRENT_LOCKS,
- context.get(Tasks.USE_CONCURRENT_LOCKS)
- );
- if (useConcurrentLocks != null) {
- return useConcurrentLocks;
- }
- TaskLockType taskLockType = QueryContexts.getAsEnum(
- Tasks.TASK_LOCK_TYPE,
- context.get(Tasks.TASK_LOCK_TYPE),
- TaskLockType.class
+ return TaskLocks.shouldUseConcurrentLocksForAppend(
+ spec.getContext(),
+ Tasks.DEFAULT_USE_CONCURRENT_LOCKS
);
- return taskLockType == TaskLockType.APPEND;
}
}
diff --git
a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/KillUnusedSegmentsTaskTest.java
b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/KillUnusedSegmentsTaskTest.java
index a84d28d2f15..90070060b9c 100644
---
a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/KillUnusedSegmentsTaskTest.java
+++
b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/KillUnusedSegmentsTaskTest.java
@@ -1406,7 +1406,7 @@ public class KillUnusedSegmentsTaskTest extends
IngestionTestBase
{
final KillUnusedSegmentsTask task = new KillUnusedSegmentsTaskBuilder()
.dataSource(DATA_SOURCE)
- .context(ImmutableMap.of(Tasks.TASK_LOCK_TYPE, TaskLockType.APPEND))
+ .context(ImmutableMap.of(Tasks.TASK_LOCK_TYPE,
TaskLockType.APPEND.name()))
.interval(Intervals.of("2019-03-01/2019-04-01"))
.build();
diff --git
a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/duty/UnusedSegmentsKillerTest.java
b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/duty/UnusedSegmentsKillerTest.java
index 7f0d718b0b9..1c8d885c924 100644
---
a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/duty/UnusedSegmentsKillerTest.java
+++
b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/duty/UnusedSegmentsKillerTest.java
@@ -28,6 +28,7 @@ import org.apache.druid.indexing.common.task.TaskMetrics;
import org.apache.druid.indexing.overlord.GlobalTaskLockbox;
import org.apache.druid.indexing.overlord.IndexerMetadataStorageCoordinator;
import org.apache.druid.indexing.overlord.TimeChunkLockRequest;
+import org.apache.druid.indexing.overlord.config.DefaultTaskConfig;
import org.apache.druid.indexing.test.TestDataSegmentKiller;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.Intervals;
@@ -82,7 +83,7 @@ public class UnusedSegmentsKillerTest
emitter = taskActionTestKit.getServiceEmitter();
leaderSelector = new TestDruidLeaderSelector();
dataSegmentKiller = new TestDataSegmentKiller();
- killerConfig = new UnusedSegmentKillerConfig(true, Period.ZERO, null);
+ killerConfig = new UnusedSegmentKillerConfig(true, Period.ZERO, null,
null);
killExecutor = new BlockingExecutorService("UnusedSegmentsKillerTest-%s");
storageCoordinator = taskActionTestKit.getMetadataStorageCoordinator();
initKiller();
@@ -96,6 +97,7 @@ public class UnusedSegmentsKillerTest
SegmentMetadataCache.UsageMode.ALWAYS,
killerConfig
),
+ new DefaultTaskConfig(),
taskActionTestKit::createTaskActionClient,
storageCoordinator,
leaderSelector,
@@ -106,6 +108,14 @@ public class UnusedSegmentsKillerTest
);
}
+ private void resetKillQueue()
+ {
+ killer.run();
+
+ // Invoke rebuild of kill queue on the executor thread
+ killExecutor.finishNextPendingTask();
+ }
+
private void finishQueuedKillJobs()
{
killExecutor.finishAllPendingTasks();
@@ -116,13 +126,13 @@ public class UnusedSegmentsKillerTest
{
final DutySchedule schedule = killer.getSchedule();
Assert.assertEquals(Duration.standardHours(1).getMillis(),
schedule.getPeriodMillis());
- Assert.assertEquals(Duration.standardMinutes(15).getMillis(),
schedule.getInitialDelayMillis());
+ Assert.assertEquals(Duration.standardMinutes(30).getMillis(),
schedule.getInitialDelayMillis());
}
@Test
public void test_getSchedule_returnsZeroPeriod_ifDisabled()
{
- killerConfig = new UnusedSegmentKillerConfig(false, null, null);
+ killerConfig = new UnusedSegmentKillerConfig(false, null, null, null);
initKiller();
final DutySchedule schedule = killer.getSchedule();
@@ -143,7 +153,7 @@ public class UnusedSegmentsKillerTest
@Test
public void test_run_isNoop_ifDisabled()
{
- killerConfig = new UnusedSegmentKillerConfig(false, null, null);
+ killerConfig = new UnusedSegmentKillerConfig(false, null, null, null);
initKiller();
Assert.assertFalse(killer.isEnabled());
@@ -201,68 +211,96 @@ public class UnusedSegmentsKillerTest
}
@Test
- public void test_maxSegmentsKilledInAnInterval_is_1k()
+ public void test_maxSegmentsKilledInRun_isLimitedByConfig()
{
+ killerConfig = new UnusedSegmentKillerConfig(true, Period.ZERO, null, 700);
+ initKiller();
leaderSelector.becomeLeader();
final List<DataSegment> segments =
CreateDataSegments.ofDatasource(TestDataSource.WIKI)
- .forIntervals(1, Granularities.DAY)
- .withNumPartitions(2000)
+ .forIntervals(10, Granularities.DAY)
+ .withNumPartitions(100)
.eachOfSizeInMb(50);
storageCoordinator.commitSegments(Set.copyOf(segments), null);
storageCoordinator.markAllSegmentsAsUnused(TestDataSource.WIKI);
Assert.assertEquals(
- 2000,
- retrieveUnusedSegments(segments.get(0).getInterval()).size()
+ 1000,
+ retrieveUnusedSegments(Intervals.ETERNITY).size()
);
- // Reset the kill queue and execute kill tasks
- killer.run();
+ resetKillQueue();
finishQueuedKillJobs();
- // Verify that a single kill task has run which killed 1k segments
- emitter.verifyEmitted(TaskMetrics.RUN_DURATION, 1);
- emitter.verifySum(TaskMetrics.SEGMENTS_DELETED_FROM_METADATA_STORE, 1000L);
-
- Assert.assertEquals(
- 1000,
- retrieveUnusedSegments(segments.get(0).getInterval()).size()
- );
+ // Verify that a total of 700 segments were identified for kill
+ emitter.verifySum(UnusedSegmentsKiller.Metric.ELIGIBLE_UNUSED_SEGMENTS,
700L);
+ emitter.verifySum(TaskMetrics.SEGMENTS_DELETED_FROM_DEEPSTORE, 700L);
}
@Test(timeout = 20_000L)
- public void test_maxIntervalsKilledInADatasource_is_10k()
+ public void test_maxSegmentsKilledByTask_is_10k()
{
leaderSelector.becomeLeader();
+ // Create an interval with 12k killable unused segments
final List<DataSegment> segments =
CreateDataSegments.ofDatasource(TestDataSource.WIKI)
- .forIntervals(20_000, Granularities.DAY)
+ .forIntervals(1, Granularities.DAY)
+ .withNumPartitions(12_000)
.eachOfSizeInMb(50);
storageCoordinator.commitSegments(Set.copyOf(segments), null);
storageCoordinator.markAllSegmentsAsUnused(TestDataSource.WIKI);
Assert.assertEquals(
- 20_000,
+ 12_000,
retrieveUnusedSegments(Intervals.ETERNITY).size()
);
- // Reset the kill queue and execute kill tasks
- killer.run();
+ resetKillQueue();
finishQueuedKillJobs();
- // Verify that 10k kill tasks have run, each killing a single segment
- emitter.verifyEmitted(TaskMetrics.RUN_DURATION, 10000);
- emitter.verifySum(TaskMetrics.SEGMENTS_DELETED_FROM_METADATA_STORE,
10_000L);
+ // Verify that all the segments in the interval were eligible for kill
+ emitter.verifySum(UnusedSegmentsKiller.Metric.UNUSED_SEGMENT_INTERVALS,
1L);
+ emitter.verifySum(UnusedSegmentsKiller.Metric.ELIGIBLE_UNUSED_SEGMENTS,
12_000L);
+
+ // Verify that 2 tasks were launched to kill the segments
+ emitter.verifySum(UnusedSegmentsKiller.Metric.PROCESSED_KILL_JOBS, 2L);
+ final List<String> taskIds =
emitter.getMetricEvents(TaskMetrics.RUN_DURATION)
+ .stream()
+ .map(event ->
event.getUserDims().get("taskId").toString())
+ .toList();
+ Assert.assertEquals(2, taskIds.size());
+
+ // Verify that the tasks killed 10k and 2k segments respectively
+ emitter.verifySum(TaskMetrics.SEGMENTS_DELETED_FROM_DEEPSTORE,
Map.of("taskId", taskIds.get(0)), 10_000);
+ emitter.verifySum(TaskMetrics.SEGMENTS_DELETED_FROM_DEEPSTORE,
Map.of("taskId", taskIds.get(1)), 2_000);
+ }
+
+ @Test(timeout = 20_000L)
+ public void test_maxIntervalsKilledInADatasource_is_10k()
+ {
+ leaderSelector.becomeLeader();
+
+ final List<DataSegment> segments =
+ CreateDataSegments.ofDatasource(TestDataSource.WIKI)
+ .forIntervals(10_001, Granularities.DAY)
+ .eachOfSizeInMb(50);
+
+ storageCoordinator.commitSegments(Set.copyOf(segments), null);
+ storageCoordinator.markAllSegmentsAsUnused(TestDataSource.WIKI);
Assert.assertEquals(
- 10_000,
+ 10_001,
retrieveUnusedSegments(Intervals.ETERNITY).size()
);
+
+ resetKillQueue();
+
+ emitter.verifySum(UnusedSegmentsKiller.Metric.UNUSED_SEGMENT_INTERVALS,
10_000L);
+ emitter.verifySum(UnusedSegmentsKiller.Metric.ELIGIBLE_UNUSED_SEGMENTS,
10_000L);
}
@Test
@@ -398,7 +436,7 @@ public class UnusedSegmentsKillerTest
@Test
public void test_run_doesNotKillSegment_ifUpdatedWithinBufferPeriod()
{
- killerConfig = new UnusedSegmentKillerConfig(true, Period.hours(1), null);
+ killerConfig = new UnusedSegmentKillerConfig(true, Period.hours(1), null,
null);
initKiller();
storageCoordinator.commitSegments(Set.copyOf(WIKI_SEGMENTS_1X10D), null);
@@ -408,13 +446,12 @@ public class UnusedSegmentsKillerTest
killer.run();
finishQueuedKillJobs();
- // Verify that tasks are launched but no segment is killed
- emitter.verifyValue(UnusedSegmentsKiller.Metric.UNUSED_SEGMENT_INTERVALS,
10L);
- emitter.verifyEmitted(UnusedSegmentsKiller.Metric.PROCESSED_KILL_JOBS, 10);
- emitter.verifyEmitted(TaskMetrics.RUN_DURATION, 10);
+ // Verify that no tasks are launched
+
emitter.verifyNotEmitted(UnusedSegmentsKiller.Metric.UNUSED_SEGMENT_INTERVALS);
+
emitter.verifyNotEmitted(UnusedSegmentsKiller.Metric.ELIGIBLE_UNUSED_SEGMENTS);
- emitter.verifySum(TaskMetrics.SEGMENTS_DELETED_FROM_METADATA_STORE, 0L);
- emitter.verifySum(TaskMetrics.SEGMENTS_DELETED_FROM_DEEPSTORE, 0L);
+ emitter.verifyNotEmitted(TaskMetrics.SEGMENTS_DELETED_FROM_METADATA_STORE);
+ emitter.verifyNotEmitted(TaskMetrics.SEGMENTS_DELETED_FROM_DEEPSTORE);
}
@Test
diff --git
a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordDataSourcesResourceTest.java
b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordDataSourcesResourceTest.java
index 53af56961d2..5070197c37a 100644
---
a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordDataSourcesResourceTest.java
+++
b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordDataSourcesResourceTest.java
@@ -43,7 +43,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
-import java.util.stream.Collectors;
public class OverlordDataSourcesResourceTest
{
@@ -210,7 +209,7 @@ public class OverlordDataSourcesResourceTest
final String version2 = WIKI_SEGMENTS_10X1D.get(0).getVersion() + "__2";
final List<DataSegment> wikiSegmentsV2 = WIKI_SEGMENTS_10X1D.stream().map(
segment -> DataSegment.builder(segment).version(version2).build()
- ).collect(Collectors.toList());
+ ).toList();
storageCoordinator.commitSegments(Set.copyOf(wikiSegmentsV2), null);
diff --git
a/indexing-service/src/test/java/org/apache/druid/indexing/test/TestIndexerMetadataStorageCoordinator.java
b/indexing-service/src/test/java/org/apache/druid/indexing/test/TestIndexerMetadataStorageCoordinator.java
index 41a30024163..803be668960 100644
---
a/indexing-service/src/test/java/org/apache/druid/indexing/test/TestIndexerMetadataStorageCoordinator.java
+++
b/indexing-service/src/test/java/org/apache/druid/indexing/test/TestIndexerMetadataStorageCoordinator.java
@@ -38,6 +38,7 @@ import
org.apache.druid.segment.realtime.appenderator.SegmentIdWithShardSpec;
import
org.apache.druid.server.coordinator.simulate.TestSegmentsMetadataManager;
import org.apache.druid.server.http.DataSegmentPlus;
import org.apache.druid.timeline.DataSegment;
+import org.apache.druid.timeline.DatasourceInterval;
import org.apache.druid.timeline.SegmentId;
import org.apache.druid.timeline.SegmentTimeline;
import org.joda.time.DateTime;
@@ -75,6 +76,16 @@ public class TestIndexerMetadataStorageCoordinator
implements IndexerMetadataSto
return List.of();
}
+ @Override
+ public Map<DatasourceInterval, Integer> retrieveSomeUnusedSegmentIntervals(
+ DateTime maxUpdatedTime,
+ int maxResultSize,
+ int maxSegmentsToScan
+ )
+ {
+ return Map.of();
+ }
+
@Override
public List<DataSegmentPlus> retrieveUnusedSegmentsWithExactInterval(
String dataSource,
diff --git
a/processing/src/main/java/org/apache/druid/timeline/DatasourceInterval.java
b/processing/src/main/java/org/apache/druid/timeline/DatasourceInterval.java
new file mode 100644
index 00000000000..0e7796c1646
--- /dev/null
+++ b/processing/src/main/java/org/apache/druid/timeline/DatasourceInterval.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.timeline;
+
+import org.joda.time.Interval;
+
+/**
+ * A tuple containing a datasource and an interval.
+ */
+public record DatasourceInterval(String dataSource, Interval interval)
+{
+}
diff --git
a/server/src/main/java/org/apache/druid/indexing/overlord/IndexerMetadataStorageCoordinator.java
b/server/src/main/java/org/apache/druid/indexing/overlord/IndexerMetadataStorageCoordinator.java
index 26dcae7cd1f..2d0193796e6 100644
---
a/server/src/main/java/org/apache/druid/indexing/overlord/IndexerMetadataStorageCoordinator.java
+++
b/server/src/main/java/org/apache/druid/indexing/overlord/IndexerMetadataStorageCoordinator.java
@@ -28,6 +28,7 @@ import org.apache.druid.segment.SegmentSchemaMapping;
import org.apache.druid.segment.realtime.appenderator.SegmentIdWithShardSpec;
import org.apache.druid.server.http.DataSegmentPlus;
import org.apache.druid.timeline.DataSegment;
+import org.apache.druid.timeline.DatasourceInterval;
import org.apache.druid.timeline.SegmentId;
import org.apache.druid.timeline.SegmentTimeline;
import org.joda.time.DateTime;
@@ -618,6 +619,16 @@ public interface IndexerMetadataStorageCoordinator
*/
List<Interval> retrieveSomeUnusedSegmentIntervals(String dataSource, int
limit);
+ /**
+ * Extracts eligible unused segments intervals from the metadata store using
+ * {@link
org.apache.druid.metadata.SqlSegmentsMetadataQuery#retrieveSomeUnusedSegmentIntervals(DateTime,
int, int)}
+ */
+ Map<DatasourceInterval, Integer> retrieveSomeUnusedSegmentIntervals(
+ DateTime maxUpdatedTime,
+ int maxResultSize,
+ int maxSegmentsToScan
+ );
+
/**
* Returns the number of segment entries in the database whose state was
changed as the result of this call (that is,
* the segments were marked as used). If the call results in a database
error, an exception is relayed to the caller.
diff --git
a/server/src/main/java/org/apache/druid/metadata/IndexerSQLMetadataStorageCoordinator.java
b/server/src/main/java/org/apache/druid/metadata/IndexerSQLMetadataStorageCoordinator.java
index fe04a9a0c1d..49e18862c05 100644
---
a/server/src/main/java/org/apache/druid/metadata/IndexerSQLMetadataStorageCoordinator.java
+++
b/server/src/main/java/org/apache/druid/metadata/IndexerSQLMetadataStorageCoordinator.java
@@ -61,6 +61,7 @@ import org.apache.druid.segment.metadata.SegmentSchemaManager;
import org.apache.druid.segment.realtime.appenderator.SegmentIdWithShardSpec;
import org.apache.druid.server.http.DataSegmentPlus;
import org.apache.druid.timeline.DataSegment;
+import org.apache.druid.timeline.DatasourceInterval;
import org.apache.druid.timeline.Partitions;
import org.apache.druid.timeline.SegmentId;
import org.apache.druid.timeline.SegmentTimeline;
@@ -171,6 +172,18 @@ public class IndexerSQLMetadataStorageCoordinator
implements IndexerMetadataStor
);
}
+ @Override
+ public Map<DatasourceInterval, Integer> retrieveSomeUnusedSegmentIntervals(
+ DateTime maxUpdatedTime,
+ int maxResultSize,
+ int maxSegmentsToScan
+ )
+ {
+ return inReadOnlyTransaction(
+ sql -> sql.retrieveSomeUnusedSegmentIntervals(maxUpdatedTime,
maxResultSize, maxSegmentsToScan)
+ );
+ }
+
@Override
public Set<DataSegment> retrieveUsedSegmentsForIntervals(
final String dataSource,
diff --git
a/server/src/main/java/org/apache/druid/metadata/SegmentsMetadataManagerConfig.java
b/server/src/main/java/org/apache/druid/metadata/SegmentsMetadataManagerConfig.java
index 8fd23da5e15..d02bd6bdec2 100644
---
a/server/src/main/java/org/apache/druid/metadata/SegmentsMetadataManagerConfig.java
+++
b/server/src/main/java/org/apache/druid/metadata/SegmentsMetadataManagerConfig.java
@@ -52,7 +52,7 @@ public class SegmentsMetadataManagerConfig
{
this.pollDuration = Configs.valueOrDefault(pollDuration,
Period.minutes(1));
this.useIncrementalCache = Configs.valueOrDefault(useIncrementalCache,
SegmentMetadataCache.UsageMode.IF_SYNCED);
- this.killUnused = Configs.valueOrDefault(killUnused, new
UnusedSegmentKillerConfig(null, null, null));
+ this.killUnused = Configs.valueOrDefault(killUnused, new
UnusedSegmentKillerConfig(null, null, null, null));
if (this.killUnused.isEnabled() && this.useIncrementalCache ==
SegmentMetadataCache.UsageMode.NEVER) {
throw DruidException
.forPersona(DruidException.Persona.OPERATOR)
diff --git
a/server/src/main/java/org/apache/druid/metadata/SqlSegmentsMetadataQuery.java
b/server/src/main/java/org/apache/druid/metadata/SqlSegmentsMetadataQuery.java
index dfeb376204d..d16ba18d0b2 100644
---
a/server/src/main/java/org/apache/druid/metadata/SqlSegmentsMetadataQuery.java
+++
b/server/src/main/java/org/apache/druid/metadata/SqlSegmentsMetadataQuery.java
@@ -36,6 +36,7 @@ import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.Intervals;
import org.apache.druid.java.util.common.JodaUtils;
+import org.apache.druid.java.util.common.Pair;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.jackson.JacksonUtils;
import org.apache.druid.java.util.common.logger.Logger;
@@ -48,6 +49,7 @@ import
org.apache.druid.segment.realtime.appenderator.SegmentIdWithShardSpec;
import org.apache.druid.server.http.DataSegmentPlus;
import org.apache.druid.timeline.CompactionState;
import org.apache.druid.timeline.DataSegment;
+import org.apache.druid.timeline.DatasourceInterval;
import org.apache.druid.timeline.SegmentId;
import org.apache.druid.timeline.SegmentTimeline;
import org.apache.druid.utils.CloseableUtils;
@@ -1079,7 +1081,7 @@ public class SqlSegmentsMetadataQuery
*
* @return List of distinct unused segment intervals for the specified
datasource
* containing at least 1 entry if there is any unused segment for the
datasource,
- * upto a maximum of {@code limit} entries.
+ * up to a maximum of {@code limit} entries.
*/
public List<Interval> retrieveSomeUnusedSegmentIntervals(String dataSource,
int limit)
{
@@ -1116,6 +1118,70 @@ public class SqlSegmentsMetadataQuery
return
intervals.stream().filter(Objects::nonNull).collect(Collectors.toList());
}
+ /**
+ * Scans up to {@code maxSegmentsToScan} unused segments which are eligible
for
+ * kill and returns the unique datasource-interval for the segments scanned.
+ * <p>
+ * This method ensures that if there is any unused segment in any datasource
+ * which was updated earlier than {@code maxUpdatedTime}, then the returned
+ * map is not empty. However, it does NOT guarantee that:
+ * <ul>
+ * <li>the candidates in the returned map would be ordered by datasource or
interval</li>
+ * <li>the result would contain {@code maxResultSize} entries when there are
more distinct
+ * intervals with eligible unused segments in the metadata store.</li>
+ * </ul>
+ *
+ * @param maxUpdatedTime Unused segments are considered eligible for kill
+ * if they were last updated before this time.
+ * @param maxResultSize Maximum number of candidate intervals to return
+ * across all datasources.
+ * @param maxSegmentsToScan Maximum number of eligible unused segments to
scan
+ * in the metadata store.
+ * @return Map from {@link DatasourceInterval} to the number of unused
segments
+ * eligible for kill.
+ */
+ public Map<DatasourceInterval, Integer> retrieveSomeUnusedSegmentIntervals(
+ DateTime maxUpdatedTime,
+ int maxResultSize,
+ int maxSegmentsToScan
+ )
+ {
+ final String sql = StringUtils.format(
+ // Disable checkstyle to avoid argumentLineBreaking rule from getting
triggered
+ //CHECKSTYLE.OFF: Regexp
+ """
+ SELECT dataSource, start, %2$send%2$s, COUNT(*) AS totalCount
+ FROM (
+ SELECT dataSource, start, %2$send%2$s
+ FROM %1$s
+ WHERE used = false
+ AND (used_status_last_updated IS NULL OR
used_status_last_updated <= :maxUpdatedTime)
+ %3$s
+ ) AS unused
+ GROUP BY dataSource, %2$send%2$s, start
+ %4$s
+ """,
+ //CHECKSTYLE.ON: Regexp
+ dbTables.getSegmentsTable(),
+ connector.getQuoteString(),
+ connector.limitClause(maxSegmentsToScan),
+ connector.limitClause(maxResultSize)
+ );
+
+ final List<Pair<DatasourceInterval, Integer>> unusedSegmentIntervals =
connector.inReadOnlyTransaction(
+ (handle, status) ->
+ handle.createQuery(sql)
+ .setFetchSize(connector.getStreamingFetchSize())
+ .bind("maxUpdatedTime", maxUpdatedTime.toString())
+ .map((index, r, ctx) -> mapToUnusedSegmentInterval(r))
+ .list()
+ );
+
+ return unusedSegmentIntervals.stream().filter(Objects::nonNull).collect(
+ Collectors.toMap(pair -> pair.lhs, pair -> pair.rhs)
+ );
+ }
+
/**
* Retrieves unused segments that exactly match the given interval.
*
@@ -1691,6 +1757,9 @@ public class SqlSegmentsMetadataQuery
return sql;
}
+ /**
+ * Reads the fields {@code start} and {@code end} from the given result set.
+ */
@Nullable
private Interval mapToInterval(ResultSet resultSet, String dataSource)
{
@@ -1706,6 +1775,30 @@ public class SqlSegmentsMetadataQuery
}
}
+ /**
+ * Reads the fields {@code dataSource}, {@code start}, {@code end} and
+ * {@code totalCount} from the given result set.
+ */
+ @Nullable
+ private Pair<DatasourceInterval, Integer>
mapToUnusedSegmentInterval(ResultSet resultSet)
+ {
+ try {
+ final String dataSource = resultSet.getString("dataSource");
+ final int totalCount = resultSet.getInt("totalCount");
+
+ final Interval interval = mapToInterval(resultSet, "");
+ if (interval == null) {
+ return null;
+ } else {
+ return Pair.of(new DatasourceInterval(dataSource, interval),
totalCount);
+ }
+ }
+ catch (Throwable t) {
+ log.error(t, "Could not read unused segment interval record");
+ return null;
+ }
+ }
+
/**
* Tries to parse the fields of the result set into a {@link
SegmentSchemaRecord}.
*
diff --git
a/server/src/main/java/org/apache/druid/metadata/UnusedSegmentKillerConfig.java
b/server/src/main/java/org/apache/druid/metadata/UnusedSegmentKillerConfig.java
index b355795e0b8..b2deed50438 100644
---
a/server/src/main/java/org/apache/druid/metadata/UnusedSegmentKillerConfig.java
+++
b/server/src/main/java/org/apache/druid/metadata/UnusedSegmentKillerConfig.java
@@ -22,6 +22,7 @@ package org.apache.druid.metadata;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.druid.common.config.Configs;
+import org.apache.druid.error.InvalidInput;
import org.apache.druid.java.util.common.logger.Logger;
import org.joda.time.Period;
@@ -35,6 +36,14 @@ public class UnusedSegmentKillerConfig
{
private static final Logger log = new
Logger(UnusedSegmentKillerConfig.class);
+ /**
+ * Maximum number of segments that should be killed in a single run of the
duty.
+ * A value of 200k typically causes the search query to finish within ~5s and
+ * allows the kill queue to finish processing in about (200 * 30s) = 100
minutes,
+ * since a batch of 1000 segments takes about 30s to be processed.
+ */
+ public static final int DEFAULT_MAX_SEGMENTS_TO_KILL = 200_000;
+
@JsonProperty("enabled")
private final boolean enabled;
@@ -44,15 +53,33 @@ public class UnusedSegmentKillerConfig
@JsonProperty("dutyPeriod")
private final Period dutyPeriod;
+ @JsonProperty("maxSegmentsToKill")
+ private final Integer maxSegmentsToKill;
+
@JsonCreator
public UnusedSegmentKillerConfig(
@JsonProperty("enabled") @Nullable Boolean enabled,
@JsonProperty("bufferPeriod") @Nullable Period bufferPeriod,
- @JsonProperty("dutyPeriod") @Nullable Period dutyPeriod
+ @JsonProperty("dutyPeriod") @Nullable Period dutyPeriod,
+ @JsonProperty("maxSegmentsToKill") @Nullable Integer maxSegmentsToKill
)
{
this.enabled = Configs.valueOrDefault(enabled, false);
this.bufferPeriod = Configs.valueOrDefault(bufferPeriod, Period.days(30));
+ this.maxSegmentsToKill = Configs.valueOrDefault(maxSegmentsToKill,
DEFAULT_MAX_SEGMENTS_TO_KILL);
+
+ if (this.maxSegmentsToKill > DEFAULT_MAX_SEGMENTS_TO_KILL) {
+ log.warn(
+ "Setting a high value[%d] for
'druid.manager.segments.killUnused.maxSegmentsToKill'."
+ + " This may slow down the segment killer and/or put undue strain on
the metadata store.",
+ this.maxSegmentsToKill
+ );
+ } else {
+ InvalidInput.conditionalException(
+ this.maxSegmentsToKill > 0,
+ "'druid.manager.segments.killUnused.maxSegmentsToKill' must be
greater than zero"
+ );
+ }
if (dutyPeriod == null) {
this.dutyPeriod = Period.hours(1);
@@ -68,6 +95,7 @@ public class UnusedSegmentKillerConfig
/**
* Period for which segments are retained even after being marked as unused.
+ * Default value is 30 days.
*/
public Period getBufferPeriod()
{
@@ -84,6 +112,15 @@ public class UnusedSegmentKillerConfig
return dutyPeriod;
}
+ /**
+ * Maximum number of segments to kill in a single run of the duty. Default
+ * value is {@link #DEFAULT_MAX_SEGMENTS_TO_KILL}.
+ */
+ public int getMaxSegmentsToKill()
+ {
+ return maxSegmentsToKill;
+ }
+
public boolean isEnabled()
{
return enabled;
diff --git
a/server/src/test/java/org/apache/druid/metadata/IndexerSQLMetadataStorageCoordinatorTest.java
b/server/src/test/java/org/apache/druid/metadata/IndexerSQLMetadataStorageCoordinatorTest.java
index 33b65e55f80..45f02c3713a 100644
---
a/server/src/test/java/org/apache/druid/metadata/IndexerSQLMetadataStorageCoordinatorTest.java
+++
b/server/src/test/java/org/apache/druid/metadata/IndexerSQLMetadataStorageCoordinatorTest.java
@@ -63,6 +63,7 @@ import
org.apache.druid.server.coordinator.simulate.WrappingScheduledExecutorSer
import org.apache.druid.server.http.DataSegmentPlus;
import org.apache.druid.timeline.CompactionState;
import org.apache.druid.timeline.DataSegment;
+import org.apache.druid.timeline.DatasourceInterval;
import org.apache.druid.timeline.SegmentId;
import org.apache.druid.timeline.SegmentTimeline;
import org.apache.druid.timeline.partition.DimensionRangeShardSpec;
@@ -2252,7 +2253,7 @@ public class IndexerSQLMetadataStorageCoordinatorTest
extends IndexerSqlMetadata
}
@Test
- public void testRetrieveSomeUnusedSegmentIntervals()
+ public void testRetrieveSomeUnusedSegmentIntervalsForDatasource()
{
final String dataSource = defaultSegment.getDataSource();
coordinator.commitSegments(Set.of(defaultSegment, defaultSegment3), null);
@@ -2278,6 +2279,38 @@ public class IndexerSQLMetadataStorageCoordinatorTest
extends IndexerSqlMetadata
);
}
+ @Test
+ public void testRetrieveSomeUnusedSegmentIntervals()
+ {
+ final String dataSource = defaultSegment.getDataSource();
+ coordinator.commitSegments(Set.of(defaultSegment, defaultSegment3), null);
+
+ final DateTime maxUpdatedTime = DateTimes.nowUtc();
+
+
Assert.assertTrue(coordinator.retrieveSomeUnusedSegmentIntervals(maxUpdatedTime,
100, 100).isEmpty());
+
+ markAllSegmentsUnused(Set.of(defaultSegment),
DateTimes.nowUtc().minusHours(1));
+ Assert.assertEquals(
+ Map.of(new DatasourceInterval(dataSource,
defaultSegment.getInterval()), 1),
+ coordinator.retrieveSomeUnusedSegmentIntervals(maxUpdatedTime, 100,
100)
+ );
+
+ markAllSegmentsUnused(Set.of(defaultSegment3),
DateTimes.nowUtc().minusHours(1));
+ Assert.assertEquals(
+ Map.of(
+ new DatasourceInterval(dataSource, defaultSegment.getInterval()),
1,
+ new DatasourceInterval(dataSource, defaultSegment3.getInterval()),
1
+ ),
+ coordinator.retrieveSomeUnusedSegmentIntervals(maxUpdatedTime, 100,
100)
+ );
+
+ // Verify retrieve with limit 1 returns only 1 interval
+ Assert.assertEquals(
+ 1,
+ coordinator.retrieveSomeUnusedSegmentIntervals(maxUpdatedTime, 1,
1).size()
+ );
+ }
+
@Test
public void testRetrieveAllDatasourceNames()
{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]