Add lazy segment loading for OFFLINE tables#18987
Open
hash-14 wants to merge 3 commits into
Open
Conversation
Servers register a metadata-only stub on ONLINE assignment instead of
downloading the segment; the first query materializes it from the deep
store, and a background sweeper evicts idle segments back to stubs.
Server-side only: brokers and clients are unchanged.
Config:
- Table level: lazyLoadConfig {enabled, idleEvictionSeconds, deleteLocalOnEvict}
- Instance level: pinot.server.instance.lazy.load.enabled / lazy.sweep.interval.seconds / lazy.materialize.parallelism
New server metrics: LAZY_SEGMENT_COLD_LOADS, LAZY_SEGMENT_EVICTIONS,
LAZY_SEGMENT_LOAD_TIME_MS, LAZY_STUBBED_SEGMENT_COUNT
New behavior must be opt-in: operators enable it per instance with pinot.server.instance.lazy.load.enabled=true, and per table with lazyLoadConfig.enabled=true.
Fixes issues found in pre-submission review: - Retry acquire when it races the eviction sweeper (both the map-miss and refcount-failure variants), instead of surfacing a false missing-segment error for a servable segment. - Trigger materialization of stubbed optional segments asynchronously: previously they were silently skipped forever (dropping rows with no error flag); now they warm up for subsequent queries without ever blocking the current one. - Bound the multi-segment materialization wait with a configurable timeout (pinot.server.instance.lazy.materialize.timeout.seconds, default 60); still-stubbed segments are reported missing for that query while downloads continue in the background. Handle interrupts and executor shutdown instead of parking query workers on an untimed, uninterruptible join. A lone cold segment keeps the first-query-pays synchronous path, immune to pool backlog. - Dedup in-flight materializations across concurrent queries so retries share one download instead of stacking duplicate tasks that pin pool workers on the segment lock. - Only register a stub when the deep store holds an authoritative copy (otherwise the OFFLINE->ONLINE transition attempts the load so permanent failures surface as Helix ERROR states) and no local index directory exists in the target-tier or default directory (so restarts warm up from disk instead of resetting hot segments to stubs). Clear stale stub entries when a segment takes the eager path. The deep-store-copy predicate is shared with eviction so the two can never diverge. - Stamp the idle clock on release as well as acquire, so a query running longer than the eviction TTL does not leave its segment instantly evictable.
dbcdd79 to
50b6196
Compare
Author
|
cc @Jackie-Jiang @J-HowHuang @xiangfu0 — tagging you as recent committers to |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds opt-in lazy segment loading for OFFLINE tables: servers register a
metadata-only stub on ONLINE assignment instead of downloading the segment;
the first query that touches the segment materializes it from the deep store,
and a background sweeper evicts segments idle longer than a configurable TTL
back to stubs. Disk/memory then scale with the working set instead of total
assigned data. Entirely server-side — brokers, controllers and clients are
unchanged, and stubbed segments remain ONLINE and routable.
Resolves #18986
Why
Servers currently must hold 100% of every hosted table locally at all times,
so fleets are sized by total data volume rather than working set. Workloads
with a long cold tail (historical/audit data, sparse per-tenant tables) pay
for disk and memory they rarely use. See the linked issue for the full design
discussion.
How
pinot-spi: newLazyLoadConfig(enabled,idleEvictionSeconds,deleteLocalOnEvict) onTableConfig;InstanceDataManagerConfiggainsdefault methods for the three instance-level keys.
pinot-common: ZK ser/de forlazyLoadConfig; new metricsLAZY_SEGMENT_COLD_LOADS,LAZY_SEGMENT_EVICTIONS,LAZY_SEGMENT_LOAD_TIME_MS,LAZY_STUBBED_SEGMENT_COUNT.pinot-segment-local:SegmentDataManager.lastAccessTimeMsstamped onacquire (idle clock); table-config validation restricting the feature to
plain OFFLINE tables (rejected for REALTIME/upsert/dedup/dimension).
pinot-core(BaseTableDataManager): stub registry; transparentmaterialization on
acquireSegment(s)(required and optional segments)under the per-segment lock (concurrent cold queries dedupe into one
download; multi-segment queries materialize in parallel on a bounded,
idle-timeout pool, waiting at most
lazy.materialize.timeout.seconds— still-stubbed segments are reportedmissing for that query while downloads continue in the background); acquire
retries the eviction-sweeper race so no false missing-segment errors; stubs
are registered only when the deep store has an authoritative copy and no
valid local copy exists (restart warms up from disk; permanent deep-store
failures still surface as Helix ERROR states); TTL eviction that never
touches segments with in-flight queries or without a deep-store URL; failed
materialization keeps the stub so the next query retries.
pinot-server:pinot.server.instance.lazy.*config keys; singlelazy-eviction-sweeperdaemon inHelixInstanceDataManager.Feature is off by default and opt-in twice: instance-level
pinot.server.instance.lazy.load.enabled=trueAND per-tablelazyLoadConfig.enabled=true. Tables withoutlazyLoadConfigfollow theexisting eager path exactly (covered by a regression test).
Testing
New
OfflineTableDataManagerLazyLoadTest(TestNG, follows theBaseTableDataManagerTestpattern with afile://deep store):This feature has also been running in production since June 2026 (Pinot 1.5.0
base, MinIO deep store, including a 10 GB / 135M-row scale test).
Known limitations
Called out in the linked issue for design discussion (happy to address in
this PR or fast-follows per reviewer preference): stub sizes are not yet
included in table-size/quota reporting; materialization holds the per-segment
lock (slow downloads can delay Helix transitions on that segment); non-query
REST callers of
acquireSegmentmaterialize stubs as a side effect; reloadpaths skip stubbed segments.