This is an automated email from the ASF dual-hosted git repository.
smengcl 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 2a0cf514699 HDDS-16628. RackAware placement should try other replicas'
racks before giving up (#11403)
2a0cf514699 is described below
commit 2a0cf514699f708ccaba07b61ec73b85f3ccfcf3
Author: Peter Lee <[email protected]>
AuthorDate: Sat Oct 10 13:13:15 2026 +0800
HDDS-16628. RackAware placement should try other replicas' racks before
giving up (#11403)
---
.../algorithms/SCMContainerPlacementRackAware.java | 141 +++++++++------------
.../TestSCMContainerPlacementRackAware.java | 60 +++++++++
2 files changed, 123 insertions(+), 78 deletions(-)
diff --git
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/placement/algorithms/SCMContainerPlacementRackAware.java
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/placement/algorithms/SCMContainerPlacementRackAware.java
index 7bcc5fb8926..9f3bd557fa7 100644
---
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/placement/algorithms/SCMContainerPlacementRackAware.java
+++
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/placement/algorithms/SCMContainerPlacementRackAware.java
@@ -24,14 +24,12 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
import org.apache.hadoop.hdds.conf.ConfigurationSource;
import org.apache.hadoop.hdds.protocol.DatanodeDetails;
import org.apache.hadoop.hdds.scm.SCMCommonPlacementPolicy;
import org.apache.hadoop.hdds.scm.exceptions.SCMException;
import org.apache.hadoop.hdds.scm.net.NetConstants;
import org.apache.hadoop.hdds.scm.net.NetworkTopology;
-import org.apache.hadoop.hdds.scm.net.Node;
import org.apache.hadoop.hdds.scm.node.NodeManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -442,95 +440,82 @@ private DatanodeDetails chooseNode(List<DatanodeDetails>
excludedNodes,
List<DatanodeDetails> affinityNodes, List<DatanodeDetails> usedNodes,
long metadataSizeRequired,
long dataSizeRequired) throws SCMException {
- int ancestorGen = RACK_LEVEL;
- int maxRetry = MAX_RETRY;
- List<String> excludedNodesForCapacity = null;
-
- // When affinity node is null, in this case new node to be selected
- // should be in different rack than used nodes rack.
- // Exclude nodes should be just excluded from topology node selection,
- // which is filled in excludedNodesForCapacity
- // Used node rack should not be part of rack selection
- // which is filled in excludedNodes
- if (affinityNodes == null && excludedNodes != null) {
- excludedNodesForCapacity = excludedNodes.stream()
- .map(DatanodeDetails::getNetworkFullPath)
- .collect(Collectors.toList());
- excludedNodes = usedNodes;
- }
-
- boolean isFallbacked = false;
- while (true) {
- metrics.incrDatanodeChooseAttemptCount();
- DatanodeDetails node = null;
- if (affinityNodes != null) {
- for (Node affinityNode : affinityNodes) {
- node = (DatanodeDetails)networkTopology.chooseRandom(
- NetConstants.ROOT, excludedNodesForCapacity, excludedNodes,
- affinityNode, ancestorGen);
- if (node != null) {
- break;
- }
- }
- } else {
- node = (DatanodeDetails)networkTopology.chooseRandom(NetConstants.ROOT,
- excludedNodesForCapacity, excludedNodes, null, ancestorGen);
+ List<String> excludedNodesForCapacity = new ArrayList<>();
+ if (excludedNodes != null) {
+ for (DatanodeDetails node : excludedNodes) {
+ excludedNodesForCapacity.add(node.getNetworkFullPath());
}
+ }
- if (node == null) {
- // cannot find the node which meets all constrains
- LOG.warn("Failed to find the datanode for container. excludedNodes:" +
- (excludedNodes == null ? "" : excludedNodes.toString()) +
- ", affinityNode:" +
- (affinityNodes == null ? "" : affinityNodes.stream()
- .map(Object::toString).collect(Collectors.joining(", "))));
- if (fallback) {
- isFallbacked = true;
- // fallback, don't consider the affinity node
- if (affinityNodes != null) {
- affinityNodes = null;
- continue;
- }
- // fallback, don't consider cross rack
- if (ancestorGen == RACK_LEVEL) {
- ancestorGen--;
- continue;
- }
- }
- // there is no constrains to reduce or fallback is true
- throw new SCMException("No satisfied datanode to meet the" +
- " excludedNodes and affinityNode constrains.", null);
+ // The places to look for a node, in order. Each one gets MAX_RETRY tries:
+ // 1. the rack of each affinity node;
+ // 2. any rack that none of usedNodes is on (right away if there are no
affinity nodes, otherwise only as a
+ // fallback);
+ // 3. any rack at all, as a fallback.
+ List<Step> steps = new ArrayList<>();
+ if (affinityNodes != null) {
+ for (DatanodeDetails affinityNode : affinityNodes) {
+ steps.add(new Step(affinityNode, RACK_LEVEL, false));
}
+ }
+ if (affinityNodes == null || fallback) {
+ steps.add(new Step(null, RACK_LEVEL, affinityNodes != null));
+ }
+ if (fallback) {
+ steps.add(new Step(null, 0, true));
+ }
- if (usedNodes != null && usedNodes.contains(node)) {
- if (excludedNodesForCapacity == null) {
- excludedNodesForCapacity = new ArrayList<>();
+ for (Step step : steps) {
+ int retries = MAX_RETRY;
+ while (retries > 0) {
+ metrics.incrDatanodeChooseAttemptCount();
+ DatanodeDetails node = (DatanodeDetails)
networkTopology.chooseRandom(NetConstants.ROOT,
+ excludedNodesForCapacity, step.affinityNode == null ? usedNodes :
null, step.affinityNode,
+ step.ancestorGen);
+ if (node == null) {
+ LOG.warn("Failed to find the datanode for container. excludedNodes:
{}, affinityNode: {}",
+ excludedNodes, step.affinityNode);
+ break;
}
excludedNodesForCapacity.add(node.getNetworkFullPath());
- continue;
- }
-
- if (isValidNode(node, metadataSizeRequired, dataSizeRequired)) {
- metrics.incrDatanodeChooseSuccessCount();
- if (isFallbacked) {
- metrics.incrDatanodeChooseFallbackCount();
+ if (usedNodes != null && usedNodes.contains(node)) {
+ continue;
+ }
+ if (isValidNode(node, metadataSizeRequired, dataSizeRequired)) {
+ metrics.incrDatanodeChooseSuccessCount();
+ if (step.fallback) {
+ metrics.incrDatanodeChooseFallbackCount();
+ }
+ return node;
}
- return node;
+ retries--;
}
-
- maxRetry--;
- if (maxRetry == 0) {
- // avoid the infinite loop
- String errMsg = "No satisfied datanode to meet the space constrains. "
+ // Decommissioned and maintenance nodes stay in the topology, so a rack
can use up all its tries on them.
+ // If that happens on an affinity node's rack, move on to the next
place. Anywhere else, give up.
+ if (retries == 0 && step.affinityNode == null) {
+ String errMsg = "No writable datanode with enough space was found. "
+ "metadata size required: " + metadataSizeRequired +
" data size required: " + dataSizeRequired;
LOG.info(errMsg);
throw new SCMException(errMsg, null);
}
- if (excludedNodesForCapacity == null) {
- excludedNodesForCapacity = new ArrayList<>();
- }
- excludedNodesForCapacity.add(node.getNetworkFullPath());
+ }
+ throw new SCMException("No satisfied datanode to meet the excludedNodes
and affinityNode constrains.", null);
+ }
+
+ /** One place where chooseNode looks for a node. */
+ private static final class Step {
+ /** Look near this node, or anywhere when null. */
+ private final DatanodeDetails affinityNode;
+ /** RACK_LEVEL: stay on affinityNode's rack, or, without one, avoid the
racks of usedNodes. 0: any rack. */
+ private final int ancestorGen;
+ /** Whether a node found here counts as a fallback in the metrics. */
+ private final boolean fallback;
+
+ Step(DatanodeDetails affinityNode, int ancestorGen, boolean fallback) {
+ this.affinityNode = affinityNode;
+ this.ancestorGen = ancestorGen;
+ this.fallback = fallback;
}
}
diff --git
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/placement/algorithms/TestSCMContainerPlacementRackAware.java
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/placement/algorithms/TestSCMContainerPlacementRackAware.java
index e58579b867c..264d7e61d46 100644
---
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/placement/algorithms/TestSCMContainerPlacementRackAware.java
+++
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/placement/algorithms/TestSCMContainerPlacementRackAware.java
@@ -18,6 +18,7 @@
package org.apache.hadoop.hdds.scm.container.placement.algorithms;
import static
org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeOperationalState.DECOMMISSIONED;
+import static
org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeOperationalState.DECOMMISSIONING;
import static
org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeOperationalState.IN_SERVICE;
import static
org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeState.HEALTHY;
import static
org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_DATANODE_RATIS_VOLUME_FREE_SPACE_MIN;
@@ -678,6 +679,65 @@ public void chooseNodeWithUsedNodesMultipleRack(int
datanodeCount)
cluster.isSameParent(datanodes.get(5), datanodeDetails.get(0)));
}
+ @Test
+ public void chooseNodeWhenUsedNodeRackHasOnlyDecommissionedNodes() throws
SCMException {
+ setup(3 * NODE_PER_RACK);
+ // rack1 has no usable node left: node5 already has a replica, and nodes
6-9 are decommissioned.
+ for (int i = 6; i < 10; i++) {
+ dnInfos.get(i).setNodeStatus(NodeStatus.valueOf(DECOMMISSIONED,
HEALTHY));
+ }
+ // The container has replicas on node5 (rack1) and node0 (rack0). rack1 is
tried first.
+ List<DatanodeDetails> usedNodes = new
ArrayList<>(Arrays.asList(datanodes.get(5), datanodes.get(0)));
+
+ List<DatanodeDetails> datanodeDetails = policy.chooseDatanodes(usedNodes,
new ArrayList<>(), null, 1, 0, 5);
+
+ assertEquals(1, datanodeDetails.size());
+ // Instead of giving up after rack1, placement tries rack0 and finds a
node there.
+ assertTrue(cluster.isSameParent(datanodes.get(0), datanodeDetails.get(0)));
+ assertNotEquals(datanodes.get(0), datanodeDetails.get(0));
+ }
+
+ @Test
+ public void fallbackWhenUsedNodeRacksHaveOnlyDecommissionedNodes() throws
SCMException {
+ setup(3 * NODE_PER_RACK);
+ // The replicas are on node0 (rack0) and node5 (rack1), and neither rack
has another usable node:
+ // node1 and node6 are excluded because their replicas are being
decommissioned, and the rest are decommissioned.
+ dnInfos.get(1).setNodeStatus(NodeStatus.valueOf(DECOMMISSIONING, HEALTHY));
+ for (int i : new int[] {2, 3, 4, 6, 7, 8, 9}) {
+ dnInfos.get(i).setNodeStatus(NodeStatus.valueOf(DECOMMISSIONED,
HEALTHY));
+ }
+ List<DatanodeDetails> usedNodes = new
ArrayList<>(Arrays.asList(datanodes.get(0), datanodes.get(5)));
+ List<DatanodeDetails> excludedNodes = new
ArrayList<>(Arrays.asList(datanodes.get(1), datanodes.get(6)));
+
+ List<DatanodeDetails> datanodeDetails = policy.chooseDatanodes(usedNodes,
excludedNodes, null, 1, 0, 5);
+
+ assertEquals(1, datanodeDetails.size());
+ // After both racks fail, placement falls back to rack2, the only rack
with usable nodes.
+ assertTrue(cluster.isSameParent(datanodes.get(10),
datanodeDetails.get(0)));
+ }
+
+ @Test
+ public void fallbackDoesNotSkipRackOfExcludedNode() throws SCMException {
+ setup(4 * NODE_PER_RACK);
+ // The replicas are on node0 (rack0), node5 (rack1) and node15 (rack3),
and those racks have no other usable
+ // node. node10 on rack2 has a replica that is being decommissioned, so it
is excluded.
+ for (int i : new int[] {1, 2, 3, 4, 6, 7, 8, 9, 16, 17, 18, 19}) {
+ dnInfos.get(i).setNodeStatus(NodeStatus.valueOf(DECOMMISSIONED,
HEALTHY));
+ }
+ dnInfos.get(10).setNodeStatus(NodeStatus.valueOf(DECOMMISSIONING,
HEALTHY));
+ List<DatanodeDetails> usedNodes =
+ new ArrayList<>(Arrays.asList(datanodes.get(0), datanodes.get(5),
datanodes.get(15)));
+ List<DatanodeDetails> excludedNodes = new
ArrayList<>(Arrays.asList(datanodes.get(10)));
+
+ List<DatanodeDetails> datanodeDetails = policy.chooseDatanodes(usedNodes,
excludedNodes, null, 1, 0, 5);
+
+ assertEquals(1, datanodeDetails.size());
+ // The fallback looks for a rack with no replica on it. That is rack2, and
the excluded node there must not
+ // rule out the whole rack.
+ assertTrue(cluster.isSameParent(datanodes.get(10),
datanodeDetails.get(0)));
+ assertNotEquals(datanodes.get(10), datanodeDetails.get(0));
+ }
+
@Test
public void chooseSingleNodeRackWithUsedAndExcludeNodes()
throws SCMException {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]