greenwich opened a new pull request, #11838:
URL: https://github.com/apache/ozone/pull/11838

   ## What changes were proposed in this pull request?
   
   On every read that returns block locations (`GetKeyInfo`, `lookupKey`, 
`getFileStatus`, `listStatus`), OM sorts each block's datanodes by distance to 
the client: `KeyManagerImpl.sortDatanodes` → 
`NetworkTopologyImpl.sortByDistanceCost` → `getDistanceCost` **once per node**. 
Each `getDistanceCost` call takes the read lock of `netlock`, a single global 
fair `ReentrantReadWriteLock`. For non-datanode clients, `getOtherNode` also 
takes it once per request to look up the client's rack.
   
   With many concurrent readers, every read-lock acquisition CASes one shared 
state word and looks up a per-thread hold count in a `ThreadLocal`. With 100+ 
handlers this turns into CAS retries and long `ThreadLocal` probes, OM CPU 
climbs while throughput falls, and slower calls keep more handlers in flight.
   
   Observed in production (2.2.1, 3-node OM HA, ~32 datanodes, 168-core OM 
host):
   - The OM leader bursts from a 3–5 core baseline to **80–135 cores for 1–5 
minutes**, 20+ times a day, as concurrent S3 GETs rise to ~15–30k/s.
   - During a burst, `GetKeyInfo` takes **~6 ms** against 1.5–2.5 ms at 
baseline, at a lower call rate.
   - `om_performance_metrics_get_key_info_sort_datanodes_latency_ns_avg_time`: 
**0.01 ms** quiet, **4.2 ms** at the burst peak, ~70% of the call.
   - JFR on the IPC handlers: **68%** of CPU samples in 
`ThreadLocal$ThreadLocalMap.getEntryAfterMiss` (9% at baseline). Of those, 
**85%** come from `ReadLock.lock` in `getDistanceCost` ← `sortByDistanceCost`, 
and **15%** from `getNode` ← `KeyManagerImpl.getOtherNode`.
   
   HDFS solved this long ago: `NetworkTopology` has the same fair `netlock`, 
but `sortByDistance` computes weights with `getWeight`, a lock-free walk of 
parent pointers that compares nodes by path (`NodeBase.equals`). This PR 
applies the same approach.
   
   ### Changes
   
   1. **`NetworkTopologyImpl`: lock-free distance walk for sorting.**
      - The cost walk moves into `getDistanceCostWithoutLock`, which 
`sortByDistanceCost` uses. `getDistanceCost` keeps the lock for its other 
callers and delegates to the same method, so the two cannot diverge.
      - Tree nodes are compared by full path, as HDFS does, instead of 
`InnerNodeImpl.equals`. That method compares whole child maps when the two 
nodes are different instances; without the lock it could iterate a map that SCM 
is modifying.
      - The cost formula is unchanged (schema `getCost()` sums, 
`Integer.MAX_VALUE` for nodes outside the tree). HDFS's hop count is not used, 
because Ozone schemas support per-level costs.
   2. **`KeyManagerImpl`: cache resolved non-datanode clients.**
      - `getOtherNode` caches the client `Node` per client host. This removes 
the per-request `getNode` lock, the mapping lookup and a `NodeImpl` allocation.
      - The cache belongs to one cluster-map instance. `ScmTopologyClient` 
replaces the instance on refresh, and the cache is replaced with it, so a 
cached node always points at the current tree.
      - It holds at most 10,000 entries, and entries expire after 
`ozone.om.network.topology.refresh.duration` (default 1h).
   
   ### Edge cases
   
   - **OM:** the cluster map is built once, published through an 
`AtomicReference`, and never modified, so the lock protected nothing there. 
Results are identical.
   - **SCM:** the topology does change, and a concurrent change can make one 
sort see a stale cost. That's the same tolerance HDFS accepts, and it's 
harmless for replica ordering. If a node is removed mid-walk, so that its 
parent becomes `null` after the ancestor check, the walk returns `MAX_VALUE` 
instead of a partial cost, so a node being removed never sorts first.
   - **Path comparison changes two cases, neither expected in practice:**
     - A node attached to another topology instance with the same paths now 
gets its real cost instead of being treated as outside the cluster.
     - A non-datanode client whose hostname equals a datanode's network name on 
the same rack costs 0 (same host) instead of 2. The read path matches such 
clients to the datanode before this point.
   - **Clients on a rack with no datanodes:** the rack is not in OM's map. This 
result is cached too, because rack existence only changes with the map instance.
   - **Resolution failures:** when the mapping returns nothing or throws, the 
result is not cached and is retried on the next request, as today (HDDS-16776 
is unchanged).
   - **Mappings that don't cache:** the default `ScriptBasedMapping` caches 
successful lookups forever, so nothing changes for it. A mapping configured 
directly, such as `StaticMapping` or a custom class, is now consulted at most 
once per refresh interval per client.
   - **Unchanged:**
     - When the client is itself a pipeline datanode, OM still does one 
`getNode` lookup per request.
     - Read-path rack locality is unchanged. Pipeline nodes deserialized from 
SCM are not linked into OM's map, so they still score `MAX_VALUE` (cheaply 
now). That is fixed separately in HDDS-17555.
   
   ## What is the link to the Apache JIRA
   
   https://issues.apache.org/jira/browse/HDDS-17553
   
   ## How was this patch tested?
   
   New tests:
   - `TestNetworkTopologyImpl`:
     - `testSortByDistanceCostDoesNotTakeLockPerNode`: sorting never calls the 
locked `getDistanceCost`. It fails on the old code.
     - `testSortByDistanceCostWhileTopologyChanges`: four threads sort while a 
fifth removes and re-adds half the nodes, on all five schema topologies. Every 
result contains all nodes, and the nodes that aren't changing stay ordered by 
cost.
     - `testDistanceCostIsMaxWhenNodeIsRemovedDuringWalk`: a node that loses 
its parent after the ancestor check costs `MAX_VALUE`. The old code gave it 1, 
below a same-rack node's 2.
   - `TestOMSortDatanodes`:
     - `cachesResolvedClientPerClusterMap`: same node for the same map, a new 
one for a new map instance.
     - `cachesClientOnRackWithoutDatanodes`: the "no rack in this map" result 
is cached per map instance.
     - `cachedClientExpiresAfterTopologyRefreshInterval`: an entry expires 
after the refresh interval.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to