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

Siyao Meng updated HDDS-17269:
------------------------------
    Attachment: TestBugMC11HeartbeatAfterReregister.java

> Datanode heartbeat queued behind a slow one runs after SCM asks to reregister 
> and drops pending container actions
> -----------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17269
>                 URL: https://issues.apache.org/jira/browse/HDDS-17269
>             Project: Apache Ozone
>          Issue Type: Bug
>    Affects Versions: 0.5.0, 1.0.0, 1.1.0, 1.2.0, 1.3.0, 1.2.1, 1.4.0, 2.0.0, 
> 1.4.1, 2.1.0, 2.2.0, 2.1.1, 2.1.2, 2.2.1
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: 
> MC-6-wait-for-running-endpoint-task-instead-of-queuing.patch, 
> TestBugCR1StaleEndpointBacklog.java, 
> TestBugMC11HeartbeatAfterReregister.java, TestBugMC6EndpointTaskBacklog.java
>
>
> h3. Mechanism
> {{RunningDatanodeState.execute}} builds one task per endpoint on every 
> heartbeat cycle, from the endpoint's state at that moment, and submits it to 
> the endpoint's single thread executor (created in {{EndpointStateMachine}}, 
> unbounded queue). It waits for the task for at most one heartbeat interval, 
> does not cancel it when that wait times out, and does not check whether the 
> endpoint's previous task is still running. A queued task runs later for the 
> endpoint state and the endpoint object it was built for. 
> {{VersionEndpointTask}} and {{RegisterEndpointTask}} check the endpoint state 
> under the endpoint lock and do nothing if it has moved on. 
> {{HeartbeatEndpointTask.call}} has no such check: it drains the incremental 
> reports and container actions queued for that SCM address and sends them 
> (close pipeline actions are only read: {{PipelineActionMap.getActions}} keeps 
> them queued until the pipeline is gone). If the call fails with an 
> {{IOException}}, only incremental container reports and command status 
> reports are put back.
> A cycle starts while the previous task is still running in two ways. The 
> heartbeat RPC takes longer than {{hdds.heartbeat.interval}} (with the 
> defaults, a call to an SCM that accepts the connection but does not answer 
> takes about 95s by calculation: 16 attempts of the 5s 
> {{ozone.scm.heartbeat.rpc-timeout}} and 15 retry sleeps of 1s). Or 
> {{DatanodeStateMachine.triggerHeartbeat}} (immediate ICR, close container 
> action, pipeline close) interrupts the state machine thread while a heartbeat 
> is in flight, and the next cycle starts at once.
> This shows up in three ways:
> # *Backlog and burst.* While an SCM is reachable but answers more slowly than 
> the heartbeat interval, the endpoint queue grows by one task per heartbeat 
> cycle (every interval, and on every immediate heartbeat trigger) for as long 
> as that lasts, and the queue is replayed back to back as soon as the SCM 
> answers again.
> # *Heartbeat after a reregister command.* If the heartbeat in flight gets a 
> reregister command (the SCM restarted or failed over and lost the 
> registration), {{HeartbeatEndpointTask.processReregisterCommand}} moves the 
> endpoint to GETVERSION. The queued heartbeat task then runs in GETVERSION, 
> drains what was raised in the meantime and sends it to an SCM that does not 
> consider the datanode registered. {{SCMDatanodeHeartbeatDispatcher}} answers 
> with another reregister command and processes none of the reports or actions.
> # *Old endpoint after a DNS refresh.* This path was added by HDDS-15533 and 
> is not released yet. With {{ozone.client.failover.resolve-needed=true}} 
> (default false), a heartbeat that fails with a connection class error after 
> {{hdds.heartbeat.address.refresh.missed-count-threshold}} misses calls 
> {{SCMConnectionManager.refreshSCMServer}}. That replaces the endpoint under 
> the same {{HostAndPort}} key and calls {{EndpointStateMachine.close}} on the 
> old one, which uses {{ExecutorService.shutdown()}}. Tasks already queued on 
> the old endpoint still run: they heartbeat to the old address and drain the 
> queues that the rebuilt endpoint shares under the same {{HostAndPort}}.
> h3. Trigger
> * Backlog: an SCM that stays reachable but answers heartbeats more slowly 
> than the heartbeat interval, for example with saturated handlers or a long GC 
> pause.
> * Heartbeat after a reregister command: an SCM restart or failover while a 
> heartbeat to that SCM is in flight and the next cycle starts before it 
> returns. Either the first heartbeat after the restart takes longer than the 
> interval, or an immediate heartbeat trigger lands while its RPC is in flight.
> * Old endpoint (HDDS-15533, not yet released): DNS refresh enabled, the SCM 
> moves to a new IP, and the old IP still accepts connections but does not 
> answer (when the old IP refuses connections the IPC client re-resolves the 
> host by itself and the refresh path is not reached). Heartbeats queued during 
> the misses before the refresh form the old endpoint's backlog.
> h3. Impact
> * The SCM receives the queued heartbeats as a burst after it recovers. Each 
> one is built when it runs, so it carries current data, and SCM's heartbeat 
> handling is idempotent, so on its own this is extra load on SCM and one small 
> queued task per interval in the datanode heap.
> * After a reregister command, the reports and actions drained by the queued 
> heartbeat are gone from the datanode. Shown for a close container action and 
> an incremental container report. The replica state in the lost ICR is also in 
> the full container report sent at re-registration. A lost close container 
> action is added again by {{HddsDispatcher.sendCloseContainerActionIfNeeded}} 
> on the next write to that container, so it only stays lost if no write 
> reaches the container afterwards; then the container stays OPEN in SCM until 
> its pipeline is closed. That last step is argued from the code, not run here.
> * After a DNS refresh, a container action drained by the old endpoint's 
> backlog is lost the same way when the call to the old address fails (shown). 
> Incremental reports drained there are put back, but only after each such call 
> fails (about 95s each with the defaults). The old endpoint also logs "Unable 
> to communicate to SCM server at <host>:<port>/<new IP>" because it shares the 
> updated {{HostAndPort}} (code reading).
> h3. Reproduction
> Three self contained classes for {{hadoop-hdds/container-service}} (target 
> path and Maven command in each class comment). Each one runs heartbeat cycles 
> through {{StateContext.execute}}, the call the datanode state machine thread 
> makes every interval, with the real {{RunningDatanodeState}}, 
> {{EndpointStateMachine}} and {{HeartbeatEndpointTask}}. Only the SCM RPC 
> proxy is a stub; it holds the first heartbeat until the test releases it, 
> which stands for a slow call. The third class also runs the real 
> {{SCMConnectionManager.refreshSCMServer}} and {{EndpointStateMachine.close}}, 
> with the DNS answer stubbed ({{HostAndPort.resolveLatest}}). They pass on 
> unmodified source when the bug is present.
> * [^TestBugMC6EndpointTaskBacklog.java]: 10 cycles while the SCM holds the 
> first heartbeat. Output without the fix:
> {noformat}Heartbeat cycles while SCM was slow: 10, heartbeats SCM received 
> right after it answered again: 10{noformat}
> * [^TestBugMC11HeartbeatAfterReregister.java]: the SCM answers every 
> heartbeat with a reregister command; a close container action and an ICR are 
> raised while the first heartbeat is in flight and a second cycle starts. 
> Output without the fix:
> {noformat}Heartbeats sent: [endpoint HEARTBEAT, container actions 0, ICRs 0, 
> endpoint GETVERSION, container actions 1, ICRs 1]; endpoint now GETVERSION; 
> container actions still queued for SCM: 0, ICRs: 0{noformat}
> * [^TestBugCR1StaleEndpointBacklog.java]: refresh threshold 1, the old 
> address holds the first heartbeat and then times out, a close container 
> action is raised right after the rebuild. Output without the fix:
> {noformat}Endpoint rebuilt: true, now dialing /10.0.0.7:9861; container 
> actions in each heartbeat to the old address: [0, 1, 0]; actions still queued 
> for the rebuilt endpoint: 0{noformat}
> The full chain on a real cluster (SCM restart under load, then a container 
> that stays OPEN) was not run.
> h3. Patch
> [^MC-6-wait-for-running-endpoint-task-instead-of-queuing.patch], against 
> b10a7d427bdf83807a7e48a1be507d3b02d5a503. It also applies to master at 
> e0de543cbe9.
> {{RunningDatanodeState}} remembers the last task it submitted to each 
> endpoint. If that task is still running when the next cycle starts, the cycle 
> waits for it (with the same bounded wait as before) instead of building and 
> queuing another one. If it finishes during that cycle, the next cycle starts 
> right away, so a heartbeat triggered while it was running is sent as soon as 
> it returns, as before (this also happens when nothing was triggered, which 
> costs at most one early heartbeat after a slow call). Every task is therefore 
> built after the previous one finished, from the current endpoint state and 
> the current endpoint list, which removes all three symptoms with one change: 
> there is no backlog, no heartbeat built before a reregister command runs 
> after it, and the replaced endpoint gets no new task after the refresh. The 
> task is now submitted to the endpoint executor from the state machine thread; 
> an endpoint closed between listing and submitting is skipped. Nothing changes 
> for an SCM that answers within the interval.
> HDDS-17267 puts container actions back when a heartbeat fails or is answered 
> with a reregister command, which also covers the first heartbeat sent 
> normally after an SCM restart; this patch stops queued heartbeats from 
> draining the queues at all, so the two changes are independent, touch 
> different files and apply together.
> Alternatives considered: a state check in {{HeartbeatEndpointTask.call}} 
> fixes only the second symptom, and {{shutdownNow()}} in 
> {{EndpointStateMachine.close}} only the third (it would also interrupt an in 
> flight call on SCM removal). Neither bounds the queue.
> Covered by three cases in the existing {{TestRunningDatanodeState}}: 
> {{testEndpointTaskIsNotQueuedBehindRunningOne}}, 
> {{testTaskAfterReregisterIsBuiltForNewEndpointState}} and 
> {{testReplacedEndpointRunsNoQueuedTask}}, plus 
> {{testClosedEndpointIsSkipped}} for the closed endpoint path (it passes 
> without the change too, where the same race only logged an error). Without 
> the change they fail ("Wanted 1 time ... But was 4 times" for 
> {{sendHeartbeat}}, and the close container action missing from the queue in 
> the other two). With it they pass, together with the existing tests of 
> {{TestRunningDatanodeState}}, {{TestDatanodeStateMachine}}, 
> {{TestHeartbeatEndpointTask}}, {{TestHeartbeatEndpointTaskDnsRefresh}}, 
> {{TestSCMConnectionManager}}, {{TestStateContext}}, the command handler 
> tests, {{TestEndPoint}} and the mini cluster suite 
> {{TestDatanodeSCMNodesReconfiguration}} (SCM nodes added and removed at 
> runtime). Checkstyle is clean. With the change the three attached 
> reproductions fail at their bug assertions: the SCM gets 1 heartbeat instead 
> of 10, and in the other two only the first heartbeat is sent and the close 
> container action is still queued.
> Found by TLA+ model checking and code review of the datanode registration and 
> heartbeat paths under HDDS-15926, on commit 
> b10a7d427bdf83807a7e48a1be507d3b02d5a503. Checked against HDDS issues and 
> apache/ozone pull requests for duplicates before filing. Related but not 
> duplicates: HDDS-4386 (introduced the per endpoint executor to stop thread 
> growth with a slow SCM), HDDS-13126 (datanode down after an endpoint task 
> timeout), HDDS-6634 (reregister goes to GETVERSION), HDDS-15533 (the DNS 
> refresh on heartbeat failure) and HDDS-16723 (oversized heartbeat from queued 
> ICRs on re-registration). The attached patch is a proposal for review. 
> Generated with Specula (Claude Opus 5.5).



--
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