yandrey321 opened a new pull request, #11783:
URL: https://github.com/apache/ozone/pull/11783

   ## What changes were proposed in this pull request?
   
   **Problem.** Since HDDS-16396, positioned reads (preads) of an EC key run 
concurrently and leave the sequential
   position alone. They do it by building a new reader for each call: 
`ECBlockInputStreamProxy.readPositioned` creates
   a throwaway proxy, seeks it, reads and closes it. Every pread therefore 
opens new per-index block streams. That means
   a GetBlock call to a datanode for each index the pread touches, before any 
data is read. A 4 KB pread costs a GetBlock
   plus a ReadChunk, and a 1 MB pread that spans two cells costs two of each.
   
   **Implementation idea.** A healthy pread reads through the proxy's own 
long-lived `ECBlockInputStream`, using the
   block streams it already has open:
   - `ECBlockInputStream.readPositioned`, which used to throw, splits the range 
into cells by data index. It reads each
     index's cells with the stateless positioned read of that index's block 
stream (`BlockInputStream.readPositioned`,
     HDDS-16396). Neither the EC stream nor the per-index streams move.
   - With `ozone.client.ec.parallel.read.enabled` (HDDS-16867), the indexes are 
read in parallel, as in a sequential
     read. Otherwise they are read one after another.
   - `ECBlockInputStreamProxy.readPositioned` takes this path while the stream 
is healthy. If any index fails, the
     failed locations are recorded, and that call alone falls back to the 
existing per-call reader, which reconstructs
     the data. Once a location has failed, later preads take the reconstruction 
path, as they do today.
   
   **Key decisions.**
   - **A pread never changes shared state.** A failed index isn't switched to a 
spare and its stream isn't closed;
     the error goes to the proxy, which reconstructs for that call. Spare 
switching stays with the sequential read.
   - **Thread safety:**
     - A small lock covers the lazy creation of per-index streams, their close, 
and the switch to a spare, because
       preads open streams without the stream monitor.
     - After switching to a spare, the sequential read closes the slot again, 
in case a concurrent pread reopened the
       stream on the failed location.
     - If a sequential read closes a stream that a pread is using, the pread 
fails and falls back. That is slower but
       still correct.
   - **One code path for cell reads.** The cell split, the parallel fan-out and 
the failure report are extracted from
     the HDDS-16867 parallel read and shared. The per-index read gets a 
`positioned` flag.
   - **Degraded reads are unchanged.** When a reconstruction reader is in use, 
each pread still gets its own decoder
     and buffers. Reading only the requested range in that case is a separate 
sub-task.
   - `ECBlockReconstructedStripeInputStream` keeps rejecting preads, and still 
doesn't report `PREADBYTEBUFFER`.
   
   **Side effects.**
   - Per-index block streams now serve sequential reads and preads at the same 
time. Both use `BlockInputStream`'s
     existing concurrency contract, the same one non-EC keys already rely on.
   - One stream that fails during a pread costs that call a failed attempt plus 
a reconstruction. Before, the per-call
     reader switched to a spare first, if one existed.
   
   Generated-by: Claude Code (Claude Opus 5.5)
   
   ## What is the link to the Apache JIRA
   
   https://issues.apache.org/jira/browse/HDDS-16870
   
   ## How was this patch tested?
   
   - **Benchmark:** new `TestEcPositionedReadBenchmark` (`@Tag("benchmark")`, 
so it doesn't run in CI).
     - Setup: MiniOzone with 5 datanodes, a 48 MB RS(3,2) key with 1 MB cells, 
and one open stream. Preads go to random
       offsets; 5 rounds after 1 warm-up. The table gives the median throughput 
of the rounds and the latency over all
       rounds.
     - The A/B ran in sequence, base `hdds-client` and then this patch, twice; 
the two repetitions agree within 4%. The
       table shows repetition 1.
   
     | pread | threads | ops/s before | ops/s after | p50 ms before / after | 
p99 ms before / after | GetBlock per pread before / after |
     |---|---|---|---|---|---|---|
     | 4 KB | 1 | 1604 | 2562 (+60%) | 0.64 / 0.37 | 1.24 / 1.12 | 1.01 / 0.05 |
     | 4 KB | 8 | 4436 | 8537 (+92%) | 1.72 / 0.86 | 3.53 / 1.80 | 1.01 / 0.01 |
     | 64 KB | 1 | 2087 | 3316 (+59%) | 0.44 / 0.27 | 0.94 / 0.66 | 1.07 / 0.05 
|
     | 64 KB | 8 | 4206 | 8148 (+94%) | 1.77 / 0.90 | 4.15 / 2.22 | 1.06 / 0.01 
|
     | 1 MB | 1 | 677 | 904 (+34%) | 1.46 / 1.04 | 4.18 / 1.90 | 2.02 / 0.24 |
     | 1 MB | 8 | 2044 | 3139 (+54%) | 3.78 / 2.39 | 7.21 / 5.19 | 2.00 / 0.03 |
   
     - ReadChunk calls per pread are identical in both arms, so the data path 
is unchanged; only the GetBlock calls go.
     - The GetBlock calls that remain come from opening the stream once per 
round, spread over that round's preads.
     - Small preads gain the most, because a GetBlock round trip is a large 
part of their cost; with 8 threads, their
       throughput nearly doubles.
     - The 1 MB p99 at 1 thread falls by half because the reads no longer wait 
on a GetBlock.
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to