Skip to content

HDDS-15059. Shift streaming write sortDatanodes logic to OM#10633

Open
chihsuan wants to merge 29 commits into
apache:masterfrom
chihsuan:HDDS-15059
Open

HDDS-15059. Shift streaming write sortDatanodes logic to OM#10633
chihsuan wants to merge 29 commits into
apache:masterfrom
chihsuan:HDDS-15059

Conversation

@chihsuan

@chihsuan chihsuan commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed?

Move streaming-write pipeline sorting from SCM to OM. OM already caches the cluster topology; making SCM repeat the sort on every allocateBlock adds work to the SCM hot path, especially when several OMs share one SCM.

The new ozone.om.block.write.sort.datanodes.enabled option is disabled by default, preserving the current SCM-side behavior. When enabled:

  • OM resolves RPC-deserialized pipeline nodes against its cached topology before sorting, then maps the order back to the original nodes.
  • The client is matched by IP or hostname. If it is empty or cannot be resolved, the original pipeline order is kept because the first node is the write primary.
  • Pipelines with the same nodes are sorted once per allocation request.
  • If the OM topology cache is unavailable, the client address is sent to SCM and SCM performs the sort.

SCM behavior is unchanged, so old OMs still get SCM-side sorting during a rolling upgrade. There is no Protobuf or RPC change.

JIRA

https://issues.apache.org/jira/browse/HDDS-15059

Testing

  • TestOMAllocateBlockRequest: configuration paths, SCM fallback, empty clients, pipeline ordering, and per-pipeline caching.
  • TestOMSortDatanodes: topology-aware ordering for RPC-deserialized nodes, IP/hostname matching, and unresolved clients.
  • Fork build-branch CI: https://github.com/chihsuan/ozone/actions/runs/28455187435

Generated-by: Claude Code (Claude Opus 4.8)

@chihsuan chihsuan marked this pull request as ready for review June 30, 2026 17:08

@peterxcli peterxcli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@ivandika3 Is there any risk that a block ends up allocated on a suboptimal datanodes/pipeline because the OM cache topology is somehow stale?⁠

@ivandika3

ivandika3 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

@peterxcli thanks for checking.

Is there any risk that a block ends up allocated on a suboptimal datanodes/pipeline because the OM cache topology is somehow stale?⁠

Should have some effect on performance but not correctness, since Streaming Write Pipeline should be able to pick an arbitrary topology. The the data path (streaming WriteChunk data) can be sent to any primary (first node) is separated from the metadata path (PutBlock) which will be sent to the DN leader. The impact of suboptimal Streaming write pipeline topology should be worse write latency (e.g. if the topology picks the furthest node as the primary node). But this possible performance penalty also apply to read path (i.e. where the further node is read first) so I think it should be acceptable. Please let me know if I miss something.

@chihsuan Thanks for the patch, I'll review this soon.

@ivandika3 ivandika3 requested a review from szetszwo July 2, 2026 01:36
@szetszwo

szetszwo commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

... We can do the sorting on the OM instead of SCM to reduce SCM RPC processing time.

@ivandika3 , @chihsuan , Would this change improve the overall performance?

OM is supposed to be more CPU demanding than SCM. So, we probably want to save the CPU cycles in OM. What do you think?

@ivandika3

ivandika3 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@szetszwo Thanks for checking this out.

The reason I raised for this is to follow the HDDS-9343 which should make the sort datanodes logic done in OM.

Would this change improve the overall performance?

I have not measured the overall performance difference yet, but for a single OM and single OM service, the performance improvement (if any) might not be that much.

OM is supposed to be more CPU demanding than SCM. So, we probably want to save the CPU cycles in OM. What do you think?

That is a good point. However, in our cluster we have multiple OM services that points to a single SCM service, so I think SCM service resources should be protected more since it can be the bottleneck as more OM services point to the same SCM service. Additionally, ideally SCM should spend most of its times on background services (processing heartbeat reports, etc) and therefore should spend as little time as possible in the user foreground processing (i.e. allocating blocks, fetching read pipelines, etc) which includes sort datanodes.

Please let me know what you think. This is not really a critical issue so I think we can defer it if you don't see any reason to support it now.

@szetszwo

szetszwo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

... in our cluster we have multiple OM services ...

Good to know that you are running with multiple OM services!

... This is not really a critical issue so I think we can defer it if you don't see any reason to support it now.

How about making it configurable?

@ivandika3

Copy link
Copy Markdown
Contributor

How about making it configurable?

Yes, making it configurable is a good idea.

@chihsuan

chihsuan commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @szetszwo and @ivandika3 for the discussion.

I'll add an OM-side boolean (e.g. ozone.om.block.write.sort.datanodes.enabled, name open to bikeshed), defaulting to false (SCM sorts, legacy behavior) so the default doesn't add OM CPU. Set true to sort on OM instead, for multiple OMs behind one SCM.

Does false sound like the right default, or would you prefer true?

@ivandika3

ivandika3 commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Does false sound like the right default, or would you prefer true?

@chihsuan Thanks for the follow up, let's set the default to false to preserve the current behavior. We can remove this configuration in the future if there is a significant performance improvement.

@chihsuan

chihsuan commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Hi @ivandika3 I've added an OM-side toggle in ec36f7d (default false, so SCM keeps doing the sort; set true to sort on OM). The change is pushed. Reviews welcome whenever you get a chance, thanks! 🙏

@ivandika3 ivandika3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @chihsuan for the update, left some more comments.

