[
https://issues.apache.org/jira/browse/HDDS-16895?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Siyao Meng updated HDDS-16895:
------------------------------
Attachment: TestBugMC1RefreshSubtractsReportedContainers.java
> SCM can leave safe mode below the container threshold when a container report
> is processed during the container rule refresh
> ----------------------------------------------------------------------------------------------------------------------------
>
> Key: HDDS-16895
> URL: https://issues.apache.org/jira/browse/HDDS-16895
> Project: Apache Ozone
> Issue Type: Bug
> Reporter: Siyao Meng
> Priority: Minor
> Attachments:
> MC-1-count-only-containers-removed-by-deleted-scan.patch,
> TestBugMC1RefreshSubtractsReportedContainers.java
>
>
> h3. Mechanism
> {{AbstractContainerSafeModeRule.reinitializeRule}} removes DELETED containers
> from the rule's container map and then lowers {{totalContainers}} by the
> difference between the map size before and after the scan. The refresh holds
> the rule's lock, but report processing does not:
> {{SafeModeExitRule.onMessage}} calls {{process()}} between two {{validate()}}
> calls, and {{handleReportedContainer}} removes each reported container from
> the same map and increments {{containersWithMinReplicas}}. Every container
> that a report removes while the refresh is between its two size reads is
> therefore counted as reported and also subtracted from the total. The ratio
> can exceed 1.0, the rule validates, and once the other rules pass SCM leaves
> safe mode. The exit is not reversed.
> {{ECContainerSafeModeRule.handleReportedContainer}} has a second, smaller gap
> of the same kind: it increments the count without checking that its own
> {{remove()}} removed the container, so an EC container removed by the refresh
> between {{containsKey}} and {{remove}} is still counted (code reading, not
> run).
> h3. Trigger
> # SCM starts in safe mode. The {{safemode-refresh-thread}} calls
> {{refresh(false)}} on every rule every
> {{hdds.scm.safemode.rule.refresh.interval}} (default 5s) while the rule is
> not met.
> # A refresh starts while a datanode's registration container report is being
> processed.
> No fault and no operator action is needed. {{refresh()}} and {{validate()}}
> are synchronized on the rule, and {{onMessage}} calls {{validate()}} before
> {{process()}}, so a report that arrives during a refresh waits for it. Only
> the one report already inside {{process()}} when the refresh starts can
> overlap, and the chance of a hit per registration report is roughly that
> report's processing time divided by the refresh interval. The window itself
> is the DELETED lookup (a copy of the DELETED containers under the container
> state read lock) plus the removal loop: about 50ms per 100,000 DELETED
> records in a unit timing with a mocked {{ContainerManager}}. It decides how
> much of the report in progress fits inside the window.
> h3. Impact
> * SCM leaves safe mode with fewer containers reported than
> {{hdds.scm.safemode.threshold.pct}} (default 0.99). Each refresh can subtract
> at most the containers that the one report in progress when it starts removes
> during the refresh, because the next report waits for the rule lock in
> {{validate()}}. Successive refreshes add up. At the default 5s interval a hit
> is rare, and the expected shortfall is estimated at a few percent of the
> containers below the threshold (estimated from the code, not measured). A
> single hit on a large early report in a small cluster can cost more, up to
> that report's share of the containers. The other rules, mainly the 90% one
> replica pipeline rule, still need most datanodes registered before SCM can
> exit.
> * On exit, block allocation and {{allocateContainer}} stop returning
> {{SAFE_MODE_EXCEPTION}}, and {{getContainerWithPipeline}} stops refusing open
> containers without enough replicas. ReplicationManager, block deletion and
> the pipeline scrubber start after {{hdds.scm.wait.time.after.safemode.exit}}
> (default 5m). The background pipeline creator already runs in safe mode once
> the pre check passes ({{hdds.scm.safemode.pipeline.creation}}, default true),
> so the exit does not change it. Containers whose replicas have still not been
> reported by then are handled as missing or under replicated (argued from the
> code, not run). No data loss is expected.
> * The subtraction was added by HDDS-15238 and is in 2.2.0 and 2.2.1. There
> the refresh runs from {{SCMStateMachine.applyTransaction}} while in safe mode
> instead of from the periodic thread, so the same race should be reachable
> (argued, not run on 2.2.x).
> h3. Reproduction
> [^TestBugMC1RefreshSubtractsReportedContainers.java]
> ({{hadoop-hdds/server-scm}}) uses the real {{SCMSafeModeManager}}, its
> refresh thread (interval lowered to 100ms) and the EventQueue handler of
> {{RatisContainerSafeModeRule}}, with a mocked {{ContainerManager}} holding 3
> closed containers. One datanode reports 2 of them. Two latches only order the
> threads: the handler waits just before {{process()}} until the refresh is
> inside the DELETED lookup, and the refresh waits there until the report is
> processed. On the unmodified commit it fails every time with {{SCM left safe
> mode with 2 of 3 containers reported}}, and the log shows {{Refreshed RATIS
> Containers threshold count to 1.}} followed by {{SCM exiting safe mode.}}
> With the patch it passes.
> Without any ordering, a stress run (20,000 closed containers, 200,000 DELETED
> containers in the mock, one report of 19,700 containers, i.e. 98.5%, fired at
> a random offset to a 100ms refresh) left safe mode in 6 of 20 trials on the
> unmodified commit and in 0 of 20 with the patch. In the failing trials the
> total fell to as little as 5,132 of 20,000. That size comes from the stress
> settings: a 100ms interval and a DELETED lookup of about 100ms (200,000
> records) against one report of 19,700 containers, so a large part of that
> single report fell inside refresh windows. It is not a typical shortfall. The
> stress harness is not attached. Not run at the default 5s interval or on a
> real cluster.
> h3. Patch
> [^MC-1-count-only-containers-removed-by-deleted-scan.patch], against
> b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. It also applies to master at
> 6818707698c.
> {{reinitializeRule}} now subtracts only the containers that its own
> {{containers.remove()}} removed (non null return) instead of the change in
> map size. Each container is then removed exactly once, either by a report
> (counted, kept in the total) or by the DELETED scan (subtracted, not
> counted). {{ECContainerSafeModeRule.handleReportedContainer}} increments only
> when its {{remove()}} returned a value, as the Ratis rule already does.
> Report processing stays outside the rule lock. Making {{process()}}
> synchronized would also close the race, but it would hold the lock for a
> whole registration report and block {{validate()}}, the safe mode status call
> and the refresh meanwhile.
> Covered by {{testRefreshKeepsContainersReportedDuringRefresh}} in the
> existing {{AbstractContainerSafeModeRuleTest}}, which runs for the Ratis and
> the EC rule. A report is processed inside the mocked DELETED lookup, and the
> test expects only the DELETED container to leave the total. Without the
> change it fails for both rules ({{expected: <2> but was: <1>}}). With it, the
> {{org.apache.hadoop.hdds.scm.safemode}} unit tests (76) and
> {{TestSCMSafeModeWithPipelineRules}} on a mini cluster (2) pass, and
> checkstyle is clean.
> Found by TLA+ model checking and code review of the SCM safe mode rules under
> HDDS-15926, on commit b0aa6475b7856d2c7f467dd3db12b9a1dd116f63. Checked
> against HDDS issues and apache/ozone pull requests for duplicates before
> filing. 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]