Siyao Meng created HDDS-16807:
---------------------------------
Summary: DiskBalancer can move a container that is being
reconciled, the repairs are lost and SCM gets a report from the retired replica
Key: HDDS-16807
URL: https://issues.apache.org/jira/browse/HDDS-16807
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments:
MC-4-skip-diskbalancer-move-of-container-being-reconciled.patch,
TestBugMC4ReconcileDiskBalancerMove.java
h3. Mechanism
{{ContainerController.reconcileContainer}} looks the container up once.
{{KeyValueHandler.reconcileContainerInternal}} then repairs that object,
without holding the container lock, and ends with an incremental container
report (ICR) built from it. {{DiskBalancerService.DiskBalancerTask}} copies a
container to another volume under the container read lock, which does not
exclude those writes, replaces the entry in the container set
({{ContainerSet.updateContainer}}) and marks the source replica DELETED.
Neither side knows about the other: {{DefaultContainerChoosingPolicy}} does not
skip a container that is being reconciled, and the states DiskBalancer moves
(CLOSED, QUASI_CLOSED) are the ones reconciliation works on. A repair that is
written after the copy has read that part of the replica exists only in the
source replica, which is deleted.
HDDS-15066 handled the replaced container object for delete, mark unhealthy,
close and block deletion with {{ContainerSet.getContainerWithWriteLock}}.
Reconciliation was not covered.
h3. Trigger
# DiskBalancer is running on a datanode (an admin has started it).
# An admin runs {{ozone admin container reconcile}} for a container that has a
replica on that datanode.
# DiskBalancer picks that container while the reconciliation is running, or the
reconciliation starts while the move is copying.
The outcome depends on the order:
* The reconciliation ends after the move has copied the data and before the
move replaces the replica: the closing ICR reports the repaired replica, which
is then replaced by the copy from before the repair (run). This order has the
widest window, from the moment the copy has read the repaired part until the
replica is replaced, which is the rest of the copy and the import (read from
the code, not measured).
* The move completes before the reconciliation writes: every repair fails with
"is not in closed state, Container state is DELETED" and the closing ICR
reports state DELETED for a container whose live replica is CLOSED (run).
* The reconciliation ends in the few statements between
{{ContainerSet.updateContainer}} and the source being marked DELETED: the
remaining repairs go to the old replica and the closing ICR is built from it
(run, the test holds the move at this point). Without that hold the source is
marked DELETED right away and the writes fail as in the second order (read from
the code).
h3. Impact
* The repair is lost and nothing repeats it, reconciliation runs only on an
admin request. In the first order (and the narrow third) the datanode logs
"Completed reconciliation ... 1 blocks were updated. Data checksum updated"
although the replica that remains is not repaired. In the second order it logs
the failed chunk writes. The replica is left as it was before the command, so
no data is lost relative to that.
* SCM receives a report that does not describe the live replica. In the first
order it is the repaired BCSID and data checksum, and no report follows the
move (run, datanode side only, the run has no SCM), so SCM would keep that view
until the next full container report ({{hdds.container.report.interval}},
default 60m; read from the code, not run).
* In the other two orders the scan that the reconciliation requests at its end
runs on the live replica and sends a correct ICR when that scan completes (run
with a 12 KB container; the on demand scanner is throttled to 5 MB/s per volume
by default, {{hdds.container.scrub.on.demand.volume.bytes.per.second}}). For
state DELETED, SCM drops the replica from its view until the next report for
it, so the container can look under replicated meanwhile (read from the code,
not run).
The code is the same in 2.2.0 and 2.2.1 (read from the release tags, not run
there).
h3. Reproduction
PASS, deterministic, unmodified source.
[^TestBugMC4ReconcileDiskBalancerMove.java] ({{hadoop-hdds/container-service}})
has one datanode with two volumes whose replica has blocks 0 and 1 at BCSID 1,
and a peer that also has block 2 at BCSID 2. It uses the real
{{KeyValueHandler}}, {{ContainerSet}}, {{ContainerController}}, on demand
scanner and the move task handed out by {{DiskBalancerService.getTasks}}. Only
the calls to the peer are mocked, as in
{{TestContainerReconciliationWithMockDatanodes}}, and the ICRs are recorded
instead of sent. The only timing aids are two existing test hooks. In the first
test {{KeyValueContainer.setInjector}} holds the move after its copy while the
reconciliation runs from start to end: the only ICR reports BCSID 2 and the
peer's data checksum, the move then puts the copy in place, the live replica
has blocks 0 and 1 at BCSID 1, block 2 exists only in the DELETED replica, and
no further ICR is sent. In the second test the move is started from the peer's
first GetBlock call and {{DiskBalancerService.setInjector}} holds it right
after the container set points to the copy: the closing ICR comes from the
retired replica with BCSID 2 and the live replica is unrepaired. In the third
test the move completes at that GetBlock call: the closing ICR reports state
DELETED while the live replica is CLOSED and unrepaired. A passing test means
the defect is present.
h3. Patch
[^MC-4-skip-diskbalancer-move-of-container-being-reconciled.patch], against
1e528b97aed3eef1be6846f1f0767c1ef98a883a. It also applies to master at
8ca44914771.
{{KeyValueHandler.reconcileContainer}} resolves the container with
{{ContainerSet.getContainerWithWriteLock}}, marks it as being reconciled under
that lock and releases the lock before it starts. A move in progress holds the
read lock until it has replaced the replica, so the reconciliation waits for it
and works on the replica the move left in the container set.
{{DiskBalancerTask}} checks the mark under its read lock, next to its existing
state check, and skips the move. The container is eligible again once the
reconciliation has ended. The mark is in memory only. The write lock is not
held during the reconciliation, so export, delete and the scanner are not
blocked by it.
Behaviour that changes outside the race: the start of a reconciliation now
waits for a running export or move of that container (while it waits, new
readers of that container lock queue behind it, as for the other users of
{{getContainerWithWriteLock}}), and a reconcile command whose container is
deleted before the reconciliation starts, for example while it waits, now fails
with {{CONTAINER_NOT_FOUND}} instead of running on the deleted object.
Covered by {{moveSkipsContainerBeingReconciled}},
{{reconcileWaitsForMoveInProgress}} and
{{reconcileAppliesOnDestVolumeContainer}} in the existing
{{TestDiskBalancerWithConcurrentBackgroundTasks}}. Without the change all three
fail for all 8 schema and layout combinations (the container is moved during
the reconciliation, the reconciliation does not wait for the move, and the ICR
is sent for the stale replica). With it that suite (56 tests) and the
DiskBalancer, {{KeyValueHandler}}, container and reconciliation unit suites
pass (715 tests, 6 skipped by the suite), {{TestDiskBalancer}} and
{{TestDiskBalancerDuringDecommissionAndMaintenance}} pass on a mini cluster,
checkstyle is clean. {{TestContainerCommandReconciliation}} has not been run
with the patch (its mini cluster did not start in the test environment). With
the patch all three tests of the reproduction fail, because the move is skipped
or the reconciliation waits for it, and the replica is repaired in place.
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]