This is an automated email from the ASF dual-hosted git repository.
jojochuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 0c4c66e9f4b HDDS-15412. Add per-volume push replication thread pools
on DataNode.
0c4c66e9f4b is described below
commit 0c4c66e9f4b4b7bf84d6135235b2d0ae0d74dd92
Author: Wei-Chiu Chuang <[email protected]>
AuthorDate: Fri Jul 31 19:12:30 2026 -0700
HDDS-15412. Add per-volume push replication thread pools on DataNode.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
---
.../apache/hadoop/ozone/HddsDatanodeService.java | 28 +-
.../common/statemachine/DatanodeStateMachine.java | 7 +
.../container/replication/ReplicationServer.java | 73 +++-
.../replication/ReplicationSupervisor.java | 183 +++++++-
.../replication/ReplicationSupervisorMetrics.java | 5 +-
.../replication/VolumeReplicationThreadPools.java | 166 ++++++++
.../replication/TestReplicationConfig.java | 30 ++
.../replication/TestReplicationSupervisor.java | 467 +++++++++++++++++++++
hadoop-hdds/docs/content/feature/Decommission.md | 9 +-
.../docs/content/feature/Reconfigurability.md | 3 +-
.../reconfig/TestDatanodeReconfiguration.java | 2 +
11 files changed, 947 insertions(+), 26 deletions(-)
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/HddsDatanodeService.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/HddsDatanodeService.java
index c2a30d97d52..356e5887745 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/HddsDatanodeService.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/HddsDatanodeService.java
@@ -30,6 +30,7 @@
import static org.apache.hadoop.ozone.common.Storage.StorageState.INITIALIZED;
import static
org.apache.hadoop.ozone.conf.OzoneServiceConfig.DEFAULT_SHUTDOWN_HOOK_PRIORITY;
import static
org.apache.hadoop.ozone.container.common.statemachine.DatanodeConfiguration.HDDS_DATANODE_BLOCK_DELETE_THREAD_MAX;
+import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.PER_VOLUME_STREAMS_LIMIT_KEY;
import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.REPLICATION_STREAMS_LIMIT_KEY;
import static org.apache.hadoop.security.UserGroupInformation.getCurrentUser;
import static org.apache.hadoop.util.ExitUtil.terminate;
@@ -96,6 +97,7 @@
import org.apache.hadoop.ozone.container.common.volume.MutableVolumeSet;
import org.apache.hadoop.ozone.container.common.volume.StorageVolume;
import
org.apache.hadoop.ozone.container.diskbalancer.DiskBalancerProtocolServer;
+import
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig;
import org.apache.hadoop.ozone.ha.ConfUtils;
import org.apache.hadoop.ozone.util.OzoneNetUtils;
import org.apache.hadoop.ozone.util.ShutdownHookManager;
@@ -318,7 +320,9 @@ public String getNamespace() {
.register(OZONE_BLOCK_DELETING_SERVICE_TIMEOUT,
this::reconfigBlockDeletingServiceTimeout)
.register(REPLICATION_STREAMS_LIMIT_KEY,
- this::reconfigReplicationStreamsLimit);
+ this::reconfigReplicationStreamsLimit)
+ .register(PER_VOLUME_STREAMS_LIMIT_KEY,
+ this::reconfigPerVolumeStreamsLimit);
scmServiceId = HddsUtils.getScmServiceId(conf);
@@ -719,6 +723,28 @@ private String reconfigReplicationStreamsLimit(String
value) {
return value;
}
+ private String reconfigPerVolumeStreamsLimit(String value) {
+ int newSize = Integer.parseInt(value);
+ Preconditions.checkArgument(newSize >= 1,
+ PER_VOLUME_STREAMS_LIMIT_KEY + " must be at least 1 but was %s",
+ value);
+ ReplicationConfig replicationConfig =
+ getDatanodeStateMachine().getSupervisor().getReplicationConfig();
+ if (!replicationConfig.isPerVolumeEnabled()) {
+ LOG.warn("Ignoring reconfiguration of {} to {} because per-volume "
+ + "replication is disabled", PER_VOLUME_STREAMS_LIMIT_KEY, value);
+ return value;
+ }
+ try {
+ getDatanodeStateMachine().getSupervisor().setPerVolumePoolSize(newSize);
+ } catch (RuntimeException e) {
+ LOG.warn("Failed to apply per-volume replication thread pool resize to "
+ + "{}: {}", value, e.getMessage(), e);
+ throw e;
+ }
+ return value;
+ }
+
private String reconfigBlockDeletingServiceInterval(String value) {
return value;
}
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/DatanodeStateMachine.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/DatanodeStateMachine.java
index 00a9bdd1043..4f6078d0bd2 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/DatanodeStateMachine.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/statemachine/DatanodeStateMachine.java
@@ -204,9 +204,16 @@ public DatanodeStateMachine(HddsDatanodeService
hddsDatanodeService,
.stateContext(context)
.datanodeConfig(dnConf)
.replicationConfig(replicationConfig)
+ .containerSet(container.getContainerSet())
+ .volumeSet(container.getVolumeSet())
.clock(clock)
.build();
+ container.getVolumeSet().setFailedVolumeListener(() -> {
+ container.handleVolumeFailures();
+ supervisor.shutdownFailedVolumePools(container.getVolumeSet());
+ });
+
replicationSupervisorMetrics =
ReplicationSupervisorMetrics.create(supervisor);
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationServer.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationServer.java
index 8375b1100bd..ceb35201b3c 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationServer.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationServer.java
@@ -183,16 +183,30 @@ public static final class ReplicationConfig {
static final String REPLICATION_OUTOFSERVICE_FACTOR_KEY =
PREFIX + "." + OUTOFSERVICE_FACTOR_KEY;
+ public static final String PER_VOLUME_ENABLED_KEY =
+ PREFIX + ".per.volume.enabled";
+ public static final String PER_VOLUME_STREAMS_LIMIT_KEY =
+ PREFIX + ".per.volume.streams.limit";
+ public static final int PER_VOLUME_STREAMS_LIMIT_DEFAULT = 2;
+
/**
- * The maximum number of replication commands a single datanode can execute
- * simultaneously.
+ * Base size of the global replication handler executor and inbound
+ * replication server executor.
*/
@Config(key = "hdds.datanode.replication.streams.limit",
type = ConfigType.INT,
defaultValue = "10",
tags = {DATANODE},
- description = "The maximum number of replication commands a single " +
- "datanode can execute simultaneously"
+ description = "Sets both the base size of the global replication "
+ + "handler executor and the inbound replication server executor. "
+ + "The global executor is subject to outofservice.limit.factor "
+ + "scaling. When "
+ + "hdds.datanode.replication.per.volume.enabled is false
(default), "
+ + "all source-side replication tasks use the global executor. "
+ + "When per.volume.enabled is true, per-volume executors handle "
+ + "normal source-side push tasks, while this limit still applies "
+ + "to non-push and fallback source tasks and target-side inbound "
+ + "push requests."
)
private int replicationMaxStreams = REPLICATION_MAX_STREAMS_DEFAULT;
@@ -224,6 +238,34 @@ public static final class ReplicationConfig {
)
private double outOfServiceFactor = OUTOFSERVICE_FACTOR_DEFAULT;
+ @Config(key = PER_VOLUME_ENABLED_KEY,
+ type = ConfigType.BOOLEAN,
+ defaultValue = "false",
+ tags = {DATANODE},
+ description = "When true, push-based container replication uses a " +
+ "separate replication handler thread pool per data volume so " +
+ "that slow replication on one disk does not block replication " +
+ "on other disks. Pull replication and other replication tasks " +
+ "continue to use the global replication handler thread pool."
+ )
+ private boolean perVolumeEnabled = false;
+
+ @Config(key = PER_VOLUME_STREAMS_LIMIT_KEY,
+ type = ConfigType.INT,
+ defaultValue = "2",
+ reconfigurable = true,
+ tags = {DATANODE},
+ description = "When hdds.datanode.replication.per.volume.enabled is "
+ + "true, maximum concurrent push replication commands per data "
+ + "volume (each volume has its own handler thread pool; effective "
+ + "push parallelism on the datanode is roughly the number of "
+ + "volumes times this limit, with outofservice.limit.factor "
+ + "applied per pool on decommissioning or maintenance nodes). "
+ + "Push replication is usually disk-bound, so one or two "
+ + "concurrent transfers per volume often saturates the disk."
+ )
+ private int perVolumeStreamsLimit = PER_VOLUME_STREAMS_LIMIT_DEFAULT;
+
public double getOutOfServiceFactor() {
return outOfServiceFactor;
}
@@ -257,6 +299,22 @@ public void setReplicationQueueLimit(int limit) {
this.replicationQueueLimit = limit;
}
+ public boolean isPerVolumeEnabled() {
+ return perVolumeEnabled;
+ }
+
+ public void setPerVolumeEnabled(boolean enabled) {
+ this.perVolumeEnabled = enabled;
+ }
+
+ public int getPerVolumeStreamsLimit() {
+ return perVolumeStreamsLimit;
+ }
+
+ public void setPerVolumeStreamsLimit(int limit) {
+ this.perVolumeStreamsLimit = limit;
+ }
+
@PostConstruct
public void validate() {
if (replicationMaxStreams < 1) {
@@ -279,6 +337,13 @@ public void validate() {
clamped);
outOfServiceFactor = clamped;
}
+
+ if (perVolumeStreamsLimit < 1) {
+ LOG.warn(PER_VOLUME_STREAMS_LIMIT_KEY + " must be greater than zero " +
+ "and was set to {}. Defaulting to {}",
+ perVolumeStreamsLimit, PER_VOLUME_STREAMS_LIMIT_DEFAULT);
+ perVolumeStreamsLimit = PER_VOLUME_STREAMS_LIMIT_DEFAULT;
+ }
}
}
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationSupervisor.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationSupervisor.java
index 8b6daf6aae5..d805e2249b8 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationSupervisor.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationSupervisor.java
@@ -28,6 +28,7 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalLong;
@@ -35,6 +36,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -49,8 +51,13 @@
import
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ReplicationCommandPriority;
import org.apache.hadoop.metrics2.lib.MetricsRegistry;
import org.apache.hadoop.metrics2.lib.MutableRate;
+import org.apache.hadoop.ozone.container.common.impl.ContainerSet;
+import org.apache.hadoop.ozone.container.common.interfaces.Container;
import
org.apache.hadoop.ozone.container.common.statemachine.DatanodeConfiguration;
import org.apache.hadoop.ozone.container.common.statemachine.StateContext;
+import org.apache.hadoop.ozone.container.common.volume.HddsVolume;
+import org.apache.hadoop.ozone.container.common.volume.MutableVolumeSet;
+import org.apache.hadoop.ozone.container.common.volume.StorageVolume;
import
org.apache.hadoop.ozone.container.replication.AbstractReplicationTask.Status;
import
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig;
import org.apache.hadoop.util.Time;
@@ -106,6 +113,8 @@ public final class ReplicationSupervisor {
private final IntConsumer executorThreadUpdater;
private final ReplicationConfig replicationConfig;
private final DatanodeConfiguration datanodeConfig;
+ private final ContainerSet containerSet;
+ private final VolumeReplicationThreadPools volumePools;
/**
* Builder for {@link ReplicationSupervisor}.
@@ -114,10 +123,13 @@ public static class Builder {
private StateContext context;
private ReplicationConfig replicationConfig;
private DatanodeConfiguration datanodeConfig;
+ private ContainerSet containerSet;
+ private MutableVolumeSet volumeSet;
private ExecutorService executor;
private Clock clock;
private IntConsumer executorThreadUpdater = threadCount -> {
};
+ private VolumeReplicationThreadPools volumePools;
public Builder clock(Clock newClock) {
clock = newClock;
@@ -149,6 +161,16 @@ public Builder executorThreadUpdater(IntConsumer
newUpdater) {
return this;
}
+ public Builder containerSet(ContainerSet newContainerSet) {
+ containerSet = newContainerSet;
+ return this;
+ }
+
+ public Builder volumeSet(MutableVolumeSet newVolumeSet) {
+ volumeSet = newVolumeSet;
+ return this;
+ }
+
public ReplicationSupervisor build() {
if (replicationConfig == null || datanodeConfig == null) {
ConfigurationSource conf = new OzoneConfiguration();
@@ -191,8 +213,20 @@ public ReplicationSupervisor build() {
};
}
+ if (replicationConfig.isPerVolumeEnabled() && volumeSet != null) {
+ LOG.info("Per-volume container replication thread pools enabled with "
+ + "{} threads per volume",
+ replicationConfig.getPerVolumeStreamsLimit());
+ volumePools = new VolumeReplicationThreadPools();
+ String threadNamePrefix =
+ context != null ? context.getThreadNamePrefix() : "";
+ volumePools.init(volumeSet.getVolumesList(),
+ replicationConfig.getPerVolumeStreamsLimit(), threadNamePrefix);
+ }
+
return new ReplicationSupervisor(context, executor, replicationConfig,
- datanodeConfig, clock, executorThreadUpdater);
+ datanodeConfig, clock, executorThreadUpdater, containerSet,
+ volumePools);
}
}
@@ -204,14 +238,18 @@ public static Map<String, String> getMetricsMap() {
return Collections.unmodifiableMap(METRICS_MAP);
}
+ @SuppressWarnings("checkstyle:ParameterNumber")
private ReplicationSupervisor(StateContext context, ExecutorService executor,
ReplicationConfig replicationConfig, DatanodeConfiguration
datanodeConfig,
- Clock clock, IntConsumer executorThreadUpdater) {
+ Clock clock, IntConsumer executorThreadUpdater, ContainerSet
containerSet,
+ VolumeReplicationThreadPools volumePools) {
this.inFlight = ConcurrentHashMap.newKeySet();
this.context = context;
this.executor = executor;
this.replicationConfig = replicationConfig;
this.datanodeConfig = datanodeConfig;
+ this.containerSet = containerSet;
+ this.volumePools = volumePools;
maxQueueSize = datanodeConfig.getCommandQueueLimit();
this.clock = clock;
this.executorThreadUpdater = executorThreadUpdater;
@@ -266,17 +304,69 @@ public void initCounters(AbstractReplicationTask task) {
}
private void addToQueue(AbstractReplicationTask task) {
- if (inFlight.add(task)) {
- if (task.getPriority() != ReplicationCommandPriority.LOW) {
- // Low priority tasks are not included in the replication queue sizes
- // returned to SCM in the heartbeat, so we only update the count for
- // priorities other than low.
- taskCounter.computeIfAbsent(task.getClass(),
- k -> new AtomicInteger()).incrementAndGet();
- }
- queuedCounter.get(task.getMetricName()).incrementAndGet();
- executor.execute(new TaskRunner(task));
+ if (!inFlight.add(task)) {
+ return;
+ }
+ if (task.getPriority() != ReplicationCommandPriority.LOW) {
+ taskCounter.computeIfAbsent(task.getClass(),
+ k -> new AtomicInteger()).incrementAndGet();
+ }
+ queuedCounter.get(task.getMetricName()).incrementAndGet();
+ try {
+ selectExecutor(task).execute(new TaskRunner(task));
+ } catch (RejectedExecutionException e) {
+ LOG.warn("Rejected {} in ReplicationSupervisor: {}", task,
e.getMessage());
+ rollbackQueuedTask(task);
+ }
+ }
+
+ private void rollbackQueuedTask(AbstractReplicationTask task) {
+ queuedCounter.get(task.getMetricName()).decrementAndGet();
+ inFlight.remove(task);
+ decrementTaskCounter(task);
+ }
+
+ private ExecutorService selectExecutor(AbstractReplicationTask task) {
+ if (!replicationConfig.isPerVolumeEnabled() || volumePools == null) {
+ return executor;
+ }
+ if (!(task instanceof ReplicationTask)) {
+ return executor;
+ }
+ ReplicationTask replicationTask = (ReplicationTask) task;
+ return resolveVolumeExecutor(replicationTask.getContainerId());
+ }
+
+ private ExecutorService resolveVolumeExecutor(long containerId) {
+ if (containerSet == null) {
+ return executor;
+ }
+ Container<?> container = containerSet.getContainer(containerId);
+ if (container == null) {
+ LOG.warn("Container {} not found for push replication; falling back to "
+ + "ReplicationSupervisor global replication handler thread pool",
+ containerId);
+ return executor;
+ }
+ HddsVolume volume = container.getContainerData().getVolume();
+ String volumeRoot = volume == null ? "unknown"
+ : volume.getStorageDir().getPath();
+ if (volume == null || volume.isFailed()) {
+ LOG.warn("No per-volume replication handler thread pool available for "
+ + "container {} on volume {}; falling back to global replication
"
+ + "handler thread pool",
+ containerId, volumeRoot);
+ return executor;
+ }
+ ExecutorService volumeExecutor = volumePools.getExecutor(volumeRoot);
+ if (volumeExecutor == null) {
+ LOG.warn("No per-volume replication handler thread pool available for "
+ + "container {} on volume {}; falling back to global replication
"
+ + "handler thread pool",
+ containerId, volumeRoot);
+ return executor;
}
+ return volumeExecutor;
}
private void decrementTaskCounter(AbstractReplicationTask task) {
@@ -304,9 +394,49 @@ public void stop() {
executor.shutdownNow();
}
} catch (InterruptedException ie) {
- // Ignore, we don't really care about the failure.
Thread.currentThread().interrupt();
}
+ if (volumePools != null) {
+ cancelDrainedTaskRunners(volumePools.shutdownAll());
+ }
+ }
+
+ public ReplicationConfig getReplicationConfig() {
+ return replicationConfig;
+ }
+
+ public void setPerVolumePoolSize(int newSize) {
+ if (volumePools != null) {
+ replicationConfig.setPerVolumeStreamsLimit(newSize);
+ resize(state.get());
+ }
+ }
+
+ public void shutdownFailedVolumePools(MutableVolumeSet volumeSet) {
+ if (volumePools == null || volumeSet == null) {
+ return;
+ }
+ for (StorageVolume volume : volumeSet.getFailedVolumesList()) {
+ cancelDrainedTaskRunners(
+ volumePools.shutdownVolume(volume.getStorageDir().getPath()));
+ }
+ }
+
+ private void cancelDrainedTaskRunners(List<Runnable> drained) {
+ for (Runnable runnable : drained) {
+ if (!(runnable instanceof TaskRunner)) {
+ continue;
+ }
+ AbstractReplicationTask task = ((TaskRunner) runnable).getTask();
+ queuedCounter.get(task.getMetricName()).decrementAndGet();
+ inFlight.remove(task);
+ decrementTaskCounter(task);
+ }
+ }
+
+ @VisibleForTesting
+ VolumeReplicationThreadPools getVolumeReplicationThreadPools() {
+ return volumePools;
}
/**
@@ -371,6 +501,20 @@ private void resize(HddsProtos.NodeOperationalState
nodeState) {
maxQueueSize = newMaxQueueSize;
executorThreadUpdater.accept(threadCount);
+
+ if (volumePools != null) {
+ int perVolumeThreadCount = replicationConfig.getPerVolumeStreamsLimit();
+ if (isMaintenance(nodeState) || isDecommission(nodeState)) {
+ perVolumeThreadCount =
+ replicationConfig.scaleOutOfServiceLimit(perVolumeThreadCount);
+ }
+ LOG.info("Scaling per-volume replication thread pools to {} "
+ + "(base={}, factor={})",
+ perVolumeThreadCount,
+ replicationConfig.getPerVolumeStreamsLimit(),
+ replicationConfig.getOutOfServiceFactor());
+ volumePools.setPoolSize(perVolumeThreadCount);
+ }
}
/**
@@ -383,6 +527,10 @@ public TaskRunner(AbstractReplicationTask task) {
this.task = task;
}
+ AbstractReplicationTask getTask() {
+ return task;
+ }
+
@Override
public void run() {
final long startTime = Time.monotonicNow();
@@ -478,11 +626,14 @@ public long getReplicationRequestCount(String
metricsName) {
}
public long getQueueSize() {
+ long queueSize = 0;
if (executor instanceof ThreadPoolExecutor) {
- return ((ThreadPoolExecutor)executor).getQueue().size();
- } else {
- return 0;
+ queueSize += ((ThreadPoolExecutor) executor).getQueue().size();
+ }
+ if (volumePools != null) {
+ queueSize += volumePools.getTotalQueueSize();
}
+ return queueSize;
}
public long getMaxReplicationStreams() {
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationSupervisorMetrics.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationSupervisorMetrics.java
index 64854e1ea2c..151e07a451b 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationSupervisorMetrics.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/ReplicationSupervisorMetrics.java
@@ -83,8 +83,9 @@ public void getMetrics(MetricsCollector collector, boolean
all) {
"Number of replication requests skipped as the container is "
+ "already present"),
supervisor.getReplicationSkippedCount())
- .addGauge(Interns.info("maxReplicationStreams", "Maximum number of "
- + "concurrent replication tasks which can run simultaneously"),
+ .addGauge(Interns.info("maxReplicationStreams", "Maximum pool size of "
+ + "the global replication handler executor (not total capacity "
+ + "when per-volume push replication thread pools are enabled)"),
supervisor.getMaxReplicationStreams());
Map<String, String> metricsMap = ReplicationSupervisor.getMetricsMap();
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/VolumeReplicationThreadPools.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/VolumeReplicationThreadPools.java
new file mode 100644
index 00000000000..f8ddf8fbaf9
--- /dev/null
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/replication/VolumeReplicationThreadPools.java
@@ -0,0 +1,166 @@
+/*
+ * 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.hadoop.ozone.container.replication;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hadoop.hdds.utils.HddsServerUtil;
+import org.apache.hadoop.ozone.container.common.volume.StorageVolume;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Per-volume replication handler thread pools for push-based replication.
+ */
+final class VolumeReplicationThreadPools {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(VolumeReplicationThreadPools.class);
+
+ private final ConcurrentHashMap<String, ThreadPoolExecutor> pools =
+ new ConcurrentHashMap<>();
+ private int currentPoolSize;
+
+ void init(Collection<? extends StorageVolume> volumes, int poolSize,
+ String threadNamePrefix) {
+ currentPoolSize = poolSize;
+ List<String> volumeRoots = new ArrayList<>();
+ for (StorageVolume volume : volumes) {
+ String volumeRoot = volume.getStorageDir().getPath();
+ volumeRoots.add(volumeRoot);
+ pools.put(volumeRoot, createPool(poolSize, threadNamePrefix,
volumeRoot));
+ }
+ LOG.info("Initialized {} per-volume replication thread pools "
+ + "(threads per volume = {}): {}",
+ volumeRoots.size(), poolSize, volumeRoots);
+ }
+
+ private static ThreadPoolExecutor createPool(int poolSize,
+ String threadNamePrefix, String volumeRoot) {
+ AtomicInteger threadId = new AtomicInteger();
+ ThreadFactory threadFactory = runnable -> {
+ Thread thread = new Thread(runnable, threadNamePrefix
+ + "ContainerReplicationThread-" + volumeRoot + "-"
+ + threadId.getAndIncrement());
+ thread.setDaemon(true);
+ return thread;
+ };
+ return new ThreadPoolExecutor(
+ poolSize,
+ poolSize,
+ 60, TimeUnit.SECONDS,
+ new PriorityBlockingQueue<>(),
+ threadFactory);
+ }
+
+ ExecutorService getExecutor(String volumeRoot) {
+ return pools.get(volumeRoot);
+ }
+
+ List<Runnable> shutdownVolume(String volumeRoot) {
+ ThreadPoolExecutor pool = pools.remove(volumeRoot);
+ if (pool == null) {
+ return Collections.emptyList();
+ }
+ LOG.info("Shutting down per-volume replication thread pool for failed "
+ + "volume {}", volumeRoot);
+ List<Runnable> drained = Collections.emptyList();
+ try {
+ drained = pool.shutdownNow();
+ if (!pool.awaitTermination(3, TimeUnit.SECONDS)) {
+ LOG.warn("Per-volume replication thread pool for volume {} did not "
+ + "terminate within timeout", volumeRoot);
+ }
+ } catch (InterruptedException e) {
+ LOG.warn("Interrupted while shutting down per-volume replication thread "
+ + "pool for volume {}", volumeRoot, e);
+ Thread.currentThread().interrupt();
+ } catch (RuntimeException e) {
+ LOG.warn("Failed to shut down per-volume replication thread pool for "
+ + "volume {}: {}", volumeRoot, e.getMessage(), e);
+ }
+ return drained;
+ }
+
+ List<Runnable> shutdownAll() {
+ List<Runnable> drained = new ArrayList<>();
+ for (String volumeRoot : new ArrayList<>(pools.keySet())) {
+ drained.addAll(shutdownVolume(volumeRoot));
+ }
+ return drained;
+ }
+
+ void setPoolSize(int newSize) {
+ LOG.info("Resizing per-volume replication thread pools from {} to {}",
+ currentPoolSize, newSize);
+ int successCount = 0;
+ int totalCount = pools.size();
+ for (Map.Entry<String, ThreadPoolExecutor> entry : pools.entrySet()) {
+ try {
+ HddsServerUtil.setPoolSize(entry.getValue(), newSize, LOG);
+ successCount++;
+ } catch (RuntimeException e) {
+ LOG.warn("Failed to resize per-volume replication thread pool for "
+ + "volume {}: {}", entry.getKey(), e.getMessage(), e);
+ }
+ }
+ currentPoolSize = newSize;
+ if (successCount < totalCount) {
+ LOG.warn("Resized {}/{} per-volume replication thread pools to {}",
+ successCount, totalCount, newSize);
+ } else if (totalCount > 0) {
+ LOG.info("Resized all {} per-volume replication thread pools to {}",
+ totalCount, newSize);
+ }
+ }
+
+ int getCurrentPoolSize() {
+ return currentPoolSize;
+ }
+
+ @VisibleForTesting
+ int getPoolSize(String volumeRoot) {
+ ThreadPoolExecutor pool = pools.get(volumeRoot);
+ return pool == null ? 0 : pool.getMaximumPoolSize();
+ }
+
+ @VisibleForTesting
+ long getTotalQueueSize() {
+ long total = 0;
+ for (ThreadPoolExecutor pool : pools.values()) {
+ total += pool.getQueue().size();
+ }
+ return total;
+ }
+
+ @VisibleForTesting
+ boolean hasPool(String volumeRoot) {
+ return pools.containsKey(volumeRoot);
+ }
+}
diff --git
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/replication/TestReplicationConfig.java
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/replication/TestReplicationConfig.java
index f1f182f40a3..a0b0f544542 100644
---
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/replication/TestReplicationConfig.java
+++
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/replication/TestReplicationConfig.java
@@ -20,10 +20,14 @@
import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.OUTOFSERVICE_FACTOR_DEFAULT;
import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.OUTOFSERVICE_FACTOR_MAX;
import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.OUTOFSERVICE_FACTOR_MIN;
+import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.PER_VOLUME_STREAMS_LIMIT_DEFAULT;
+import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.PER_VOLUME_STREAMS_LIMIT_KEY;
import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.REPLICATION_MAX_STREAMS_DEFAULT;
import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.REPLICATION_OUTOFSERVICE_FACTOR_KEY;
import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.REPLICATION_STREAMS_LIMIT_KEY;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig;
@@ -138,6 +142,32 @@ public void isCreatedWitDefaultValues() {
subject.getReplicationMaxStreams());
assertEquals(OUTOFSERVICE_FACTOR_DEFAULT,
subject.getOutOfServiceFactor(), 0.001);
+ assertFalse(subject.isPerVolumeEnabled());
+ assertEquals(PER_VOLUME_STREAMS_LIMIT_DEFAULT,
+ subject.getPerVolumeStreamsLimit());
+ }
+
+ @Test
+ public void acceptsPerVolumeConfigValues() {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setBoolean(ReplicationConfig.PER_VOLUME_ENABLED_KEY, true);
+ conf.setInt(PER_VOLUME_STREAMS_LIMIT_KEY, 3);
+
+ ReplicationConfig subject = conf.getObject(ReplicationConfig.class);
+
+ assertTrue(subject.isPerVolumeEnabled());
+ assertEquals(3, subject.getPerVolumeStreamsLimit());
+ }
+
+ @Test
+ public void overridesInvalidPerVolumeStreamsLimit() {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setInt(PER_VOLUME_STREAMS_LIMIT_KEY, 0);
+
+ ReplicationConfig subject = conf.getObject(ReplicationConfig.class);
+
+ assertEquals(PER_VOLUME_STREAMS_LIMIT_DEFAULT,
+ subject.getPerVolumeStreamsLimit());
}
}
diff --git
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/replication/TestReplicationSupervisor.java
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/replication/TestReplicationSupervisor.java
index 026ac6ac76a..21efce17dee 100644
---
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/replication/TestReplicationSupervisor.java
+++
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/replication/TestReplicationSupervisor.java
@@ -28,9 +28,15 @@
import static
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ReplicationCommandPriority.NORMAL;
import static
org.apache.hadoop.ozone.container.common.impl.ContainerImplTestUtils.newContainerSet;
import static
org.apache.hadoop.ozone.container.replication.AbstractReplicationTask.Status.DONE;
+import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.PER_VOLUME_ENABLED_KEY;
+import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.PER_VOLUME_STREAMS_LIMIT_KEY;
import static
org.apache.hadoop.ozone.protocol.commands.ReplicateContainerCommand.toTarget;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyLong;
@@ -59,16 +65,22 @@
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BooleanSupplier;
import java.util.function.Function;
+import org.apache.hadoop.hdds.HddsConfigKeys;
import org.apache.hadoop.hdds.client.ECReplicationConfig;
import org.apache.hadoop.hdds.conf.ConfigurationSource;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.fs.MockSpaceUsageCheckFactory;
+import org.apache.hadoop.hdds.fs.SpaceUsageCheckFactory;
import org.apache.hadoop.hdds.protocol.DatanodeDetails;
import org.apache.hadoop.hdds.protocol.MockDatanodeDetails;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ReplicationCommandPriority;
+import org.apache.hadoop.hdds.scm.ScmConfigKeys;
import org.apache.hadoop.hdds.security.symmetric.SecretKeySignerClient;
import
org.apache.hadoop.hdds.security.x509.certificate.client.CertificateClient;
import org.apache.hadoop.metrics2.impl.MetricsCollectorImpl;
@@ -76,9 +88,13 @@
import org.apache.hadoop.ozone.container.checksum.ReconcileContainerTask;
import org.apache.hadoop.ozone.container.common.impl.ContainerLayoutVersion;
import org.apache.hadoop.ozone.container.common.impl.ContainerSet;
+import org.apache.hadoop.ozone.container.common.interfaces.Container;
import
org.apache.hadoop.ozone.container.common.statemachine.DatanodeConfiguration;
import
org.apache.hadoop.ozone.container.common.statemachine.DatanodeStateMachine;
import org.apache.hadoop.ozone.container.common.statemachine.StateContext;
+import org.apache.hadoop.ozone.container.common.volume.HddsVolume;
+import org.apache.hadoop.ozone.container.common.volume.MutableVolumeSet;
+import org.apache.hadoop.ozone.container.common.volume.StorageVolume;
import
org.apache.hadoop.ozone.container.ec.reconstruction.ECReconstructionCommandInfo;
import
org.apache.hadoop.ozone.container.ec.reconstruction.ECReconstructionCoordinator;
import
org.apache.hadoop.ozone.container.ec.reconstruction.ECReconstructionCoordinatorTask;
@@ -91,6 +107,7 @@
import
org.apache.hadoop.ozone.protocol.commands.ReconstructECContainersCommand;
import org.apache.hadoop.ozone.protocol.commands.ReplicateContainerCommand;
import org.apache.ozone.test.GenericTestUtils;
+import org.apache.ozone.test.GenericTestUtils.LogCapturer;
import org.apache.ozone.test.MockClock;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -107,6 +124,8 @@ public class TestReplicationSupervisor {
private File tempDir;
private final ContainerReplicator noopReplicator = task -> { };
+ private final ContainerReplicator doneReplicator =
+ task -> task.setStatus(DONE);
private final ContainerReplicator throwingReplicator = task -> {
throw new RuntimeException("testing replication failure");
};
@@ -1052,4 +1071,452 @@ private void scheduleTasks(
rs.addTask(new ReplicationTask(toTarget(i, target), noopReplicator));
}
}
+
+ @ContainerLayoutTestInfo.ContainerTest
+ public void perVolumeDisabledUsesGlobalPool(ContainerLayoutVersion layout) {
+ this.layoutVersion = layout;
+ ReplicationServer.ReplicationConfig repConf =
+ new ReplicationServer.ReplicationConfig();
+ repConf.setPerVolumeEnabled(false);
+
+ ReplicationSupervisor supervisor = ReplicationSupervisor.newBuilder()
+ .stateContext(context)
+ .replicationConfig(repConf)
+ .executor(newDirectExecutorService())
+ .clock(clock)
+ .build();
+
+ try {
+ assertNull(supervisor.getVolumeReplicationThreadPools());
+ replicatorRef.set(doneReplicator);
+ supervisor.addTask(createTask(1L));
+ assertEquals(1, supervisor.getReplicationSuccessCount());
+ } finally {
+ supervisor.stop();
+ }
+ }
+
+ @ContainerLayoutTestInfo.ContainerTest
+ public void perVolumeInitLogging(ContainerLayoutVersion layout,
+ @TempDir File perVolumeTempDir) throws Exception {
+ this.layoutVersion = layout;
+ OzoneConfiguration conf = perVolumeConf(perVolumeTempDir, 1);
+ MutableVolumeSet volumeSet = newVolumeSet(conf);
+ ReplicationServer.ReplicationConfig repConf =
+ conf.getObject(ReplicationServer.ReplicationConfig.class);
+
+ LogCapturer supervisorLogs =
+ LogCapturer.captureLogs(ReplicationSupervisor.class);
+ LogCapturer poolLogs =
+ LogCapturer.captureLogs(VolumeReplicationThreadPools.class);
+
+ ReplicationSupervisor supervisor = ReplicationSupervisor.newBuilder()
+ .stateContext(context)
+ .replicationConfig(repConf)
+ .containerSet(set)
+ .volumeSet(volumeSet)
+ .executor(newDirectExecutorService())
+ .clock(clock)
+ .build();
+
+ try {
+ assertNotNull(supervisor.getVolumeReplicationThreadPools());
+ assertThat(supervisorLogs.getOutput())
+ .contains("Per-volume container replication thread pools enabled");
+ assertThat(poolLogs.getOutput())
+ .contains("Initialized 2 per-volume replication thread pools");
+ for (StorageVolume volume : volumeSet.getVolumesList()) {
+ assertThat(poolLogs.getOutput())
+ .contains(volume.getStorageDir().getPath());
+ }
+ } finally {
+ supervisorLogs.stopCapturing();
+ poolLogs.stopCapturing();
+ supervisor.stop();
+ }
+ }
+
+ @ContainerLayoutTestInfo.ContainerTest
+ public void perVolumePoolSizeRespected(ContainerLayoutVersion layout,
+ @TempDir File perVolumeTempDir) throws Exception {
+ this.layoutVersion = layout;
+ OzoneConfiguration conf = perVolumeConf(perVolumeTempDir, 3);
+ MutableVolumeSet volumeSet = newVolumeSet(conf);
+ ReplicationServer.ReplicationConfig repConf =
+ conf.getObject(ReplicationServer.ReplicationConfig.class);
+
+ ReplicationSupervisor supervisor = ReplicationSupervisor.newBuilder()
+ .stateContext(context)
+ .replicationConfig(repConf)
+ .containerSet(set)
+ .volumeSet(volumeSet)
+ .executor(newDirectExecutorService())
+ .clock(clock)
+ .build();
+
+ try {
+ VolumeReplicationThreadPools pools =
+ supervisor.getVolumeReplicationThreadPools();
+ assertNotNull(pools);
+ for (StorageVolume volume : volumeSet.getVolumesList()) {
+ assertEquals(3, pools.getPoolSize(volume.getStorageDir().getPath()));
+ }
+ } finally {
+ supervisor.stop();
+ }
+ }
+
+ @ContainerLayoutTestInfo.ContainerTest
+ public void perVolumePoolResize(ContainerLayoutVersion layout,
+ @TempDir File perVolumeTempDir) throws Exception {
+ this.layoutVersion = layout;
+ OzoneConfiguration conf = perVolumeConf(perVolumeTempDir, 1);
+ MutableVolumeSet volumeSet = newVolumeSet(conf);
+ ReplicationServer.ReplicationConfig repConf =
+ conf.getObject(ReplicationServer.ReplicationConfig.class);
+
+ ReplicationSupervisor supervisor = ReplicationSupervisor.newBuilder()
+ .stateContext(context)
+ .replicationConfig(repConf)
+ .containerSet(set)
+ .volumeSet(volumeSet)
+ .executor(newDirectExecutorService())
+ .clock(clock)
+ .build();
+
+ try {
+ supervisor.setPerVolumePoolSize(3);
+ VolumeReplicationThreadPools pools =
+ supervisor.getVolumeReplicationThreadPools();
+ for (StorageVolume volume : volumeSet.getVolumesList()) {
+ assertEquals(3, pools.getPoolSize(volume.getStorageDir().getPath()));
+ }
+ assertEquals(3, repConf.getPerVolumeStreamsLimit());
+ } finally {
+ supervisor.stop();
+ }
+ }
+
+ @ContainerLayoutTestInfo.ContainerTest
+ public void perVolumePoolResizeOnNodeStateChange(ContainerLayoutVersion
layout,
+ @TempDir File perVolumeTempDir) throws Exception {
+ this.layoutVersion = layout;
+ OzoneConfiguration conf = perVolumeConf(perVolumeTempDir, 2);
+ MutableVolumeSet volumeSet = newVolumeSet(conf);
+ ReplicationServer.ReplicationConfig repConf =
+ conf.getObject(ReplicationServer.ReplicationConfig.class);
+
+ ReplicationSupervisor supervisor = ReplicationSupervisor.newBuilder()
+ .stateContext(context)
+ .replicationConfig(repConf)
+ .containerSet(set)
+ .volumeSet(volumeSet)
+ .clock(clock)
+ .build();
+
+ try {
+ datanode.setPersistedOpState(IN_SERVICE);
+ supervisor.nodeStateUpdated(
+ HddsProtos.NodeOperationalState.DECOMMISSIONING);
+ VolumeReplicationThreadPools pools =
+ supervisor.getVolumeReplicationThreadPools();
+ int expected = repConf.scaleOutOfServiceLimit(2);
+ for (StorageVolume volume : volumeSet.getVolumesList()) {
+ assertEquals(expected,
+ pools.getPoolSize(volume.getStorageDir().getPath()));
+ }
+ } finally {
+ supervisor.stop();
+ }
+ }
+
+ @ContainerLayoutTestInfo.ContainerTest
+ public void perVolumePoolResizeDuringDecommission(ContainerLayoutVersion
layout,
+ @TempDir File perVolumeTempDir) throws Exception {
+ this.layoutVersion = layout;
+ OzoneConfiguration conf = perVolumeConf(perVolumeTempDir, 2);
+ MutableVolumeSet volumeSet = newVolumeSet(conf);
+ ReplicationServer.ReplicationConfig repConf =
+ conf.getObject(ReplicationServer.ReplicationConfig.class);
+
+ ReplicationSupervisor supervisor = ReplicationSupervisor.newBuilder()
+ .stateContext(context)
+ .replicationConfig(repConf)
+ .containerSet(set)
+ .volumeSet(volumeSet)
+ .clock(clock)
+ .build();
+
+ try {
+ datanode.setPersistedOpState(IN_SERVICE);
+ supervisor.nodeStateUpdated(DECOMMISSIONING);
+ supervisor.setPerVolumePoolSize(2);
+ VolumeReplicationThreadPools pools =
+ supervisor.getVolumeReplicationThreadPools();
+ int expected = repConf.scaleOutOfServiceLimit(2);
+ for (StorageVolume volume : volumeSet.getVolumesList()) {
+ assertEquals(expected,
+ pools.getPoolSize(volume.getStorageDir().getPath()));
+ }
+ } finally {
+ supervisor.stop();
+ }
+ }
+
+ @ContainerLayoutTestInfo.ContainerTest
+ public void nonPushReplicationUsesGlobalPoolWhenPerVolumeEnabled(
+ ContainerLayoutVersion layout, @TempDir File perVolumeTempDir) throws
Exception {
+ this.layoutVersion = layout;
+ OzoneConfiguration conf = perVolumeConf(perVolumeTempDir, 1);
+ MutableVolumeSet volumeSet = newVolumeSet(conf);
+ ReplicationServer.ReplicationConfig repConf =
+ conf.getObject(ReplicationServer.ReplicationConfig.class);
+ AtomicInteger globalExecutions = new AtomicInteger();
+
+ ExecutorService trackingGlobal = new AbstractExecutorService() {
+ @Override
+ public void shutdown() {
+ }
+
+ @Override
+ public List<Runnable> shutdownNow() {
+ return emptyList();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return false;
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return false;
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) {
+ return true;
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ globalExecutions.incrementAndGet();
+ command.run();
+ }
+ };
+
+ ReplicationSupervisor supervisor = ReplicationSupervisor.newBuilder()
+ .stateContext(context)
+ .replicationConfig(repConf)
+ .containerSet(set)
+ .volumeSet(volumeSet)
+ .executor(trackingGlobal)
+ .clock(clock)
+ .build();
+
+ try {
+ supervisor.addTask(createReconciliationTask(1L));
+ assertEquals(1, globalExecutions.get());
+ assertEquals(1, supervisor.getReplicationSuccessCount());
+ } finally {
+ supervisor.stop();
+ }
+ }
+
+ @ContainerLayoutTestInfo.ContainerTest
+ public void perVolumePushIsolation(ContainerLayoutVersion layout,
+ @TempDir File perVolumeTempDir) throws Exception {
+ this.layoutVersion = layout;
+ OzoneConfiguration conf = perVolumeConf(perVolumeTempDir, 1);
+ MutableVolumeSet volumeSet = newVolumeSet(conf);
+ HddsVolume vol1 = (HddsVolume) volumeSet.getVolumesList().get(0);
+ HddsVolume vol2 = (HddsVolume) volumeSet.getVolumesList().get(1);
+
+ addContainerOnVolume(1L, vol1, conf);
+ addContainerOnVolume(2L, vol2, conf);
+
+ ReplicationServer.ReplicationConfig repConf =
+ conf.getObject(ReplicationServer.ReplicationConfig.class);
+
+ CountDownLatch vol1Started = new CountDownLatch(1);
+ CountDownLatch vol1Release = new CountDownLatch(1);
+ ContainerReplicator volumeAwareReplicator = task -> {
+ Container<?> container = set.getContainer(task.getContainerId());
+ HddsVolume volume = container.getContainerData().getVolume();
+ if (volume == vol1) {
+ vol1Started.countDown();
+ try {
+ assertTrue(vol1Release.await(10, TimeUnit.SECONDS));
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ throw new AssertionError(ie);
+ }
+ }
+ task.setStatus(DONE);
+ };
+ replicatorRef.set(volumeAwareReplicator);
+
+ ReplicationSupervisor supervisor = ReplicationSupervisor.newBuilder()
+ .stateContext(context)
+ .replicationConfig(repConf)
+ .containerSet(set)
+ .volumeSet(volumeSet)
+ .clock(clock)
+ .build();
+
+ try {
+ supervisor.addTask(createPushTask(1L));
+ assertTrue(vol1Started.await(10, TimeUnit.SECONDS));
+
+ supervisor.addTask(createPushTask(2L));
+ GenericTestUtils.waitFor((BooleanSupplier) () ->
+ supervisor.getReplicationSuccessCount() >= 1, 100, 10000);
+
+ assertEquals(1, supervisor.getReplicationSuccessCount());
+ vol1Release.countDown();
+ GenericTestUtils.waitFor((BooleanSupplier) () ->
+ supervisor.getReplicationSuccessCount() == 2, 100, 10000);
+ } finally {
+ supervisor.stop();
+ }
+ }
+
+ @ContainerLayoutTestInfo.ContainerTest
+ public void volumeFailureCleansUpQueuedTasks(ContainerLayoutVersion layout,
+ @TempDir File perVolumeTempDir) throws Exception {
+ this.layoutVersion = layout;
+ OzoneConfiguration conf = perVolumeConf(perVolumeTempDir, 1);
+ MutableVolumeSet volumeSet = newVolumeSet(conf);
+ HddsVolume vol1 = (HddsVolume) volumeSet.getVolumesList().get(0);
+ addContainerOnVolume(1L, vol1, conf);
+ addContainerOnVolume(2L, vol1, conf);
+
+ ReplicationServer.ReplicationConfig repConf =
+ conf.getObject(ReplicationServer.ReplicationConfig.class);
+
+ CountDownLatch task1Started = new CountDownLatch(1);
+ CountDownLatch task1Block = new CountDownLatch(1);
+ AtomicBoolean task1Interrupted = new AtomicBoolean();
+ replicatorRef.set(task -> {
+ if (task.getContainerId() == 1L) {
+ task1Started.countDown();
+ try {
+ task1Block.await();
+ } catch (InterruptedException ie) {
+ task1Interrupted.set(true);
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ task.setStatus(DONE);
+ });
+
+ ReplicationSupervisor supervisor = ReplicationSupervisor.newBuilder()
+ .stateContext(context)
+ .replicationConfig(repConf)
+ .containerSet(set)
+ .volumeSet(volumeSet)
+ .clock(clock)
+ .build();
+
+ String volumeRoot = vol1.getStorageDir().getPath();
+ try {
+ supervisor.addTask(createPushTask(1L));
+ assertTrue(task1Started.await(10, TimeUnit.SECONDS));
+
+ supervisor.addTask(createPushTask(2L));
+ GenericTestUtils.waitFor((BooleanSupplier) () ->
+ supervisor.getTotalInFlightReplications() == 2, 100, 5000);
+
+ volumeSet.failVolume(volumeRoot);
+ supervisor.shutdownFailedVolumePools(volumeSet);
+
+ GenericTestUtils.waitFor((BooleanSupplier) () ->
+ supervisor.getTotalInFlightReplications() == 0, 100, 5000);
+ assertTrue(task1Interrupted.get());
+ task1Block.countDown();
+
+ supervisor.addTask(createPushTask(2L));
+ GenericTestUtils.waitFor((BooleanSupplier) () ->
+ supervisor.getReplicationSuccessCount() >= 1, 100, 5000);
+ } finally {
+ task1Block.countDown();
+ supervisor.stop();
+ }
+ }
+
+ @ContainerLayoutTestInfo.ContainerTest
+ public void volumeFailureShutsDownPool(ContainerLayoutVersion layout,
+ @TempDir File perVolumeTempDir) throws Exception {
+ this.layoutVersion = layout;
+ OzoneConfiguration conf = perVolumeConf(perVolumeTempDir, 1);
+ MutableVolumeSet volumeSet = newVolumeSet(conf);
+ HddsVolume vol1 = (HddsVolume) volumeSet.getVolumesList().get(0);
+ addContainerOnVolume(1L, vol1, conf);
+
+ ReplicationServer.ReplicationConfig repConf =
+ conf.getObject(ReplicationServer.ReplicationConfig.class);
+ ReplicationSupervisor supervisor = ReplicationSupervisor.newBuilder()
+ .stateContext(context)
+ .replicationConfig(repConf)
+ .containerSet(set)
+ .volumeSet(volumeSet)
+ .executor(newDirectExecutorService())
+ .clock(clock)
+ .build();
+ replicatorRef.set(doneReplicator);
+
+ String volumeRoot = vol1.getStorageDir().getPath();
+ try {
+ VolumeReplicationThreadPools pools =
+ supervisor.getVolumeReplicationThreadPools();
+ assertTrue(pools.hasPool(volumeRoot));
+
+ volumeSet.failVolume(volumeRoot);
+ supervisor.shutdownFailedVolumePools(volumeSet);
+ assertFalse(pools.hasPool(volumeRoot));
+
+ supervisor.addTask(createPushTask(1L));
+ assertEquals(1, supervisor.getReplicationSuccessCount());
+ } finally {
+ supervisor.stop();
+ }
+ }
+
+ private OzoneConfiguration perVolumeConf(File baseDir, int perVolumeStreams)
{
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.set(HddsConfigKeys.OZONE_METADATA_DIRS, baseDir.getAbsolutePath());
+ conf.set(ScmConfigKeys.HDDS_DATANODE_DIR_KEY,
+ baseDir.getAbsolutePath() + "/vol1,"
+ + baseDir.getAbsolutePath() + "/vol2");
+ conf.setBoolean(PER_VOLUME_ENABLED_KEY, true);
+ conf.setInt(PER_VOLUME_STREAMS_LIMIT_KEY, perVolumeStreams);
+ conf.setClass(SpaceUsageCheckFactory.Conf.configKeyForClassName(),
+ MockSpaceUsageCheckFactory.HalfTera.class,
+ SpaceUsageCheckFactory.class);
+ return conf;
+ }
+
+ private MutableVolumeSet newVolumeSet(OzoneConfiguration conf)
+ throws IOException {
+ return new MutableVolumeSet(datanode.getUuidString(), conf, null,
+ StorageVolume.VolumeType.DATA_VOLUME, null);
+ }
+
+ private void addContainerOnVolume(long containerId, HddsVolume volume,
+ OzoneConfiguration conf) {
+ KeyValueContainerData containerData = new
KeyValueContainerData(containerId,
+ layoutVersion, 100L,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString());
+ containerData.setVolume(volume);
+ KeyValueContainer container = new KeyValueContainer(containerData, conf);
+ assertDoesNotThrow(() -> set.addContainer(container));
+ }
+
+ private ReplicationTask createPushTask(long containerId) {
+ ReplicateContainerCommand cmd = ReplicateContainerCommand.toTarget(
+ containerId, MockDatanodeDetails.randomDatanodeDetails());
+ cmd.setTerm(CURRENT_TERM);
+ return new ReplicationTask(cmd, replicatorRef.get());
+ }
}
diff --git a/hadoop-hdds/docs/content/feature/Decommission.md
b/hadoop-hdds/docs/content/feature/Decommission.md
index ede26d6c7e8..53461755f58 100644
--- a/hadoop-hdds/docs/content/feature/Decommission.md
+++ b/hadoop-hdds/docs/content/feature/Decommission.md
@@ -93,9 +93,14 @@ Administrators can adjust the following properties in
`ozone-site.xml` to contro
* **Details**: For decommissioning nodes, this limit is scaled by
`hdds.datanode.replication.outofservice.limit.factor`.
* **`hdds.datanode.replication.streams.limit`**
- * **Purpose**: Sets the base number of threads for the replication
thread pool on a DataNode.
+ * **Purpose**: Sets the base size of both the global replication handler
executor and the inbound replication server executor.
* **Default**: `10`.
- * **Details**: For decommissioning nodes, this limit is also scaled by
`hdds.datanode.replication.outofservice.limit.factor`.
+ * **Details**: On decommissioning nodes, the global executor is scaled
by `hdds.datanode.replication.outofservice.limit.factor`. Per-volume pools
replace normal source-side push scheduling, but target-side inbound push
requests remain limited by the inbound replication server executor configured
by this property.
+
+* **`hdds.datanode.replication.per.volume.streams.limit`**
+ * **Purpose**: When `hdds.datanode.replication.per.volume.enabled` is
true, sets the base number of push replication handler threads **per data
volume**.
+ * **Default**: `2` (reconfigurable at runtime).
+ * **Details**: Each volume has its own pool; total push capacity on the
node scales with the number of volumes. On decommissioning or maintenance
nodes, each per-volume pool is scaled by
`hdds.datanode.replication.outofservice.limit.factor`, same as the global pool.
Push replication is typically disk-bound, so one or two concurrent transfers
per volume is often enough to keep a disk busy while isolating slow volumes.
By tuning these properties, administrators can balance the decommissioning
speed against the impact on the cluster's performance.
diff --git a/hadoop-hdds/docs/content/feature/Reconfigurability.md
b/hadoop-hdds/docs/content/feature/Reconfigurability.md
index 8bfe4a7e46b..4592f9d9485 100644
--- a/hadoop-hdds/docs/content/feature/Reconfigurability.md
+++ b/hadoop-hdds/docs/content/feature/Reconfigurability.md
@@ -104,7 +104,8 @@ ozone admin reconfig --service=[OM|SCM|DATANODE]
--address=<ip:port|hostname:por
| `ozone.block.deleting.service.workers` | `10` | Number of block deletion
service workers |
| `ozone.block.deleting.service.interval` | `60s` | Block deletion service run
interval |
| `ozone.block.deleting.service.timeout` | `300s` | Block deletion service
timeout |
-| `hdds.datanode.replication.streams.limit` | `10` | Maximum replication
streams per datanode |
+| `hdds.datanode.replication.streams.limit` | `10` | Base size of the global
replication handler executor and inbound replication server executor |
+| `hdds.datanode.replication.per.volume.streams.limit` | `2` | Maximum push
replication streams per data volume when per-volume replication thread pools
are enabled (separate from streams.limit) |
## Usage Examples
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/reconfig/TestDatanodeReconfiguration.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/reconfig/TestDatanodeReconfiguration.java
index 071661a9e1c..cf29220bac8 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/reconfig/TestDatanodeReconfiguration.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/reconfig/TestDatanodeReconfiguration.java
@@ -21,6 +21,7 @@
import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_BLOCK_DELETING_SERVICE_TIMEOUT;
import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_BLOCK_DELETING_SERVICE_WORKERS;
import static
org.apache.hadoop.ozone.container.common.statemachine.DatanodeConfiguration.HDDS_DATANODE_BLOCK_DELETE_THREAD_MAX;
+import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.PER_VOLUME_STREAMS_LIMIT_KEY;
import static
org.apache.hadoop.ozone.container.replication.ReplicationServer.ReplicationConfig.REPLICATION_STREAMS_LIMIT_KEY;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -53,6 +54,7 @@ void reconfigurableProperties() {
.add(OZONE_BLOCK_DELETING_SERVICE_WORKERS)
.add(OZONE_BLOCK_DELETING_SERVICE_INTERVAL)
.add(OZONE_BLOCK_DELETING_SERVICE_TIMEOUT)
+ .add(PER_VOLUME_STREAMS_LIMIT_KEY)
.add(REPLICATION_STREAMS_LIMIT_KEY)
.addAll(new DatanodeConfiguration().reconfigurableProperties())
.addAll(new TracingConfig().reconfigurableProperties())
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]