Siyao Meng created HDDS-17425:
---------------------------------
Summary: Datanode on older software that registers while SCM
finalizes stays HEALTHY on the finalized SCM and is never asked to register
again
Key: HDDS-17425
URL: https://issues.apache.org/jira/browse/HDDS-17425
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments: MC-1-reregister-older-datanode-after-finalize.patch,
TestBugMC1OlderDatanodeRegistersDuringFinalize.java
h3. Mechanism
{{SCMBlockProtocolServer.finalizeUpgrade}} checks that every HEALTHY datanode
runs SCM's software version ({{validateDatanodeVersionsBeforeFinalize}}) and
then submits the finalization through Ratis. Applying it
({{FinalizationStateManagerImpl.finalizeUpgrade}}) finalizes
{{ScmVersionManager}} and does not re-check registered datanodes.
{{SCMNodeManager.register}} runs {{shouldFenceDatanode}} once and shares no
lock with that path, and {{shouldFenceDatanode}} rejects older software only
once SCM is finalized. So a datanode on older software whose registration is
handled after the check and before the apply is accepted.
After the apply nothing checks a HEALTHY node again. On heartbeat,
{{SCMNodeManager.sendFinalizeToDatanodeIfNeeded}} finds the same fence
condition but only logs "Invalid datanode in the cluster" and returns,
{{SCMDatanodeHeartbeatDispatcher}} asks only unknown nodes to register again,
and the health check does not look at versions. The datanode stays HEALTHY and
IN_SERVICE, and eligible for pipelines, until it restarts.
This is the HEALTHY counterpart of HDDS-16798 (a STALE or DEAD older datanode
that heartbeats back after finalization). The fix for that issue in
apache/ozone#11692 re-checks only nodes that are not HEALTHY, so it does not
cover this path.
h3. Trigger
# A rolling upgrade from a release that already reports datanode versions
({{DatanodeVersionProto}}). Releases that do not report them are rejected at
registration, so this first applies to upgrades from the first release with
this code.
# OM finalizes SCM. The version check passes.
# A datanode still on the older release starts and its registration reaches the
SCM before that SCM has applied the finalization. In SCM HA it has to reach
every SCM before each applies it, because an SCM that has applied it rejects
the registration and the datanode shuts down. On the leader the window is the
Ratis round trip of the finalize request.
In SCM HA the same state is reachable without a timing window (read from code,
not run). The check reads only the leader's node set and skips nodes that are
not HEALTHY, so an older datanode that is STALE on the leader but HEALTHY on a
follower (cut off from the leader only) passes it and stays HEALTHY on that
follower after the follower applies the finalization; if that follower becomes
leader, the node is HEALTHY on the leader, where #11692 does not re-check it.
Even with #11692, a STALE older datanode whose heartbeat arrives inside the
window becomes HEALTHY without registering, because SCM is still not finalized
when it checks that heartbeat.
h3. Impact
* A datanode that the finalized SCM would reject at registration stays HEALTHY
and IN_SERVICE and stays in the set that pipeline placement chooses from, with
two ERROR log lines on every heartbeat, until it restarts. No data loss or
write failure was shown or is expected from this alone.
* {{queryUpgradeStatus}} then reports HDDS FINALIZED with the older datanode
counted as finalized, which is tracked in HDDS-16799.
h3. Reproduction
PASS, deterministic, unmodified source.
[^TestBugMC1OlderDatanodeRegistersDuringFinalize.java]
({{hadoop-hdds/server-scm}}) uses the real
{{SCMBlockProtocolServer.finalizeUpgrade}}, {{SCMNodeManager}} with its health
check thread, {{SCMDatanodeHeartbeatDispatcher}} and a real
{{ScmVersionManager}} on a VERSION file at {{ZDU}}. The older datanode runs and
reports {{ZDU}} (100), SCM and the upgraded datanode run
{{STREAM_PUTBLOCK_WITHOUT_RAFT}} (101). The {{FinalizationManager}} is replaced
by a stand in for the Ratis round trip that lets the older datanode's
registration complete on another thread and then runs
{{ScmVersionManager.finalizeUpgrade}}. Output on the unmodified code: the
registration during finalize returns success, the heartbeat after finalize logs
"Invalid datanode in the cluster ... Datanode software version = ZDU (100)" and
its reply carries no command, the older datanode is IN_SERVICE HEALTHY and in
the healthy in service node set, {{queryUpgradeStatus}} reports scm=FINALIZED
hdds=FINALIZED with 2 of 2 datanodes finalized, and a registration of a
datanode on the same version after finalize is rejected. A passing test means
the defect is present. Only the SCM side was run; the datanode reaction to the
rejection is read from code. The follower route above was not run.
h3. Behaviour change after a forced finalize
{{ozone admin upgrade finalize --force}} skips the datanode version check.
Today the older datanodes that are HEALTHY at a forced finalize keep serving
until they restart. With the patch every one of them is told to register again
on its next heartbeat (30s by default), is rejected and shuts down, all at
about the same time (read from code, the datanode side was not run). Containers
whose replicas are all on older datanodes are unavailable until they are
re-replicated after those nodes are declared dead, and an EC group that loses
more datanodes than its parity count is unavailable as well. This follows the
upgrade rule that all components run the new software when finalization is
given, but {{--force}} exists to bypass that check, so the upgrade owners need
to decide whether a forced finalize should remove older datanodes right away.
SCM does not record whether a finalization was forced, so the patch cannot
treat the two cases differently without new replicated state.
h3. Suggested fix
The attached patch adds the re-register on the heartbeat side of the fence for
HEALTHY nodes and leaves STALE and DEAD nodes to #11692. An alternative that
covers both in one place is a single check in
{{SCMDatanodeHeartbeatDispatcher}} for a node of any health, on the software
version rules only (older software on a finalized SCM, or newer software than
SCM), which also skips {{processHeartbeat}} for the node that is sent away. It
must not include the apparent version rule, which an SCM that has not applied
the finalization yet sees from a correctly finalized datanode (HDDS-17090).
h3. Patch
[^MC-1-reregister-older-datanode-after-finalize.patch], against
679c9ccbee740a0af3ca9f76fc2d4bc6586049eb. It also applies to master at
69d77beff60 and on top of apache/ozone#11692.
When the heartbeat side of the fence finds a datanode on older software than
SCM and SCM has applied the finalization, {{sendFinalizeToDatanodeIfNeeded}}
now queues a {{ReregisterCommand}}, which the same heartbeat reply carries. The
datanode registers again and {{register}} rejects it with
{{errorNodeNotPermitted}}, as it already does for an older datanode that
registers after finalization. The rule is extracted into one method that
{{shouldFenceDatanode}} also uses.
The re-register is limited to that rule on purpose. The other rule that fences
on heartbeat, a datanode apparent version above SCM's, is what an SCM that has
not applied the finalization yet sees from a datanode the leader has already
finalized (HDDS-17090), so re-registering on it would shut down correctly
finalized datanodes. The older software rule holds only on an SCM that has
applied the finalization, and a datanode's software version changes only with a
restart. With #11692 applied, STALE and DEAD nodes are handled in the
dispatcher before this code runs, so the two changes act on disjoint nodes.
Recon is not affected, it overrides {{sendFinalizeToDatanodeIfNeeded}}.
Covered by {{testOlderDatanodeRegisteredBeforeFinalizeIsAskedToReregister}} in
the existing {{TestSCMNodeManager}}: a datanode on {{ZDU}} that registered
while SCM was not finalized gets {{reregisterCommand}} on its heartbeat after
finalization, and an upgraded datanode gets nothing.
{{testProcessVersionReportHigherApparentVersion}} now also asserts that a
datanode whose apparent version is above SCM's gets no command, and its
comments are narrowed to that case. Without the change the new test fails
because the reply is empty. With it {{TestSCMNodeManager}},
{{TestSCMDatanodeHeartbeatDispatcher}}, {{TestSCMBlockProtocolServer}},
{{TestNodeStateManager}} and {{TestDeadNodeHandler}} (109 tests) pass, and on a
mini cluster with only this patch applied {{TestScmHAFinalization}} (including
{{testFinalizedDatanodesShutDownWithPrefinalizedScm}}) and
{{TestOMUpgradeFinalization}} pass. Checkstyle is clean. With the patch the
reproduction fails at the assertion on the heartbeat reply, which then carries
the re-register command. With the patch, an SCM that has not applied the
finalization still sends no command to a finalized datanode that is STALE on it
(run on the SCM side).
Found by TLA+ model checking and code review of the SCM and datanode upgrade
finalization paths under HDDS-15926, on commit
679c9ccbee740a0af3ca9f76fc2d4bc6586049eb. 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
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]