Comment on lines +229 to +230
final Map<List<DatanodeDetails>, List<? extends DatanodeDetails>> sortedByNodes =
sortOnOm ? new HashMap<>() : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In KeyManagerImpl#sortDatanodes, this logic uses Map<Set<String>< List<? extends DatanodeDetails>> but in here it is Map<List<DatanodeDetails>, List<? extends DatanodeDetails>>. Any reason for this? If so, can we standardize the implementation?

I think there is no need to get the use the DatanodeDetails::getUuidString in KeyManagerImpl#sortedByNodes.

@ivandika3 ivandika3 Jul 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also this comment is not entirely accurate

// Cache the sorted order per pipeline so blocks sharing a pipeline are
// sorted once (mirrors the read path's per-pipeline caching).

If there are two blocks with different pipelines, but consist of the same set of datanodes, they will share the same cache.

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.

Any reason for this? If so, can we standardize the implementation?
I think there is no need to get the use the DatanodeDetails::getUuidString in KeyManagerImpl#sortedByNodes.

Agreed on both. Since DatanodeDetails#equals/hashCode are UUID-based, keying by the node list is already equivalent to keying by UUIDs, so the getUuidString -> Set<String> step is redundant.

I originally left the read path untouched to keep the change scoped to the write path, but since you raised it, I've dropped getUuidString and standardized both paths to key by the pipeline node list in 7d3bc3e. Thanks!

@chihsuan chihsuan Jul 8, 2026

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.

Also this comment is not entirely accurate

Good catch. Reworded to "blocks whose pipelines have the same datanodes are sorted once" in 5d67fdd

Comment on lines 2269 to 2276
/**
* Resolve a node to its canonical, topology-linked instance in the given
* cluster map, or return the input node if it is not in the map.
*/
private Node toClusterMapNode(NetworkTopology clusterMap, Node node) {
final Node resolved = clusterMap.getNode(node.getNetworkFullPath());
return resolved != null ? resolved : node;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: The javadoc is kind of confusing with terms like "canonical", "topology-linked".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also instead of returning the unresolved node, should we return null instead and skip the whole sort by distance since it should fail (e.g. the unresolved node should not have the correct ancestor information)? If so, then we can simply use thie NetworkTopologyImpl#getNode and we don't need this function.

Also I saw that before this toClusterMapNode we already resolve the client machine in getClientNode. If that's the case, is this still needed?

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.

Nit: The javadoc is kind of confusing with terms like "canonical", "topology-linked".

Thanks, I've updated both javadocs to describe the lookup plainly and dropped "canonical". d310622

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.

should we return null instead and skip the whole sort by distance since it should fail (e.g. the unresolved node should not have the correct ancestor information)?

That works for datanodes, which we already skip the sort if one is missing from the map, but not for the client. A client that isn't a datanode is never in the map, yet it still sorts fine because getOtherNode attaches it to its rack. Returning null would skip sorting for all external clients.

is this still needed?

Yes, getClientNode could return an RPC-deserialized node that isn't linked to the topology. But you're right it's better done inside getClientNode itself, so I moved the lookup there and removed toClusterMapNode in 356023d. Hope this makes it easier to read, thanks for the feedback!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

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

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.

Copilot AI review requested due to automatic review settings July 10, 2026 13:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings July 10, 2026 13:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

@chihsuan

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review! @szetszwo I merged the latest master to build on the #10701 (HDDS-15803) refactor and applied the proposed changes:

  • Added SCM fallback when the OM topology cache is unavailable.
  • Added the nullable topology accessor while preserving the strict OzoneManager#getClusterMap() contract.
  • Removed the NullPointerException catch from KeyManagerImpl.
  • Added a unit test for the fallback path.

The only difference from the suggested snippet is the condition for OM-side sorting: it checks that OM sorting is enabled and the topology is available. This preserves the configuration semantics. Otherwise, sorting falls back to SCM.

I've also addressed all the Copilot review comments.

When you have a chance, could you please take another look? Thank you! 🙇‍♂️

@szetszwo szetszwo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@chihsuan , thanks for the update! Please see the comments inlined.

Comment on lines +2168 to +2169
final Map<Set<String>, List<? extends DatanodeDetails>> sortedPipelines = new HashMap<>();
final Map<PipelineID, List<? extends DatanodeDetails>> sortedPipelines = new HashMap<>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a question: why change key from UUIDs to PipelineID?

One difference I can see is that the set of UUIDs (i.e. datanodes) can have multiple pipelines. In such case, we sort the same set of datanodes multiple times. Not sure if this case is common.

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.

Good question! I switched it to PipelineID because Copilot flagged the new write-path cache for using an order-sensitive List key and aligned the read path to match for consistency.

One difference I can see is that the set of UUIDs (i.e. datanodes) can have multiple pipelines. In such case, we sort the same set of datanodes multiple times. Not sure if this case is common.

That's a fair point, and it changes my mind. The sort depends only on the datanode set, so keying by the set collapses them into a single sort, while PipelineID re-sorts each. It's never worse than PipelineID, and it's what the read path is already used on master. I've reverted both to the UUID set (7307ae2).

final List<DatanodeDetails> nodes = pipeline.getNodes();
final List<? extends DatanodeDetails> sorted = sortedByPipeline
.computeIfAbsent(pipeline.getId(),
id -> keyManager.sortDatanodesForWrite(nodes, omClientMachine));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should also pass the clusterMap.

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.

Good call, done in 59b6b3b

Copilot AI review requested due to automatic review settings July 11, 2026 06:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 11, 2026 07:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings July 11, 2026 07:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Copilot AI review requested due to automatic review settings July 11, 2026 07:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

@chihsuan

Copy link
Copy Markdown
Contributor Author

Thanks @szetszwo I've pushed a few commits addressing comments (replied inline for details).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants