[ 
https://issues.apache.org/jira/browse/HDDS-16776?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Andrey Yarovoy reassigned HDDS-16776:
-------------------------------------

    Assignee: Andrey Yarovoy

> OM and SCM re-run a failing topology script on every client request because 
> failed rack resolutions are not cached
> ------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16776
>                 URL: https://issues.apache.org/jira/browse/HDDS-16776
>             Project: Apache Ozone
>          Issue Type: Bug
>          Components: Ozone Manager, SCM
>            Reporter: Andrey Yarovoy
>            Assignee: Andrey Yarovoy
>            Priority: Major
>
> h2. Problem
>  
> When \{{net.topology.node.switch.mapping.impl}} is left at its default of
> {\{org.apache.hadoop.net.ScriptBasedMapping}} and 
> \{{net.topology.script.file.name}} points to a script
> that the service cannot execute -- missing file, wrong path, not executable, 
> wrong working directory --
> both OM and SCM attempt to fork that script *once per client request*, 
> forever. Nothing in the stack
> remembers that the previous attempt failed.
>  
> On a cluster where the OM role could not reach its configured script driven 
> by only a handful of distinct client hosts-- the rate comes from repetition, 
> not from a large client population. Each failure logs a full stack trace at 
> WARN.
>  
> Each occurrence is a ~45-line WARN with this shape (line numbers elided -- 
> the observed cluster ran a vendor Hadoop build):
> {code}
> WARN [IPC Server handler 6 on default port 
> 9862]-org.apache.hadoop.net.ScriptBasedMapping: \
> Exception running /path/to/topology.py <client-ip>
> java.io.IOException: Cannot run program "/path/to/topology.py" (in directory 
> "<redacted>"): \
> error=2, No such file or directory
> at java.lang.ProcessBuilder.start(ProcessBuilder.java)
> at org.apache.hadoop.util.Shell.runCommand(Shell.java)
> ...
> at 
> org.apache.hadoop.net.ScriptBasedMapping$RawScriptBasedMapping.resolve(ScriptBasedMapping.java)
> at 
> org.apache.hadoop.net.CachedDNSToSwitchMapping.resolve(CachedDNSToSwitchMapping.java)
> at 
> org.apache.hadoop.ozone.om.KeyManagerImpl.resolveNodeLocation(KeyManagerImpl.java)
> at org.apache.hadoop.ozone.om.KeyManagerImpl.getOtherNode(KeyManagerImpl.java)
> at 
> org.apache.hadoop.ozone.om.KeyManagerImpl.getClientNode(KeyManagerImpl.java)
> at 
> org.apache.hadoop.ozone.om.KeyManagerImpl.sortDatanodes(KeyManagerImpl.java)
> ...
> {code}
>  
> h2. Root cause
> {\{ScriptBasedMapping}} extends Hadoop's \{{CachedDNSToSwitchMapping}}, so 
> successful resolutions *are*
> cached -- but failures are not. The raw mapping returns \{{null}} for the 
> whole batch when the script
> cannot be run (\{{ScriptBasedMapping.RawScriptBasedMapping.resolve}}), and 
> the caching layer skips nulls:
> {code:java}
> // CachedDNSToSwitchMapping
> private void cacheResolvedHosts(List<String> uncachedHosts, List<String> 
> resolvedHosts) {
> if (resolvedHosts != null) { // <-- a failure is never cached
> for (int i = 0; i < uncachedHosts.size(); i++) {
> cache.put(uncachedHosts.get(i), resolvedHosts.get(i));
> }
> }
> }
> {code}
> Ozone adds no negative caching on top, so every request repeats the fork. 
> Both services resolve the
> *client* host on the request thread:
> * OM read path -- \{{KeyManagerImpl.sortDatanodes}} -> \{{getClientNode}} -> 
> \{{getOtherNode}} ->
> {\{resolveNodeLocation}} (KeyManagerImpl.java:2311, 2382, 2388, 2404), 
> reached by \{{lookupKey}},
> {\{getKeyInfo}}, \{{lookupFile}}, \{{getFileStatus}} and \{{listStatus}}.
> * OM write path -- \{{OMKeyRequest}} -> 
> \{{KeyManagerImpl.sortDatanodesForWrite}} -> \{{getClientNode}}
> (OMKeyRequest.java:253, KeyManagerImpl.java:2323).
> * SCM -- \{{SCMBlockProtocolServer.getClientNode}} -> \{{getOtherNode}} ->
> {\{StorageContainerManager.resolveNodeLocation}} 
> (SCMBlockProtocolServer.java:412, 422, 424), from both
> the \{{SortDatanodes}} RPC (:387) and \{{allocateBlock}}'s per-block loop 
> (:212).
> Two things amplify the rate:
> # \{{getClientNode}} is invoked once per *distinct pipeline* within a single 
> request. The sorted-order memoisation in \{{KeyManagerImpl.sortDatanodes}} is 
> keyed by the pipeline's node set and does not cover client resolution, so one 
> \{{listStatus}} spanning M pipelines performs M resolutions for the same 
> client host.
>  
> # The OM only matches the client against the 3-5 nodes of the pipeline being 
> sorted. SCM, by contrast, consults \{{SCMNodeManager.getNodesByAddress}} 
> first, so a client running on a datanode host never reaches the script there. 
> OM has no such index, so co-located compute clients fall through to the 
> script on nearly every request.
>  
> * *Log flood.* ~45 lines per occurrence at ~1,500 occurrences/min is on the 
> order of 65,000 WARN lines and ~6.7 MB of OM log *per minute*. Real OM events 
> are effectively
> unfindable, and log retention is destroyed.
> * *A process fork per request, on the RPC handler thread.* 
> \{{ProcessBuilder.start}} plus
> {\{Shell.runCommand}} on an IPC handler is a handler-occupancy and CPU cost 
> paid on every read and every block allocation.
> * *Topology awareness silently stops working.* With the client unresolved,
> {\{NetworkTopologyImpl.sortByDistanceCost}} takes its \{{reader == null}} 
> branch and *shuffles* the datanode list, so reads get a random order rather 
> than a locality-ordered one. Writes keep pipeline order. Nothing reports that 
> locality has been lost.
> * *No actionable signal from Ozone.* Ozone's own handler never fires -- 
> \{{ScriptBasedMapping}} swallows
> the \{{IOException}} and returns \{{null}}, so \{{getOtherNode}}'s \{{catch}} 
> block is never entered ("Could not resolve client" appeared *zero* times in 
> the log). The only evidence is a third-party WARN that Ozone cannot throttle 
> or suppress.
> h2. Proposed fix
> Cache the *failure*. Add a short-lived negative cache in front of the 
> configured \{{DNSToSwitchMapping}}: when resolution yields no location for a 
> host, remember that for a configurable period and return no location for that 
> host without invoking the mapping again.
> Default *15 minutes*, configurable, with 0 disabling the behaviour. This 
> bounds the call rate to (distinct client hosts / expiry), which is 
> independent of request rate -- in the observed case roughly
> *one call per minute* instead of ~N, three orders of magnitude lower, while 
> still recovering
> automatically once the script is fixed, within one expiry period and without 
> an OM or SCM restart.
> Keep the entries *per host*, not global. The raw mapping returns \{{null}} 
> both for "the script is broken" and for "the script does not know this host", 
> and these calls are single-host, so the two cases are indistinguishable at 
> this layer; per-host entries bound the rate without letting one unknown host 
> suppress resolution for every other client.
> Both services should share the implementation: \{{createDNSToSwitchMapping}} 
> and \{{resolveNodeLocation}}
> are currently duplicated byte-for-byte between \{{StorageContainerManager}} 
> (lines 2351-2370) and
> {\{KeyManagerImpl}} (lines 383-389, 2404-2414). Folding both into one helper 
> in \{{hdds-server-framework}} removes the duplication and fixes OM and SCM 
> together.
> h2. Out of scope
> * Resolving the client host from the topology the OM already holds (an 
> address index equivalent to SCM's \{{getNodesByAddress}}). This would both 
> avoid the script *and* restore correct locality for co-located clients, but 
> only for clients that are datanodes -- a separate improvement.
> * Validating the configured script at service startup so a misconfiguration 
> surfaces once, loudly, instead of per request.
> * Resolving the client once per request instead of once per pipeline.
> h2. Acceptance criteria
> # With a mapping that fails, a service resolves a given client host at most 
> once per expiry period, and the WARN volume falls accordingly.
> # The failure is not cached when the expiry is configured to 0, preserving 
> today's behaviour.
> # Once resolution starts succeeding, locations are resolved again within one 
> expiry period with no restart.
> # Successful resolutions are unaffected -- they continue to be cached by the 
> existing mechanism.
> # OM and SCM share one code path for creating the mapping and resolving a 
> node location.
>  



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

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

Reply via email to