Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a767f7b
HDDS-15059. Add OM-side sortDatanodesForWrite with latency metric
chihsuan Jun 29, 2026
35ad85c
HDDS-15059. Sort write pipeline on OM instead of SCM during allocateB…
chihsuan Jun 29, 2026
c3ea84c
HDDS-15059. Address review: metric getter visibility and test helper …
chihsuan Jun 29, 2026
fa5a466
HDDS-15059. Guard empty client and cache per-pipeline sort in allocat…
chihsuan Jun 29, 2026
5b19b72
HDDS-15059. Strengthen allocateBlock sort tests for reorder and empty…
chihsuan Jun 30, 2026
e29a899
HDDS-15059. Match write-sort client by both hostname and IP
chihsuan Jun 30, 2026
91bed08
HDDS-15059. Resolve write-sort pipeline nodes to OM cluster map
chihsuan Jun 30, 2026
59f9c26
HDDS-15059. Consolidate write-sort datanode tests
chihsuan Jun 30, 2026
13f1e1a
HDDS-15059. Read cluster map once per sort to avoid topology swap race
chihsuan Jun 30, 2026
5040d00
HDDS-15059. Simplify write-sort client handling
chihsuan Jun 30, 2026
ec36f7d
HDDS-15059. Make OM-side write sortDatanodes configurable (default off)
chihsuan Jul 6, 2026
7d3bc3e
HDDS-15059. Key read-sort cache by pipeline nodes, dropping getUuidSt…
chihsuan Jul 8, 2026
5d67fdd
HDDS-15059. Clarify write-sort cache comment (keyed by pipeline nodes)
chihsuan Jul 8, 2026
d310622
HDDS-15059. Reword write-sort javadoc to drop unclear terms
chihsuan Jul 8, 2026
356023d
HDDS-15059. Resolve co-located write client in getClientNode
chihsuan Jul 8, 2026
16086e2
HDDS-15059. Split write datanode sort cases
chihsuan Jul 9, 2026
f48037d
HDDS-15059. Require client machine for write sorting
chihsuan Jul 9, 2026
914bf1b
HDDS-15059. Keep write order without OM topology cache
chihsuan Jul 9, 2026
da8b21d
Merge remote-tracking branch 'upstream/master' into HDDS-15059
chihsuan Jul 10, 2026
8f420e9
HDDS-15059. Fix import order in TestOMAllocateBlockRequest
chihsuan Jul 10, 2026
0d47a4b
HDDS-15059. Fall back to SCM when OM topology is unavailable
chihsuan Jul 10, 2026
356f007
HDDS-15059. Keep pipeline order when client remote address is empty
chihsuan Jul 10, 2026
0229ec3
HDDS-15059. Key allocateBlock write-sort cache by pipeline id
chihsuan Jul 10, 2026
3b9e456
HDDS-15059. Key read-path sort cache by pipeline id for consistency
chihsuan Jul 10, 2026
49f4b02
HDDS-15059. Assert write order via getNodesInOrder and clarify client…
chihsuan Jul 10, 2026
59b6b3b
HDDS-15059. Pass OM cluster map into sortDatanodesForWrite
chihsuan Jul 11, 2026
7307ae2
HDDS-15059. Key write-sort cache by datanode UUID set, matching read …
chihsuan Jul 11, 2026
de7d86c
HDDS-15059. Cache datanode sort only when an actual sort happens
chihsuan Jul 11, 2026
93b0da9
HDDS-15059. Null-check clusterMap and document instance-return contract
chihsuan Jul 11, 2026
7e20ab8
Merge branch 'apache:master' into HDDS-15059
chihsuan Jul 12, 2026
fc381ea
Potential fix for pull request finding
chihsuan Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

package org.apache.hadoop.hdds.scm.client;

import static java.util.Objects.requireNonNull;
import static org.apache.hadoop.hdds.scm.net.NetConstants.ROOT;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_OM_NETWORK_TOPOLOGY_REFRESH_DURATION;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_OM_NETWORK_TOPOLOGY_REFRESH_DURATION_DEFAULT;
Expand Down Expand Up @@ -60,8 +59,7 @@ public ScmTopologyClient(
}

public NetworkTopology getClusterMap() {
return requireNonNull(cache.get(),
"ScmBlockLocationClient must have been initialized already.");
return cache.get();
}
Comment on lines 61 to 63

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional and has already been handled one layer up.


public void start(ConfigurationSource conf) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,18 @@ public class OmConfig extends ReconfigurableConfig {
)
private long followerReadLocalLeaseTimeMs;

