[
https://issues.apache.org/jira/browse/HDDS-16811?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Siyao Meng updated HDDS-16811:
------------------------------
Attachment: MC-2-do-not-restore-deleted-blocks-in-reconciliation.patch
> Reconciliation can restore a deleted block, or commit it without its file
> when block deletion runs during the repair
> --------------------------------------------------------------------------------------------------------------------
>
> Key: HDDS-16811
> URL: https://issues.apache.org/jira/browse/HDDS-16811
> Project: Apache Ozone
> Issue Type: Bug
> Reporter: Siyao Meng
> Priority: Major
> Attachments:
> MC-2-do-not-restore-deleted-blocks-in-reconciliation.patch,
> TestBugMC2DeletedBlockRestoredByReconciliation.java,
> TestBugMC3BlockDeletedDuringReconciliation.java
>
>
> h3. Mechanism
> {{BlockDeletingTask.deleteTransactions}} handles a delete transaction for a
> block that has no metadata in the container by calling
> {{KeyValueHandler.deleteUnreferenced}}, which removes the block file if there
> is one, and then removes the transaction like any other. A block that has
> metadata is also recorded as deleted in the container merkle tree
> ({{ContainerChecksumTreeManager.addDeletedBlocks}}). A block without metadata
> is not, so the tree keeps no trace of the deletion.
> Reconciliation copies every block that is live in the peer's tree and has no
> entry in the local tree. A deleted entry in the local tree is the only thing
> that makes it skip a block. {{KeyValueHandler.reconcileChunksPerBlock}} then
> pulls the chunks, writes them and commits the block metadata with
> {{putBlockForClosedContainer}} without holding the container lock, while the
> block deleting service works under the container write lock. Nothing orders a
> deletion against the copy of the same block.
> Two results, depending on when the block deleting service handles the
> transaction:
> # Before reconciliation reads the local tree: the block is copied back from a
> peer that has not deleted it yet, and no transaction is left to delete it.
> # While reconciliation copies the block: the block file is deleted and
> reconciliation still commits the block metadata, so the replica has block
> metadata without a block file.
> The same code is in the tags ozone-2.1.0, ozone-2.2.0 and ozone-2.2.1 (read
> with git grep on the tags, not run there).
> h3. Trigger
> # A QUASI_CLOSED container has a replica that lacks a block the other
> replicas have, for example a replica that was quasi closed behind the others.
> This is the state reconciliation exists to repair.
> # The key is deleted. SCM sends the block delete transaction to every
> replica, including the one without the block.
> {{DeletedBlockLogImpl.getTransactions}} sends it when the replication health
> of the container is HEALTHY, and a QUASI_CLOSED container whose replicas are
> all QUASI_CLOSED is HEALTHY whatever their sequence IDs are
> ({{RatisReplicationCheckHandler}} run in a unit test, {{DeletedBlockLogImpl}}
> read, not run).
> # {{ozone admin container reconcile}} is run for the container.
> The state of steps 1 and 2 is short lived by default (read in the code, not
> run). With replicas of all three origins present, the next replication
> manager pass ({{hdds.scm.replication.thread.interval}}, 300s) closes the
> replicas with the highest sequence ID ({{QuasiClosedContainerHandler}}).
> After that the lagging replica counts as unhealthy, the container is under
> replicated and its delete transactions are held back. A container that is
> stuck in QUASI_CLOSED with replicas of two origins is kept at five replicas
> by default ({{hdds.scm.replication.quasi.closed.stuck.best.origin.copies}} 3,
> {{other.origin.copies}} 2), which the same health check rates as over
> replicated. So the transaction has to be sent between the quasi close and the
> next replication manager pass, or while a stuck container has exactly three
> replicas. A transaction that a datanode has already stored stays pending
> there and is executed as long as its replica is CLOSED or QUASI_CLOSED.
> For the first result the lagging replica's block deleting service handles the
> transaction before a peer's does, and reconciliation with that peer runs in
> between. Each datanode runs the service on its own schedule. For the second
> result the transaction is still pending on the lagging replica and its block
> deleting service reaches it between the first chunk write and the commit of
> that block, which is the time it takes to pull the block's chunks from the
> peer. No fault is needed. How often either ordering occurs on a cluster was
> not measured.
> h3. Impact
> * Restored block: data of a deleted key stays on one datanode. SCM completes
> the transaction once every replica has acknowledged the command, which a
> datanode does when it stores the transaction, so it is not sent again (read,
> not run). A later reconciliation does not remove the block: once the peers
> have deleted it, their deleted entry carries the same checksum as the live
> block, all replicas report the same data checksum, and reconciling with both
> peers leaves the block in place (run). So no existing check shows the block,
> and a replication that uses this replica as its source copies it along,
> because the block files, the metadata and the tree file are all part of the
> container archive (read in {{TarContainerPacker}}, not run). The replica
> never becomes empty, and SCM deletes an empty container only when all its
> replicas report empty (read in {{EmptyContainerHandler}}, not run), so the
> container stays on all datanodes.
> * Metadata without file: the scan that reconciliation starts reports
> {{MISSING_DATA_FILE}} and marks the replica UNHEALTHY (run). If the replica
> had the block with only its first chunk, the same ordering ends in
> {{MISSING_CHUNK}} and UNHEALTHY (run). SCM is then expected to replace the
> replica from a healthy one (argued, not run). No data of a live key is lost
> in either case.
> h3. Reproduction
> PASS for both on unmodified source at
> 1e528b97aed3eef1be6846f1f0767c1ef98a883a, deterministic, module
> {{hadoop-hdds/container-service}}. Each datanode is a real
> {{KeyValueHandler}} on its own volume with the real
> {{DeleteBlocksCommandHandler}}, {{BlockDeletingService}} and on demand
> scanner. There is no SCM and no network: the delete transaction is handed to
> the command handler, and the datanode to datanode calls of reconciliation are
> answered in process by the peer's handler. A passing test means the defect is
> present.
> * [^TestBugMC2DeletedBlockRestoredByReconciliation.java]: three QUASI_CLOSED
> replicas, dn1 without block 1, the transaction stored on all three. After
> dn1's block deleting service ran: no metadata, no tree entry, no pending
> deletion. After dn1 reconciled with dn2, which has not deleted yet: metadata
> and block file present, tree entry live, no pending deletion. After dn2 and
> dn3 deleted the block and dn1 reconciled with both, ran its block deleting
> service and was scanned again: unchanged, and all three replicas report the
> same data checksum. After the remaining block is deleted, dn2 and dn3 are
> marked empty and dn1 is not.
> * [^TestBugMC3BlockDeletedDuringReconciliation.java]: two replicas, the
> transaction pending on dn1. One pass of dn1's block deleting service runs at
> the moment the peer is asked for the second chunk of the block. It runs in
> the reconciliation thread, which is possible because reconciliation holds no
> container lock there. On a datanode it is a separate thread. Result: block
> metadata present, block file absent, {{MISSING_DATA_FILE}}, replica UNHEALTHY.
> h3. Patch
> [^MC-2-do-not-restore-deleted-blocks-in-reconciliation.patch]. It applies on
> top of the patch attached to HDDS-16808 (the data checksum reported to SCM
> and stored in RocksDB is not refreshed when block deletion changes the
> container checksum tree), not to the tree without it. Both patches change the
> same call in {{BlockDeletingTask.deleteTransactions}}, and the entries added
> here move the tree root on ordinary deletes, so the refresh from HDDS-16808
> is needed for the reported data checksum to follow. Developed on
> 1e528b97aed3eef1be6846f1f0767c1ef98a883a plus that patch. The two patches in
> that order also apply to master at 8ca44914771. Below, with the patch means
> with both patches.
> Two parts, and the second relies on the first:
> # {{BlockDeletingTask.deleteTransactions}} passes the IDs of the blocks it
> found no metadata for to {{ContainerChecksumTreeManager.addDeletedBlocks}},
> which marks them as deleted in the tree write the pass already does. An entry
> that exists keeps its checksum, a new entry gets the checksum of a block
> without chunks. The call goes through the refresh that HDDS-16808 adds, so
> the data checksum in memory and in RocksDB follows the tree file. This is
> enough for the first result when the deletion is complete before
> reconciliation reads the local tree.
> # {{KeyValueHandler.reconcileChunksPerBlock}} closes the block file handle
> before the commit instead of after it, as the client put block paths do, and
> then commits under the container read lock only if the tree on disk does not
> mark the block as deleted. The read lock orders the check and the commit
> against the block deleting service, which holds the write lock for its whole
> pass.
> A block that is deleted during its repair leaves the patched code by one of
> two exits (both run):
> * The block file was removed while its handle was cached and later chunks
> were still written, which is the ordering of the second reproduction.
> {{finishWriteChunks}} then fails with "Chunk file not found" before any
> metadata is written. Moving that call in front of the commit is what stops
> this ordering. As for every caller of {{finishWriteChunks}}, the failure also
> requests an asynchronous volume check, which is throttled
> ({{hdds.datanode.disk.check.min.gap}}, 10m) and finds a healthy volume. The
> unpatched code makes the same request in this ordering, after the commit.
> * A block file exists at commit time, because the deletion ran before the
> first chunk write or the block was partly present. The tree check finds the
> deleted entry, the pulled chunks are removed and the block fails.
> Cost. The delete path gets no new lock and no additional file write, but the
> tree write it already does now changes the data checksum, see the side effect
> below. Reconciliation holds the read lock only for the commit of each
> repaired block, not while it pulls chunks. It reads and parses the tree file
> once per repaired block: measured 0.5 ms for a tree of 1,280 blocks, 1 ms for
> 10,000 and 10 ms for 100,000 (3 MB file), one chunk per block, warm page
> cache. That is noise for the usual repair of a few blocks and quadratic for a
> replica that misses most blocks of a container with very many small blocks
> (about 16 minutes in total for 100,000). Skipping the read while the tree is
> unchanged, for example with a write counter in
> {{ContainerChecksumTreeManager}}, is a possible follow up. Holding the read
> lock for the whole repair of a block would also work, but it would block
> block deletion and delete commands for the container across network reads
> from the peer.
> Side effect of the first part. Every delete of a block without metadata now
> adds a deleted entry, also for a block that no replica ever had. That is
> routine: the unused blocks of a committed key are deleted this way
> ({{OMKeyRequest.wrapUncommittedBlocksAsPseudoKey}}, read). Each such entry
> changes the data checksum in the tree of the replica that executes the
> delete, and the value the replica reports follows it in the same pass (run),
> where the unpatched code leaves the tree alone. Replicas on the patched
> version are equal again once all have executed the transaction. A replica on
> an older version, or one that executed the transaction before the upgrade,
> has no such entry and differs until it is reconciled, which only {{ozone
> admin container reconcile}} starts. SCM does not act on the value, and Recon
> lists the container as REPLICA_MISMATCH while the reported values differ
> (read). Trees written before the patch are not backfilled, so a delete
> executed before the upgrade is not protected by the patch. For a block that
> only some replicas had, the entries differ in checksum until the replicas
> reconcile, as the missing entry does today. A stricter alternative, not taken
> here, is to write the new entries only once the datanode layout is finalized
> on a version that has them, which needs a new layout feature.
> Covered by two new tests in the existing
> {{TestContainerReconciliationWithMockDatanodes}} (one per result, the second
> with three interleavings), an added assertion in
> {{TestBlockDeletingService.testWithUnrecordedBlocks}} and
> {{TestContainerMerkleTreeWriter.testMarkBlocksDeleted}}. One of the new tests
> also asserts that after a delete pass that records an entry for a block the
> replica never had, the data checksum in memory, in RocksDB and in the tree
> file agree. On HDDS-16808 alone the tests in the first two classes fail (8
> failures). With both patches 534 tests in 19 suites of
> {{hadoop-hdds/container-service}} and {{TestBlockDeletion}} on a mini cluster
> pass, checkstyle is clean. {{TestContainerCommandReconciliation}} was not run
> because its secure mini cluster did not start in the test environment. It is
> the existing suite most likely to notice the change to what block deletion
> writes to the tree, so it should pass in CI before the patch goes in. With
> both patches the two reproductions fail: the block is not restored, and in
> the second no metadata is committed and the replica stays QUASI_CLOSED. With
> HDDS-16808 alone they still pass.
> Not covered by the patch: a replica that already holds a restored block is
> not cleaned up.
> 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. Related but
> different: HDDS-11765 (apply block deletions that a replica missed, open),
> HDDS-13712 (more tests for deleted block handling in reconciliation, open)
> and HDDS-10929 (coordinate reconciliation with container deletion and
> replication, open). 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]