Chu Cheng Li created HDDS-16563:
-----------------------------------

             Summary: Streaming ReadBlock can return no data at the end of a 
block, surfacing to the client as a false EOF
                 Key: HDDS-16563
                 URL: https://issues.apache.org/jira/browse/HDDS-16563
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Chu Cheng Li
            Assignee: Chu Cheng Li


*Problem*

With {{{}ozone.client.stream.readblock.enable=true{}}}, a read of the last 
bytes of a block can come back with zero data. The block input stream reports 
end of stream, and {{KeyInputStream}} turns the short read into:
{code:java}
java.io.IOException: Inconsistent read for blockID=conID: 96 locID: 
117883640217602265 bcsId: 35223
  replicaIndex: null length=147058825 position=147054729 numBytesToRead=4096 
numBytesRead=-1
        at 
org.apache.hadoop.ozone.client.io.KeyInputStream.checkPartBytesRead(KeyInputStream.java:189)
{code}
The read is the last 4096 bytes of the block: 147054729 + 4096 = 147058825 = 
the block length the client got from OM. Reading the same key with streaming 
reads disabled works.

Nothing is logged on the datanode for this block, and the client sees a clean 
end of stream rather than an error, so the failure is invisible on both sides.

*Impact*

HBase on Ozone: a regionserver reading an HFile trailer gets
{code:java}
org.apache.hadoop.hbase.io.hfile.CorruptHFileException: Problem reading HFile 
Trailer from file
  ofs://.../f/0a2fdc103067494f87f4174fa4c02acd
{code}
which fails bulk load (the tool retried 152 times and gave up) and would 
equally fail region open, i.e. the data is unreadable for as long as the 
condition lasts. A healthy file is reported as
corrupt.

*Steps to reproduce (as observed)*
 # 3 datanodes, FSO bucket, RATIS/THREE.
 # Copy a large directory into the bucket: {{ozone fs -put -t 16 <55 GB local 
dir> ofs://...}}
(151 HFiles of ~355 MB each; this created blocks of varying size, the affected 
one 147058825 bytes, not the 256 MB default).
 # Read the tail of each file. 150 of 151 files read fine; one failed 
consistently.

Reading that one file with the same command and three client configurations:
||read path||result||
|default (non-streaming)|reads fine|
|{{ozone.client.stream.readblock.enable=true}}|CorruptHFileException, 
"Inconsistent read", numBytesRead=-1|
|Ratis data stream read (HDDS-9904)|CorruptHFileException|

Both streaming paths fail; the non-streaming path does not. Copying the same 
local file to a new Ozone path made it readable through every path, so the file 
content is fine.

*Two observations that should guide the investigation*
 # *It is not permanent.* About 30 minutes later the original file read fine 
through the streaming path. In between, all open containers in the cluster were 
closed
({{{}ozone admin container close{}}}). The leading hypothesis is therefore that 
this affects blocks in *open* containers, where the length the datanode serves 
lags the length OM reports, and that it heals when the container closes. This 
was not verified directly; the container state of container 96 at failure time 
was not captured.
 # *The non-streaming path masks it.* {{BlockInputStream}} fails over to 
another replica on error, while the streaming reader is pinned to one datanode 
and only excludes it when the outcome is treated as a failure 
({{{}failedStreamingDatanodes{}}},
StreamBlockInputStream#getStreamingReader). A stream that ends with no data is 
not a failure, so there is no failover and the read fails deterministically 
against that replica.

*Candidate mechanisms*
 # {{KeyValueHandler.readBlockImpl}} computes a read length of 0 for a range at 
the end of the block and sends no responses; {{readBlock}} returns null 
(success) and the client sees a normal end of stream. Worth checking 
{{ReadBlockComputation.computeAdjustedLength}} for a range whose end is the
last byte of the block and whose start is inside the final, partial checksum 
interval.
 # The datanode's {{blockData.getSize()}} is smaller than the client's offset, 
so the out of range branch added in HDDS-16207 fires. That branch reports on 
the stream and deliberately does not log (KeyValueHandler.java, "An out of 
range offset is a client fault"), which matches the absence of datanode 
logging, and the client converting it into EOF matches the "failure mistaken 
for a normal end of stream" behaviour described in HDDS-16241.

*Suggested fix*
 * The datanode should never answer a ReadBlock for an in-range offset with 
zero bytes and success. If the requested range cannot be served (block shorter 
than the client believes), fail the request explicitly and log it at WARN with 
block id, requested offset/length and the datanode's own block size.
 * The client should not treat "stream ended before the requested range was 
delivered" as EOF; it should fail, and fail over to another replica the way the 
non-streaming path does.
 * If the trigger really is a block in an open container, HDDS-15886 may share 
the root cause; that issue is framed around hsync, while here a plain closed 
key written by "ozone fs -put" is affected.

*Environment*

Ozone master @ aed7ceac7c (built from the HDDS-9904 branch merged with master; 
the gRPC streaming read path involved is unmodified master code), Ratis 3.3.1, 
3 datanodes on one host, HBase 2.6.6 on ofs, JDK 17/21.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to