@Config(key = "ozone.om.block.write.sort.datanodes.enabled",
defaultValue = "false",
type = ConfigType.BOOLEAN,
tags = {ConfigTag.OM, ConfigTag.PERFORMANCE},
description = "If true, OM sorts the streaming-write pipeline (nearest " +
"datanode first) locally using its cached cluster topology, instead " +
"of asking SCM to sort on every allocateBlock. Defaults to false so " +
"SCM performs the sort. Enable this to offload the sort from SCM " +
"when multiple OM services share a single SCM service."
)
private boolean sortDatanodesForWriteEnabled;

public long getRatisBasedFinalizationTimeout() {
return ratisBasedFinalizationTimeout;
}
Expand Down Expand Up @@ -224,6 +236,10 @@ public void setAllowLeaderSkipLinearizableRead(boolean newValue) {
allowLeaderSkipLinearizableRead = newValue;
}

public boolean isSortDatanodesForWriteEnabled() {
return sortDatanodesForWriteEnabled;
}

public boolean isFollowerReadLocalLeaseEnabled() {
return followerReadLocalLeaseEnabled;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static org.apache.hadoop.hdds.scm.net.NetConstants.ROOT_LEVEL;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.mock;

import com.google.common.collect.ImmutableMap;
Expand Down Expand Up @@ -176,6 +177,57 @@ private static void assertRackOrder(String rack, List<? extends DatanodeDetails>
}
}

@Test
public void sortDatanodesForWriteSortsRpcDeserializedPipeline() {
// Pipeline nodes arrive from SCM over RPC as deserialized DatanodeDetails
// with no topology linkage; the write sort must resolve them to OM's cluster
// map, otherwise every node is equidistant (MAX) and the order is random.
List<DatanodeDetails> rpcNodes = new ArrayList<>();
for (DatanodeDetails dn : nodeManager.getAllNodes()) {
rpcNodes.add(DatanodeDetails.getFromProtoBuf(dn.getProtoBufMessage()));
}
for (DatanodeDetails dn : nodeManager.getAllNodes()) {
// The client address is normally an IP, but the sort must resolve a client
// by either IP or hostname, so cover both.
List<? extends DatanodeDetails> byIp =
keyManager.sortDatanodesForWrite(rpcNodes, dn.getIpAddress(), om.getClusterMap());
assertEquals(dn, byIp.get(0),
"Source node should be sorted first for writes (IP client)");
assertRackOrder(dn.getNetworkLocation(), byIp);

List<? extends DatanodeDetails> byHostname =
keyManager.sortDatanodesForWrite(rpcNodes, dn.getHostName(), om.getClusterMap());
assertEquals(dn, byHostname.get(0),
"Source node should be sorted first for writes (hostname client)");
assertRackOrder(dn.getNetworkLocation(), byHostname);
}
}

@Test
public void sortDatanodesForWriteKeepsOrderForStaleTopology() {
List<DatanodeDetails> nodes = new ArrayList<>();
nodes.add(randomDatanodeDetails());
nodes.addAll(nodeManager.getAllNodes());

List<? extends DatanodeDetails> sorted =
keyManager.sortDatanodesForWrite(nodes, "edge0", om.getClusterMap());

assertSame(nodes, sorted,
"Pipeline order should be preserved when a node is missing from the OM topology");
}

@Test
public void sortDatanodesForWriteKeepsOrderWhenClientUnresolved() {
List<? extends DatanodeDetails> nodes = nodeManager.getAllNodes();
List<DatanodeDetails> original = new ArrayList<>(nodes);
// A client that resolves to no known rack must NOT trigger a shuffle.
String unresolved = nodes.get(0).getIpAddress() + "X";
List<? extends DatanodeDetails> result =
keyManager.sortDatanodesForWrite(nodes, unresolved, om.getClusterMap());
assertEquals(original, result,
"Write pipeline order must be preserved when client is unresolved");
}

