[
https://issues.apache.org/jira/browse/HDDS-17004?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Siyao Meng updated HDDS-17004:
------------------------------
Attachment: CR-1-fail-over-streaming-read-on-checksum-mismatch.patch
> Streaming block read does not try another replica after a client side
> checksum mismatch
> ---------------------------------------------------------------------------------------
>
> Key: HDDS-17004
> URL: https://issues.apache.org/jira/browse/HDDS-17004
> Project: Apache Ozone
> Issue Type: Bug
> Reporter: Siyao Meng
> Priority: Major
> Attachments:
> CR-1-fail-over-streaming-read-on-checksum-mismatch.patch,
> TestBugCR1StreamReadChecksumNoFailover.java
>
>
> h3. Mechanism
> With {{ozone.client.verify.checksum=true}} (default)
> {{StreamBlockInputStream.StreamingReader.onNext}} verifies each ReadBlock
> response and fails the stream with {{OzoneChecksumException}} on a mismatch.
> The reader surfaces it as {{IOException("Streaming read failed")}} wrapping
> an {{ExecutionException}}. {{ConnectionFailureUtils.unwrapCause}} only stops
> at {{StorageContainerException}} and {{TimeoutIOException}}, and
> {{StreamBlockInputStream.handleExceptions}} (sequential reads) and
> {{handlePreadException}} (positioned reads) retry only those and connectivity
> errors. So the checksum error is rethrown and no other replica is tried. On
> the sequential path the failed reader is also kept, so later sequential reads
> of other ranges on the same stream fail with the same error until the stream
> is unbuffered or reopened.
> The non streaming path validates checksums inside the per datanode loop of
> {{XceiverClientGrpc.sendCommandWithRetry}} and moves on to the next replica
> for that request. Datanodes do not verify checksums for client reads by
> default, so silent corruption on one replica reaches the client as successful
> responses with bad bytes.
> h3. Trigger
> # {{ozone.client.stream.readblock.enable=true}}, a RATIS/THREE key.
> # Silent corruption of the block data on one replica (one byte flipped in
> place).
> # Read the key with a client whose stream starts on that replica.
> Which replica a stream starts on follows the order OM returns
> ({{ozone.network.topology.aware.read=true}} by default). Replicas at the same
> distance are shuffled per lookup, so a client hits the corrupt replica first
> on every read only when it is strictly the closest one (for example on the
> same host), and otherwise on a share of the reads.
> h3. Impact
> * A streaming read that starts on the corrupt replica fails, although two
> healthy replicas exist. The non streaming read of the same key succeeds.
> * No wrong data is returned.
> * Only with streaming block reads enabled (off by default).
> * On the current code the failure appears as a false EOF ({{Inconsistent read
> ... numBytesRead=-1}}) instead of the checksum error, because of HDDS-16680:
> the error arrives while {{StreamingReader.poll}} waits and is turned into end
> of stream before the retry decision. The fix for this issue depends on
> HDDS-16680 (apache/ozone PR #11388): only with both does the read succeed.
> h3. Reproduction
> PASS, deterministic in a mini cluster, unmodified source.
> [^TestBugCR1StreamReadChecksumNoFailover.java]
> ({{hadoop-ozone/integration-test}}) writes a 32 KiB RATIS/THREE key on three
> datanodes, flips one byte of the block file on the first pipeline node, then
> reads the key 5 times without and 5 times with streaming block reads. All
> datanodes have the client's address there, so topology aware read puts the
> corrupted node first for every read. A passing test means the defect is
> present. Results:
> ||Code||Non streaming||Streaming||Streaming failure||
> |unmodified|5/5|0/5|false EOF|
> |with the attached patch|5/5|0/5|false EOF (HDDS-16680)|
> |with PR #11388 (HDDS-16680)|5/5|0/5|{{Streaming read failed}} caused by
> {{OzoneChecksumException}}|
> |with both|5/5|5/5|none, each read skips the corrupt replica once|
> The short circuit read path appears to have the same gap
> ({{BlockInputStream}} rereads the same local file after a checksum error
> until its retries are used up); this is from code reading only, was not run
> and is not covered by the attached patch.
> h3. Patch
> [^CR-1-fail-over-streaming-read-on-checksum-mismatch.patch], against
> 1fb64c8e499161d1452ae6948ffd461fc9d2c302. It also applies to master at
> fd2f6092c6d, and together with PR #11388.
> {{ConnectionFailureUtils.unwrapCause}} (used only by
> {{StreamBlockInputStream}}) also returns an {{OzoneChecksumException}} found
> in the cause chain. On such an error the sequential path
> ({{handleExceptions}}) and the positioned path ({{readPositioned}}) release
> the failed reader and skip that replica, then open a stream on the next
> replica, as the non streaming path does per request. The skip lasts for one
> positioned read, or for one refill of the sequential read buffer (the set is
> cleared when the buffer is refilled). This cannot loop: within that scope
> every checksum failure either adds a replica not tried yet or rethrows, and a
> refill that succeeds returns data that the read then consumes. A checksum
> mismatch is a fault of the data in one range, not of the datanode, so the
> replica is not added to the stream's {{failedStreamingDatanodes}}: reads of
> other ranges, and later reads of the same range, start again from the first
> replica. There is no retry sleep, no block location refresh and no use of the
> read retry limit ({{ozone.client.max.read.retries}}) for this case, on either
> path; it is bounded by the number of replicas. When every replica fails the
> check, the read throws {{IOException("Streaming read failed")}} with the
> {{OzoneChecksumException}} of the last replica tried as its root cause
> (through an {{ExecutionException}}), the same exception as today, and later
> reads of other ranges still work.
> Covered in the existing unit {{TestStreamBlockInputStream}} with three mocked
> replicas, each case for sequential and positioned reads:
> {{testChecksumMismatchFailsOverToAnotherReplica}} (first replica corrupt, the
> read returns the data),
> {{testChecksumMismatchOnAllReplicasSurfacesChecksumError}} (all replicas
> corrupt, {{OzoneChecksumException}} is the root cause),
> {{testChecksumMismatchInOneRangeDoesNotAffectOtherRanges}} (offset 0 corrupt
> on all replicas, a read of offset 2 on the same stream returns the data) and
> {{testChecksumSkipDoesNotUseReadRetry}} (first replica corrupt, second
> answers CONTAINER_NOT_FOUND, one read retry allowed, the read returns the
> data from the third). The responses arrive synchronously, so the tests do not
> depend on HDDS-16680. Without the change the first fails with {{IOException:
> Streaming read failed}} caused by {{OzoneChecksumException}}, and the third
> fails on its sequential half because the failed reader is kept. With it
> {{TestStreamBlockInputStream}} (33 tests), {{TestConnectionFailureUtils}}
> (21), {{TestBlockInputStream}} (17), {{TestChunkInputStream}} (10),
> {{TestMultipartInputStream}} (6), and in a mini cluster
> {{TestStreamReadDatanodeFailover}} (3), {{TestStreamRead}} (4) and the
> integration {{TestStreamBlockInputStream}} (3) pass, checkstyle is clean.
> Related: HDDS-16873 proposes reporting replicas that fail client side
> checksum verification; it assumes the client already moves to another replica.
> Found by code review of the Ozone client read retry and failover paths, as
> part of the TLA+ verification effort under HDDS-15926, on commit
> 1fb64c8e499161d1452ae6948ffd461fc9d2c302. 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]