HDDS-15059. Shift streaming write sortDatanodes logic to OM#10633
HDDS-15059. Shift streaming write sortDatanodes logic to OM#10633chihsuan wants to merge 29 commits into
Conversation
There was a problem hiding this comment.
@ivandika3 Is there any risk that a block ends up allocated on a suboptimal datanodes/pipeline because the OM cache topology is somehow stale?
|
@peterxcli thanks for checking.
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 , @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? |
|
@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.
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.
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. |
Good to know that you are running with multiple OM services!
How about making it configurable? |
Yes, making it configurable is a good idea. |
|
Thanks @szetszwo and @ivandika3 for the discussion. I'll add an OM-side boolean (e.g. Does |
@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. |
|
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! 🙏 |
| final Map<List<DatanodeDetails>, List<? extends DatanodeDetails>> sortedByNodes = | ||
| sortOnOm ? new HashMap<>() : null; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
Also this comment is not entirely accurate
Good catch. Reworded to "blocks whose pipelines have the same datanodes are sorted once" in 5d67fdd
| /** | ||
| * 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; | ||
| } |
There was a problem hiding this comment.
Nit: The javadoc is kind of confusing with terms like "canonical", "topology-linked".
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!
| public NetworkTopology getClusterMap() { | ||
| return requireNonNull(cache.get(), | ||
| "ScmBlockLocationClient must have been initialized already."); | ||
| return cache.get(); | ||
| } |
There was a problem hiding this comment.
This is intentional and has already been handled one layer up.
|
Thanks for the detailed review! @szetszwo I merged the latest master to build on the #10701 (HDDS-15803) refactor and applied the proposed changes:
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! 🙇♂️ |
| final Map<Set<String>, List<? extends DatanodeDetails>> sortedPipelines = new HashMap<>(); | ||
| final Map<PipelineID, List<? extends DatanodeDetails>> sortedPipelines = new HashMap<>(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
We should also pass the clusterMap.
|
Thanks @szetszwo I've pushed a few commits addressing comments (replied inline for details). |
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
allocateBlockadds work to the SCM hot path, especially when several OMs share one SCM.The new
ozone.om.block.write.sort.datanodes.enabledoption is disabled by default, preserving the current SCM-side behavior. When enabled: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.build-branchCI: https://github.com/chihsuan/ozone/actions/runs/28455187435Generated-by: Claude Code (Claude Opus 4.8)