Siyao Meng created HDDS-16824:
---------------------------------
Summary: Lease recovery takes the block length from the first
replica it can ask, so a lagging replica truncates hsynced data
Key: HDDS-16824
URL: https://issues.apache.org/jira/browse/HDDS-16824
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments:
MC-5-lease-recovery-use-largest-replica-block-length.patch,
TestBugMC5RecoveredLengthFromLaggingReplica.java
h3. Mechanism
When {{finalizeBlock}} in {{BasicRootedOzoneClientAdapterImpl}} or
{{BasicOzoneClientAdapterImpl}} cannot finalize a block through its Ratis
pipeline (the pipeline is no longer open, or FinalizeBlock failed), it falls
back to {{ContainerProtocolCalls.getCommittedBlockLength}} on a standalone
pipeline made of the datanodes of the block. {{XceiverClientGrpc}} asks the
datanodes one after another and returns the first successful answer, and
{{LeaseRecoveryClientDNHandler}} commits the file with that length.
Nothing on this path tells replicas apart. hsync commits the block to OM with
the block ID as allocated, whose block commit sequence ID is 0, so
{{BlockUtils.verifyBCSId}} on the datanode accepts every replica, and a replica
that is still open answers {{GetCommittedBlockLength}} like a closed one. A
replica that missed the last PutBlock of the block reports the shorter length,
and the file is closed with it.
With the default {{ozone.network.topology.aware.read=true}} the datanodes are
asked in the order in which SCM returned them, otherwise in random order.
h3. Trigger
# A client writes a RATIS/THREE file and calls hsync.
# One datanode of the pipeline stops. hsync keeps succeeding: after
{{hdds.ratis.raft.client.rpc.watch.request.timeout}} (default 30s) the client
stops waiting for all replicas and accepts a commit by the majority.
# The writer dies while the datanode is down, before an hsync in a next block
has reached OM. The container is closed and the pipeline removed before the
datanode returns, so it cannot catch up and its replica keeps the shorter block.
# {{recoverLease}} is called after the datanode is back and before
ReplicationManager has replaced its replica, and that datanode is the first one
asked.
Steps 1, 2 and 4 are run as written. In step 3 the reproduction closes the
container and removes the pipeline with SCM calls. That SCM does both on its
own for a datanode that stays down longer than
{{ozone.scm.stale.node.interval}} (default 5m), and that ReplicationManager
needs minutes to replace the replica ({{hdds.scm.replication.thread.interval}}
is 300s), is from reading the code, not run. Step 3 is one way to get there.
What the fallback needs is any replica that is behind and reachable when it
runs, for example a follower that is still replaying its log after a restart
(argued, not run).
h3. Impact
The recovered file is shorter than what hsync acknowledged, although two of
three replicas hold the data. In the reproduction the second hsync of 100 bytes
returned successfully and the file is closed with 100 bytes. For a write ahead
log this is silent loss of acknowledged entries. The length is final once the
file is closed, and the bytes beyond it on the two healthy replicas are no
longer reachable through the file.
h3. Reproduction
PASS on unmodified source, nothing injected, recovery not forced.
[^TestBugMC5RecoveredLengthFromLaggingReplica.java]
({{hadoop-ozone/integration-test}}) starts a mini cluster with three datanodes.
The writer hsyncs 100 bytes, the datanode whose replica SCM lists first is
stopped, and the writer hsyncs 100 more bytes. The container is closed and its
pipeline removed, the datanode is started again and still has 100 bytes of the
block, the other two have 200. {{recoverLease}} returns true and the file is
closed with 100 bytes. The only non default settings are those that enable
hsync, a lease soft limit of zero and
{{hdds.ratis.raft.client.rpc.watch.request.timeout}} of 3s instead of 30s. A
passing test means the defect is present.
h3. Suggested fix
The attached patch makes the fallback ask every replica and use the largest
length. For writers whose PutBlock goes through the Raft log, which is the
default, datanodes apply a PutBlock only after it is committed in Ratis, and
the length of a block only grows with the applied log. The largest length among
the replicas is then the length at the most advanced replica, which is the
state ReplicationManager converges to (argued from the code).
Not covered:
* A data stream writer with
{{ozone.client.datastream.putblock.without.raft.enabled}} (off by default)
sends PutBlock outside the Raft log, so each datanode applies it on its own and
the argument above does not hold. There the largest length can be one that only
one replica has (argued, not run).
* If only lagging replicas answer during recovery, the result is still too
short, and a length below the one OM already holds for the block is still
accepted. Failing recovery in that case unless it is forced belongs in
{{LeaseRecoveryClientDNHandler}} and is not part of this patch. Requiring an
answer from all or a majority of the replicas would close the rest, at the
price of failing recovery whenever a datanode is down, which is the situation
recovery exists for.
* Until ReplicationManager has replaced the lagging replica, a reader that is
served by it gets an error for the bytes it does not have (from reading the
code, not run), because the recovered key carries block commit sequence ID 0
and the read path cannot reject that replica. Returning the sequence ID from
{{GetCommittedBlockLength}} and storing it at recovery would address that. It
needs a protocol change.
h3. Patch
[^MC-5-lease-recovery-use-largest-replica-block-length.patch], against
ea69b4a7d9abd040e5259dbcbb7c6ce52eb5d199. It also applies to master at
84f5594a6a2.
{{ContainerProtocolCalls.getMaxCommittedBlockLength}} sends
{{GetCommittedBlockLength}} to all datanodes of the pipeline with the existing
{{XceiverClientSpi.sendCommandOnAllNodes}} and returns the largest length. Both
adapters use it in the fallback. Replicas that fail or do not answer are
skipped, and one warning is logged when a replica is skipped or the lengths
differ. What changes for recoveries the bug does not affect:
* It is one request per datanode of the pipeline of the block, sent in
parallel, and all answers are awaited. For a container that is no longer open
that is every replica SCM lists, in any state. A datanode that accepts the
request and does not answer now always costs one {{ozone.client.read.timeout}}
(30s), where before it cost that only if it was asked before one that answered.
The worst case drops from one timeout per replica in a row to one.
* If no replica returns a length, {{NO_SUCH_BLOCK}} or {{CONTAINER_NOT_FOUND}}
is thrown only when every datanode that was asked answered with one of the two,
so {{LeaseRecoveryClientDNHandler}} skips a block only then. Otherwise an
{{IOException}} is thrown. Before, the error of the last datanode tried was
thrown.
* An {{UNAUTHENTICATED}} answer of a datanode no longer surfaces as
{{SCMSecurityException}}. That replica is skipped like one that is unreachable
(from the code, not run).
* EC keys do not reach the fallback (hsync is not supported for EC and the
fallback rejects non replicated keys).
Covered by two unit tests in {{TestContainerProtocolCalls}} and by
{{testBlockLengthOfLaggingReplicaIsNotUsed}} in the existing
{{TestLeaseRecovery}}, which fails without the change with a file length of 100
instead of 200. Two assertions in {{TestLeaseRecovery}} that counted the
datanodes tried one after another now count the requests that are sent. On
unmodified source those two tests ({{testGetCommittedBlockLengthTimeout}},
{{testGetCommittedBlockLengthWithException}}) fail intermittently, because
their helper {{closeLatestContainer}} closes the last container SCM lists,
which is not always the container of the file, and then the fallback is never
reached. With the patch the reproduction fails at its length assertion, because
the file is closed with 200 bytes.
Found by TLA+ model checking and code review of the lease recovery paths for
hsynced files under HDDS-15926, on commit
ea69b4a7d9abd040e5259dbcbb7c6ce52eb5d199. 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]