bobhan1 opened a new pull request, #67292:
URL: https://github.com/apache/doris/pull/67292
### What problem does this PR solve?
Issue Number: None
Related PR: #66520 (superseded by this redesign)
Problem Summary:
Doris data pages are usually much smaller than a 1 MiB File Cache block. A
cold query that misses File Cache currently expands each page read to block
boundaries, which provides a fixed 1 MiB form of read-ahead but also creates
large read amplification for sparse row IDs, point queries, TopN, and delayed
materialization. At the same time, `FileColumnIterator` consumes data pages one
at a time, so the relatively small scanner batch exposes many serial S3
dependencies even when the query will soon read adjacent pages from the same
segment.
This PR adds an opt-in, query-owned range read-ahead path for cloud scans.
It plans future pages by compressed bytes across scanner batches, coalesces
nearby page intervals into bounded file ranges, reads those ranges
concurrently, serves exact page slices to the existing page decoder, and writes
back only ranges that the query actually consumes. Complete 1 MiB blocks enter
the existing asynchronous File Cache write path directly; partial blocks are
completed by a bounded background hole-fill pipeline before entering that same
path.
The feature is disabled by default. Admission rejection, cancellation,
speculative-read failure, checksum/decode failure, and cache-writeback pressure
do not change query correctness: reads fall back to the existing path, while
writeback is best effort.
#### Architecture
```mermaid
flowchart LR
SCAN["Scanner batch row IDs"] --> ROLE["SegmentIterator<br/>classify
eager and lazy columns"]
ROLE --> WINDOW["ColumnReadAhead<br/>compressed-byte windows"]
WINDOW --> PLAN["FileRangePlanner<br/>coalesce and optional block
completion"]
PLAN --> ASYNC["AsyncFileRangeReader<br/>bounded concurrent exact reads"]
ASYNC --> BUFFER["Query-owned range buffers"]
BUFFER --> PAGE["ReadAheadFileReader<br/>exact page slices"]
PAGE --> DECODE["Existing PageIO and page decoder"]
DECODE --> USED{"Was any page in the range consumed?"}
USED -->|No| DROP["Release buffer"]
USED -->|Yes| SPLIT["Split at 1 MiB File Cache boundaries"]
SPLIT -->|Complete block| PHASE1["Existing AsyncCacheWriteManager"]
SPLIT -->|Partial block| HOLE["PartialBlockWritebackManager"]
HOLE --> GET["Dedicated S3 GET pool<br/>read block-local holes"]
GET --> PHASE1
```
#### Read-ahead planning
Columns are divided by data dependency rather than by SQL expression kind:
- An eager column is required before the current filtering or expression
stage and can be planned immediately.
- A lazy column depends on an earlier result and uses a smaller speculative
window. All currently plannable eager and lazy physical columns are submitted
together before eager decoding starts, allowing ranges from different columns
to share one admission and scheduling step.
- Array and Map element row IDs are not known until their offset columns
have been decoded. Their child pages are therefore planned immediately after
the real element-space row IDs become available, using the lazy window. Struct
children remain in the parent row-ID space and can be planned with the parent.
Each physical column keeps a high and low watermark measured in compressed
page bytes. Pages required by the current batch always enter the plan, even if
they exceed the high watermark. Additional pages extend across future scanner
batches until the high watermark is reached. Once consumed, discarded, or
failed pages reduce the remaining window to the low watermark, the planner
refills it. This compensates for scanner batch boundaries without tying I/O
depth to row count or data type.
#### Range coalescing and foreground block completion
The coalescing shape follows the same left-to-right idea used by Velox: sort
and de-duplicate page intervals, then add the next interval only when all three
limits remain satisfied.
1. The gap to the next page is at most 64 KiB.
2. The resulting physical range is at most 2 MiB.
3. Physical bytes divided by the union of requested page bytes is at most
2.0.
After base ranges are formed, the planner evaluates File Cache blocks
touched by their boundaries. A block with at least 50% requested-page coverage
may be completed in the foreground when the final range still fits the 2 MiB
limit. If both boundaries qualify but cannot both fit, the side requiring fewer
additional bytes is selected first. This avoids sending a nearly complete block
through a second background S3 GET: for object storage, reading a modest number
of additional bytes in the existing request is normally cheaper than adding
another request and scheduling round.
`AsyncFileRangeReader` performs the final ranges through a `NO_WRITE +
UNALIGNED` request context. `CachedRemoteFileReader` still reuses already
downloaded cache blocks, but a cold miss reads only the exact missing spans and
does not reserve or populate File Cache on the query thread.
#### Concurrency, ownership, and cancellation
`AsyncFileRangeReader` is a BE-level concurrent file-range executor. It
deliberately does not understand pages, coalescing, or writeback policy. A
submission atomically reserves all buffers and both query/BE budgets before any
range is queued; a rejected submission leaves budgets unchanged and all pages
return to the original synchronous path.
The default executor has 64 workers. Fixed safety guards allow at most 128
in-flight ranges and 256 MiB of resident buffers per query, and 1024 ranges and
1 GiB per BE. A range slot is released at terminal I/O state, while its byte
reservation follows the range buffer lifetime so decoded pages and writeback
cannot escape accounting. Query cancellation cancels queued work and marks
running work for cancellation; BE shutdown rejects new submissions, cancels
queued work, and waits for running reads before releasing shared resources.
#### Consumed-range writeback
Writeback metadata and the File Cache invalidation epoch are captured
immediately before each asynchronous read submission. A successfully read range
becomes eligible only after at least one of its pages is decoded. Purely
speculative ranges are released without cache work.
Eligible ranges are split at File Cache block boundaries:
- A fragment covering the complete valid block, including a short physical
EOF block, is copied into the existing fixed-block asynchronous cache write
path.
- A partial fragment is copied into the BE-level
`PartialBlockWritebackManager` queue.
The existing fixed-block asynchronous cache write path is the only component
that creates and persists File Cache blocks. Query range buffers and background
partial-block buffers remain separate from its queue and ownership model.
#### Background hole fill
The partial-block queue is bounded to 256 MiB per BE. Fragments for the same
queued block are merged; a block that is already active is deduplicated. Under
pressure, the oldest queued block is evicted. When all pending blocks are
already active, the new fragment is rejected. These outcomes affect only cache
population.
The scheduler dispatches a block only when the existing cache writer has
spare capacity. If capacity is unavailable, the task remains queued and the
scheduler waits; S3 GET workers are not occupied while waiting for cache-write
capacity. The default dedicated hole-fill pool has 32 workers, so slow
object-storage GETs do not block the queue scheduler or the foreground
read-ahead executor.
For one partial block, the hole-fill algorithm is:
1. Merge all covered fragments inside that 1 MiB block.
2. Take the complement of the covered union to produce missing intervals.
3. Coalesce adjacent missing intervals with block-local limits: 32 KiB
maximum gap, 1 MiB maximum GET, and 2.0 maximum read amplification.
4. Read the resulting ranges through the underlying remote reader without
crossing the block boundary.
5. Submit the completed owned buffer to the existing cache writer with
spare-capacity-only admission, avoiding another full-block copy.
For example, if a block already contains `[0, 256 KiB)`, `[384 KiB, 416
KiB)`, and `[544 KiB, 1024 KiB)`, its holes are `[256 KiB, 384 KiB)` and `[416
KiB, 544 KiB)`. Their 32 KiB separation meets the gap and amplification limits,
so they become one S3 GET `[256 KiB, 544 KiB)`. The 32 KiB already present
between the holes is overwritten with identical remote data, trading a small
byte increase for one fewer object-storage request.
#### Failure and compatibility boundaries
- The path is enabled only for cloud `READER_QUERY` segment scans when
`enable_query_read_ahead=true`; the default remains `false`.
- The legacy Segment File Cache prefetcher is skipped only when the new
read-ahead context was created, so the two mechanisms do not issue duplicate
reads.
- Generic file readers can use the concurrent range path without File Cache
writeback. `CachedRemoteFileReader` adds the exact no-write read and
consumed-range writeback integration.
- Planning/admission errors and asynchronous read failures mark affected
pages for fallback. A checksum or decode error evicts the suspect Page Cache
entry and retries through the original reader.
- File Cache epoch invalidation, queue pressure, S3 hole-fill failure, and
fixed-block writer rejection only drop writeback work.
- Variant-column forwarding is outside this PR's current scope.
#### Configuration defaults
| Configuration | Default | Purpose |
|---|---:|---|
| `enable_query_read_ahead` | `false` | Enable the new cloud query
read-ahead path. |
| `read_ahead_io_workers_per_be` | `64` | Concurrent foreground file-range
workers per BE. |
| `read_ahead_eager_high_watermark_bytes` | `8 MiB` | Target compressed-page
bytes for each eager physical column. |
| `read_ahead_eager_low_watermark_bytes` | `4 MiB` | Refill threshold for an
eager column after pages are consumed or discarded. |
| `read_ahead_lazy_high_watermark_bytes` | `256 KiB` | Target
compressed-page bytes for each dependency-delayed physical column. |
| `read_ahead_lazy_low_watermark_bytes` | `128 KiB` | Refill threshold for a
lazy column. |
| `read_ahead_max_gap_bytes` | `64 KiB` | Maximum gap crossed while
coalescing foreground page intervals. |
| `read_ahead_max_range_bytes` | `2 MiB` | Maximum base or block-completed
foreground range. |
| `read_ahead_max_read_amplification_ratio` | `2.0` | Maximum foreground
coalescing amplification. |
| `read_ahead_block_fill_min_coverage` | `0.5` | Minimum requested-page
coverage before completing a block in the foreground. |
| `hole_fill_max_gap_bytes` | `32 KiB` | Maximum covered gap crossed while
coalescing block-local holes. |
| `hole_fill_max_range_bytes` | `1 MiB` | Maximum hole-fill GET; reads never
cross a File Cache block. |
| `hole_fill_max_read_amplification_ratio` | `2.0` | Maximum block-local
hole-fill amplification. |
| `hole_fill_max_pending_bytes_per_be` | `256 MiB` | BE-level pending and
active partial-block memory bound. |
| `hole_fill_workers_per_be` | `32` | Dedicated background S3 GET workers. |
#### Implementation structure
The 21 commits are intentionally split by component: generic range
coalescing, cache-aware planning, concurrent range execution, exact cache
reads, column windows, segment coordination, query ownership,
physical/nested-column integration, fixed-block handoff, consumed-range
dispatch, block-local hole planning, background writeback, BE lifecycle wiring,
production writeback routing, and option-validation ownership. Each core
algorithm and concurrency boundary has focused BE unit tests.
### Release note
Add an opt-in range read-ahead and File Cache writeback pipeline for cold
cloud queries. The feature is disabled by default.
### Check List (For Author)
- Test
- [ ] Regression test
- [x] Unit Test
- `./run-be-ut.sh -j100 --run
'--filter=FileRangeCoalescerTest.*:FileRangePlannerTest.*:AsyncFileRangeReaderTest.*:ColumnReadAheadTest.*:SegmentReadAheadTest.*:SegmentIteratorReadAheadTest.*:RangeWritebackDispatcherTest.*:HoleFillPlannerTest.*:RangeCacheWritebackTest.*:PartialBlockWritebackOptionsTest.*:PartialBlockWritebackManagerTest.*:QueryContextReadAheadTest.*:AsyncCachedRemoteFileReaderTest.*:AsyncCacheWriteManagerTest.*:ColumnReaderTest.*ReadAhead*'`
- Result: 137 tests from 15 suites passed under ASAN.
- `build-support/check-format.sh`
- `git diff --check upstream/master...HEAD`
- [ ] Manual test
- [ ] No need to test or manual test.
- Behavior changed:
- [ ] No.
- [x] Yes. When explicitly enabled, eligible cloud query segment reads
use bounded asynchronous range read-ahead and consumed-range File Cache
writeback. The default behavior is unchanged because the feature gate is off.
- Does this need documentation?
- [ ] No.
- [x] Yes. The Phase 2 design is complete; a public
configuration/documentation PR is pending before this Draft becomes ready for
review.
### Draft validation still pending
- End-to-end cloud regression with the feature enabled.
- Cold S3 A/B measurements for query latency, GET count, remote bytes,
memory high-watermark behavior, and File Cache population.
- Public documentation for the new BE configurations.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
--
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]