Siyao Meng created HDDS-16810:
---------------------------------
Summary: Reconciliation raises the container BCSID to a block
BCSID taken from a peer whose own container BCSID is lower
Key: HDDS-16810
URL: https://issues.apache.org/jira/browse/HDDS-16810
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments:
MC-1-cap-container-bcsid-advance-at-peer-container-bcsid.patch,
TestBugMC1ContainerBcsIdFromIncompletePeer.java
h3. Mechanism
After each peer round {{KeyValueHandler.reconcileContainerInternal}} calls
{{advanceContainerBcsIdForFullyCoveredRound}}. When every block repaired in the
round ended complete and the diff dropped no chunk as unhealthy, the container
BCSID is raised to the highest block BCSID adopted in the round. The Javadoc of
the method reasons that such a round inherits the peer's own container level
claim, but the peer's container BCSID is never read, and a block BCSID on the
peer says nothing about the peer's other blocks:
* A peer that was itself only partially repaired holds complete blocks above
its own container BCSID. It correctly did not raise its container BCSID, but it
serves those blocks.
* A block that both replicas hold in the same incomplete form is not in the
diff, so the round counts as clean.
A replica that reconciles with such a peer ends with a container BCSID that
covers a committed chunk it does not hold. Nothing on the datanode corrects
this afterwards: the scanner checks a block against its own metadata, which
lists only the chunks the replica has (run).
The logic came with HDDS-15542 and is on master only. Releases 2.1 and 2.2
raise the container BCSID after every fully pulled block, which HDDS-15542
replaced (read from the release code, not run). HDDS-16011 states the rule that
is broken: a replica that advertises BCSID N holds every chunk committed
through N.
h3. Trigger
In the reproduced schedules the container is QUASI_CLOSED in SCM and its
replicas are QUASI_CLOSED or UNHEALTHY, and reconciliation is started with
{{ozone admin container reconcile}}. Layout of the reproduction: two blocks of
two chunks, block 0 committed at BCSID 1 and 2, block 1 at BCSID 3 and 4. d1
applied every commit.
Schedule A, no RPC failure:
# d1 has a corrupt second chunk in block 0, its scanner has found it and marked
d1 UNHEALTHY. d2 and d3 are QUASI_CLOSED and empty.
# d2 reconciles. It gets block 1 and the first chunk of block 0, and keeps
container BCSID 0.
# d3 reconciles and its first peer is d2. The round is clean, d3 adopts block 1
at BCSID 4 and raises its container BCSID to 4. The round with d1 cannot add
the corrupt chunk.
SCM sends the peers as a set, so their order is not controlled. SCM also sends
the command to every replica at the same time, so whether d2 has finished its
round with d1 before d3 starts its round with d2 depends on which datanode runs
its command first. With d1 first in d3's list, d3 stays at 0 (the control in
the reproduction).
Schedule B, found by model checking, all three replicas QUASI_CLOSED and d1
healthy:
# d2 is empty, d3 applied only the first commit.
# d2 reconciles and its GetBlock for block 0 to d1 fails. d2 gets block 1 from
d1 and the short block 0 from d3, and ends at container BCSID 1.
# d3 reconciles while d1 is unreachable. It gets block 1 from d2, the round is
clean, and its container BCSID goes from 1 to 4.
h3. Impact
* d3 is QUASI_CLOSED with container BCSID 4, the same as d1, and holds one of
the two chunks of block 0 (run, both schedules).
* SCM treats the two as equal. {{QuasiClosedContainerHandler}} force closes
every QUASI_CLOSED replica with the highest sequence ID, and the datanode
accepts the close for d3 (the datanode side is run in schedule B, the SCM
decision is read from code). {{RatisUnderReplicationHandler}} keeps only
replicas with the highest sequence ID as sources, so d3 can be the copy source
when d2 is replaced (read from code, not run). The same value on a QUASI_CLOSED
replica of a CLOSED container makes {{MismatchedReplicasHandler}} force close
it (read from code, not run).
* In schedule A the chunk is already damaged on d1, the only replica that had
it. What the defect changes there is visibility:
{{QuasiClosedContainerHandler}} keeps a container QUASI_CLOSED while an
UNHEALTHY replica has a higher sequence ID than every QUASI_CLOSED one, and d3
at BCSID 4 lifts that hold (read from code, not run).
* A client read of block 0 at its committed BCSID is still rejected on d3 by
the block level check in {{BlockManagerImpl.getBlock}}, so the client fails
over (read from code, not run). The container level check is the one that no
longer protects.
* A later reconcile of d3 against a complete healthy replica would repair the
block (read from code, not run), but only an administrator starts one.
h3. Reproduction
PASS, deterministic, unmodified source.
[^TestBugMC1ContainerBcsIdFromIncompletePeer.java]
({{hadoop-hdds/container-service}}) uses the setup of
{{TestContainerReconciliationWithMockDatanodes}}: three datanodes, each a real
{{KeyValueHandler}} with a real container on disk and a real on demand scanner,
the real {{BlockInputStream}}, and only the static {{ContainerProtocolCalls}}
transport routed to the peer, which answers the request for its checksum info
through its own {{KeyValueHandler}}. There is no SCM. Each replica is created
as the prefix of the commit log that it applied and is then quasi closed.
Schedule B uses two switches in that routing (one GetBlock fails, one datanode
is unreachable). The tests assert the data and the BCSID of d2 and d3 after
each step, the control order for schedule A, and for schedule B that
{{closeContainer}} leaves d3 CLOSED at BCSID 4 with the chunk missing. A
passing test means the defect is present.
h3. Patch
[^MC-1-cap-container-bcsid-advance-at-peer-container-bcsid.patch], against
1e528b97aed3eef1be6846f1f0767c1ef98a883a. It also applies to master at
8ca44914771.
{{GetContainerChecksumInfoResponseProto}} gets an optional field with the
container BCSID of the replica that the checksum info comes from. The serving
datanode reads it before it reads the tree. The reconciling datanode keeps the
conditions from HDDS-15542 for a clean round and raises its container BCSID to
the highest adopted block BCSID as before, but never above the value the peer
reported. The field is on a datanode to datanode read response, nothing is
persisted, and the proto compatibility check of the build passes without a
change to {{proto.lock}}. The same field is the input for the rule proposed in
HDDS-16011, raising the container BCSID to the higher of the two when the trees
are equal.
A peer that does not send the field is handled as today. During a rolling
upgrade a round against a datanode without the patch can therefore still raise
the BCSID too far, and the defect is closed once both datanodes run the patch.
The other choice, never raising against such a peer, would leave replicas that
were repaired completely below the sequence ID of the container for good,
because a later reconcile finds an empty diff and adopts nothing.
Limits:
* When a partially repaired peer precedes a complete one in the list, the
replica ends complete but with the BCSID of the blocks adopted from the
complete peer only, which can be lower than that peer's container BCSID. Such a
replica rejects reads of the blocks above its container BCSID, is not force
closed, and after the container closes it is replaced and deleted as a stale
copy (read from code; the resulting BCSID is run). A later reconcile does not
raise it because the diff is then empty. HDDS-16011 already describes this
case, a replica with complete content that claims a lower BCSID, and today's
code already produces it for a replica whose own pull failed once (run).
Adopting the peer's BCSID when the trees are equal is left to HDDS-16011.
* If the only replica at the full BCSID is UNHEALTHY, that limit can leave
every other replica complete and below it, and the container then stays
QUASI_CLOSED (the hold in SCM is read from code). This needs no failed call. It
needs a peer that holds blocks above its own container BCSID, which is also
what the defect needs. Run at the datanodes: d1 is UNHEALTHY at 4 with a
corrupt second chunk in block 0, d3 is at 2 and holds block 0 intact, d2 is
empty. d2 reconciles first and ends complete at 2, with block 1 at BCSID 4. d3
then reconciles with d2 before d1 and ends complete at 2, and further
reconciles of all three change no BCSID (d1 repairs its chunk and stays
UNHEALTHY at 4). Without the patch d3 reaches 4 there, through the round that
is the defect described here, and SCM can then close the container on d3 (read
from code). With the patch the container stays QUASI_CLOSED until a replica can
adopt the container BCSID of a peer whose tree equals its own, as HDDS-16011
proposes. When d3 reconciles before d2 has pulled block 1, it reaches 4 from d1
with and without the patch.
* The patch only ever lowers the value compared with today, so it adds no way
to claim too much. It does not close the case where the peer's tree is older
than the BCSID sent with it: a reconciling peer raises its container BCSID
before it writes its tree (argued, not run). HDDS-12986 is the open issue for a
new BCSID paired with an older tree.
Covered in the existing {{TestContainerReconciliationWithMockDatanodes}} by
{{testContainerBcsIdNotAdoptedAbovePeerContainerBcsId}} (a complete peer, whose
BCSID is still adopted, and schedule A),
{{testContainerBcsIdNotAdoptedAfterPeerFailureAndOutage}} (schedule B) and
{{testContainerBcsIdAdoptedFromPeerThatDoesNotReportContainerBcsId}}. The first
two use the layout of that suite, not that of the reproduction: replicas that
start CLOSED, and three blocks that are each committed once, at BCSID 0, 1 and
2. In schedule A the corrupt chunk is the first one of block 0, so the
partially repaired peer holds none of block 0. The mock datanodes of that class
now answer the checksum info request through the real handler. With the cap
taken out and the rest of the patch in place, two of the four invocations fail,
the schedule A case of the first test and the second test, because the
reconciling replica reports BCSID 2 while its peer is at 0. With the patch the
unit suites of the {{keyvalue}}, {{ozoneimpl}} and {{checksum}} packages in
{{hadoop-hdds/container-service}} pass (1127 tests, 18 skipped by the suites),
checkstyle is clean on the three touched modules, and both schedules of the
reproduction fail because d3 keeps its own container BCSID. Not run on a mini
cluster, and not run against a datanode of an older version.
Found by TLA+ model checking and code review of the container reconciliation
paths in the datanode and SCM under HDDS-15926, on commit
1e528b97aed3eef1be6846f1f0767c1ef98a883a. 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]