private String nodeAddress(DatanodeDetails dn) {
boolean useHostname = config.getBoolean(
HddsConfigKeys.HDDS_DATANODE_USE_DN_HOSTNAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import java.util.List;
import java.util.Map;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.protocol.DatanodeDetails;
import org.apache.hadoop.hdds.scm.net.NetworkTopology;
import org.apache.hadoop.hdds.utils.BackgroundService;
import org.apache.hadoop.hdds.utils.db.Table;
import org.apache.hadoop.hdds.utils.db.TableIterator;
Expand Down Expand Up @@ -363,4 +365,27 @@ DeleteKeysResult getPendingDeletionSubFiles(long volumeId, long bucketId, OmKeyI
* @return BackgroundService
*/
CompactionService getCompactionService();

/**
* Sort the datanodes of a write pipeline by network-topology distance to the
* client, using OM's locally cached cluster map. Unlike the read-path sort,
* the original order is preserved when the client cannot be resolved, because
* the first node is used as the streaming-write primary.
*
* @param nodes the pipeline nodes to sort
* @param clientMachine client address (IP or hostname)
* @param clusterMap OM's cached cluster map used to resolve topology distance
* @return nodes sorted nearest-first, or the original {@code nodes} list
* instance unchanged when sorting is skipped (client unresolved or stale
* topology); callers may use reference equality to detect a skipped sort
*/
List<? extends DatanodeDetails> sortDatanodesForWrite(
List<? extends DatanodeDetails> nodes, String clientMachine, NetworkTopology clusterMap);

/**
* @return true if OM should sort the streaming-write pipeline locally
* ({@code ozone.om.block.write.sort.datanodes.enabled}); false to leave
* the sort to SCM.
*/
boolean isSortDatanodesForWriteEnabled();
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@
import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension;
import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension.EncryptedKeyVersion;
import org.apache.hadoop.fs.FileEncryptionInfo;
import org.apache.hadoop.hdds.HddsConfigKeys;
import org.apache.hadoop.hdds.client.BlockID;
import org.apache.hadoop.hdds.client.ReplicationConfig;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
Expand All @@ -124,6 +123,7 @@
import org.apache.hadoop.hdds.scm.ScmConfigKeys;
import org.apache.hadoop.hdds.scm.container.common.helpers.ContainerWithPipeline;
import org.apache.hadoop.hdds.scm.net.InnerNode;
import org.apache.hadoop.hdds.scm.net.NetworkTopology;
import org.apache.hadoop.hdds.scm.net.Node;
import org.apache.hadoop.hdds.scm.net.NodeImpl;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
Expand Down Expand Up @@ -2186,7 +2186,12 @@ private void sortDatanodes(String clientMachine, List<OmKeyInfo> keyInfos) {
List<? extends DatanodeDetails> sortedNodes = sortedPipelines.get(uuidSet);
if (sortedNodes == null) {
sortedNodes = sortDatanodes(nodes, clientMachine);
if (sortedNodes != null) {
// Cache only a freshly sorted order, not an input list returned
// unchanged when no sort happens: that order is per-pipeline and must
// not be reused for another pipeline with the same node set. The read
// sort always returns a new list, so this never skips caching here; it
// keeps the pattern identical to the write path.
if (sortedNodes != null && sortedNodes != nodes) {
sortedPipelines.put(uuidSet, sortedNodes);
}
} else if (LOG.isDebugEnabled()) {
Expand All @@ -2204,32 +2209,87 @@ private void sortDatanodes(String clientMachine, List<OmKeyInfo> keyInfos) {
@VisibleForTesting
public List<? extends DatanodeDetails> sortDatanodes(List<? extends DatanodeDetails> nodes,
String clientMachine) {
final Node client = getClientNode(clientMachine, nodes);
return ozoneManager.getClusterMap()
.sortByDistanceCost(client, nodes, nodes.size());
final NetworkTopology clusterMap = ozoneManager.getClusterMap();
final Node client = getClientNode(clientMachine, nodes, clusterMap);
return clusterMap.sortByDistanceCost(client, nodes, nodes.size());
}

@Override
public List<? extends DatanodeDetails> sortDatanodesForWrite(
List<? extends DatanodeDetails> nodes, String clientMachine, NetworkTopology clusterMap) {
Preconditions.checkArgument(!StringUtils.isEmpty(clientMachine),
"clientMachine is empty");
Preconditions.checkNotNull(clusterMap, "clusterMap is null");
return captureLatencyNs(
metrics.getAllocateBlockSortDatanodesLatencyNs(), () -> {
final Node client = getClientNode(clientMachine, nodes, clusterMap);
if (client == null) {
// Preserve pipeline order for writes: the first node is the write
// primary, so do not shuffle when the client cannot be resolved.
return nodes;
}
return sortByClusterMapDistance(clusterMap, client, nodes);
});
}

@Override
public boolean isSortDatanodesForWriteEnabled() {
return ozoneManager.getConfig().isSortDatanodesForWriteEnabled();
}

/**
* Sort a pipeline's nodes by topology distance to the client. The nodes come
* from SCM over RPC, so they are deserialized {@link DatanodeDetails} with no
* parent/level: the topology treats them as unknown (distance
* {@link Integer#MAX_VALUE}) and the order comes out random. Look each node
* up in OM's cluster map to get the topology-linked instance, sort those,
* then map the order back to the original nodes.
*/
private List<? extends DatanodeDetails> sortByClusterMapDistance(
NetworkTopology clusterMap, Node client,
List<? extends DatanodeDetails> nodes) {
final List<Node> topologyNodes = new ArrayList<>(nodes.size());
final Map<String, DatanodeDetails> nodeByPath = new HashMap<>();
for (DatanodeDetails node : nodes) {
final Node resolved = clusterMap.getNode(node.getNetworkFullPath());
if (resolved == null) {
return nodes;
}
topologyNodes.add(resolved);
nodeByPath.put(resolved.getNetworkFullPath(), node);
}
final List<Node> sorted =
clusterMap.sortByDistanceCost(client, topologyNodes, topologyNodes.size());
final List<DatanodeDetails> result = new ArrayList<>(sorted.size());
for (Node node : sorted) {
result.add(nodeByPath.get(node.getNetworkFullPath()));
}
return result;
}

private Node getClientNode(String clientMachine,
List<? extends DatanodeDetails> nodes) {
List<DatanodeDetails> matchingNodes = new ArrayList<>();
boolean useHostname = ozoneManager.getConfiguration().getBoolean(
HddsConfigKeys.HDDS_DATANODE_USE_DN_HOSTNAME,
HddsConfigKeys.HDDS_DATANODE_USE_DN_HOSTNAME_DEFAULT);
List<? extends DatanodeDetails> nodes, NetworkTopology clusterMap) {
for (DatanodeDetails node : nodes) {
if ((useHostname ? node.getHostName() : node.getIpAddress()).equals(
clientMachine)) {
matchingNodes.add(node);
// Match by either IP or hostname, like SCM's getNodesByAddress. clientMachine
// may be a hostname on the read path; the streaming-write remoteAddress is
// typically an IP. Matching both covers use.datanode.hostname either way.
if (clientMachine.equals(node.getIpAddress())
|| clientMachine.equals(node.getHostName())) {
// The pipeline nodes are RPC-deserialized and not linked into OM's
// cluster map; prefer the map's instance so distance can be computed.
final Node resolved = clusterMap.getNode(node.getNetworkFullPath());
return resolved != null ? resolved : node;
}
}
return !matchingNodes.isEmpty() ? matchingNodes.get(0) :
getOtherNode(clientMachine);
return getOtherNode(clientMachine, clusterMap);
}

private Node getOtherNode(String clientMachine) {
private Node getOtherNode(String clientMachine,
NetworkTopology clusterMap) {
try {
String clientLocation = resolveNodeLocation(clientMachine);
if (clientLocation != null) {
Node rack = ozoneManager.getClusterMap().getNode(clientLocation);
Node rack = clusterMap.getNode(clientLocation);
if (rack instanceof InnerNode) {
return new NodeImpl(clientMachine, clientLocation,
(InnerNode) rack, rack.getLevel() + 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public class OMPerformanceMetrics {
@Metric(about = "Sort datanodes latency in getKeyInfo")
private MutableRate getKeyInfoSortDatanodesLatencyNs;

@Metric(about = "Sort datanodes latency in allocateBlock (streaming write)")
private MutableRate allocateBlockSortDatanodesLatencyNs;

@Metric(about = "resolveBucketLink latency in getKeyInfo")
private MutableRate getKeyInfoResolveBucketLatencyNs;

Expand Down Expand Up @@ -246,6 +249,10 @@ MutableRate getGetKeyInfoSortDatanodesLatencyNs() {
return getKeyInfoSortDatanodesLatencyNs;
}

MutableRate getAllocateBlockSortDatanodesLatencyNs() {
return allocateBlockSortDatanodesLatencyNs;
}

public void setForceContainerCacheRefresh(boolean value) {
forceContainerCacheRefresh.add(value ? 1L : 0L);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.apache.hadoop.ozone.om;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.FS_TRASH_INTERVAL_DEFAULT;
import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.FS_TRASH_INTERVAL_KEY;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_BLOCK_TOKEN_ENABLED;
Expand Down Expand Up @@ -1342,6 +1343,11 @@ public void setScmTopologyClient(
}

public NetworkTopology getClusterMap() {
return requireNonNull(getClusterMapAllowNull(),
"OM topology cache has not been initialized yet.");
}

public NetworkTopology getClusterMapAllowNull() {
return scmTopologyClient.getClusterMap();
}

Expand Down
Loading