hash-14 opened a new issue, #18986:
URL: https://github.com/apache/pinot/issues/18986

   # Proposal: Lazy Segment Loading for OFFLINE Tables
   
   ## Motivation
   
   Today a Pinot server downloads every segment it is assigned as soon as the
   Helix OFFLINE→ONLINE transition fires. Local disk (and, once queried, memory)
   must therefore hold 100% of every hosted table at all times, even when large
   parts of the data are cold — accessed rarely or never. For workloads with a
   long cold tail (historical data kept for occasional audits, per-tenant tables
   with sparse access patterns), this forces server fleets to be sized by total
   data volume rather than by working set.
   
   This proposal decouples segment *assignment* from segment *materialization*:
   a server can hold a metadata-only stub for an assigned segment and fetch the
   actual data from the deep store only when a query first touches it, evicting
   it back to a stub after a configurable idle period. Disk and memory then 
scale
   with the working set, while the deep store remains the authoritative copy.
   
   We have been running this in production on a fork of Pinot 1.5.0
   (`codeaeondev/pinot_lazy`) and would like to contribute it upstream.
   
   ## Design
   
   Entirely **server-side**: brokers, controllers and clients are unchanged. A
   stubbed segment is still reported ONLINE to the cluster and remains routable.
   
   Segment lifecycle on a lazy-enabled table:
   
   ```
   assignment (OFFLINE→ONLINE) ──► STUB           (ZK metadata only; no 
download)
   first query touches segment ──► MATERIALIZING  (download tar from deep 
store, untar, mmap)
                               ──► MATERIALIZED   (serves at native speed)
   idle > idleEvictionSeconds  ──► STUB           (offload, release, optionally 
delete local dir)
   ```
   
   Key mechanics:
   
   - `OfflineTableDataManager.addOnlineSegment` registers a stub (a
     `ConcurrentHashMap<String, SegmentZKMetadata>` in `BaseTableDataManager`)
     instead of calling `addNewOnlineSegment` when lazy loading is enabled — but
     only when the deep store holds an authoritative copy (otherwise the
     OFFLINE→ONLINE transition attempts the load so a permanent failure surfaces
     as a Helix ERROR state, exactly as today) and no local index directory
     exists (so a restarted server warms up from its valid local copy instead of
     resetting hot segments to stubs).
   - `acquireSegment` / `acquireSegments` transparently materialize stubs on the
     query path (required and optional segments alike). Materialization runs
     under the existing per-segment lock, so concurrent cold queries dedupe into
     a single download. Multi-segment queries materialize stubs in parallel on a
     bounded pool (`lazy.materialize.parallelism`, threads idle-timeout to zero)
     and wait at most `lazy.materialize.timeout.seconds` (default 60): segments
     still stubbed after the timeout are reported missing for that query while
     their downloads continue in the background, so query workers are never
     parked indefinitely behind a cold-cache burst. In-flight materializations
     are dedup'd across queries, so retries share the same download instead of
     stacking duplicate tasks on the pool. Optional segments never block the
     query: their downloads are fired asynchronously (best-effort for the
     current query, warm for the next).
   - Acquire retries the rare race with the eviction sweeper (eviction registers
     the stub before destroying the loaded segment), so a query never sees a
     false missing-segment error for a segment that is stubbed and servable.
   - ZK metadata is re-fetched at materialization time, so a segment refreshed
     while stubbed loads its new version. Refresh messages received while 
stubbed
     update the stub's metadata.
   - A single scheduled daemon (`lazy-eviction-sweeper`) sweeps all tables every
     `lazy.sweep.interval.seconds` and evicts idle materialized segments back to
     stubs. Every acquire AND release stamps `lastAccessTimeMs` (so a query
     running longer than the TTL does not leave its segment instantly
     evictable); a segment with a query in flight (reference count > 1) is never
     evicted. Segments whose ZK metadata has no authoritative deep-store
     download URL (e.g. peer-download segments) are never evicted.
   - Failed materialization keeps the stub registered and reports the segment
     missing for that query (existing upstream semantics), so the next query
     retries — self-healing.
   - Helix OFFLINE/DROP transitions clear stubs like loaded segments.
   
   ### Scope / safety
   
   Lazy loading is only allowed on plain OFFLINE tables. Table-config validation
   rejects it for REALTIME, upsert, dedup and dimension tables, which rely on
   segments being local at assignment time.
   
   Everything is opt-in twice: an instance-level kill switch **(default off)**
   and a per-table config. Tables without `lazyLoadConfig` behave exactly as
   today (covered by a regression test).
   
   ## Configuration
   
   Table level (`tableIndexConfig` sibling, OFFLINE tables only):
   
   ```json
   "lazyLoadConfig": {
     "enabled": true,
     "idleEvictionSeconds": 3600,
     "deleteLocalOnEvict": true
   }
   ```
   
   `deleteLocalOnEvict: false` keeps the local index directory on eviction for a
   fast re-warm with zero deep-store traffic (two-tier mode: memory freed, disk
   kept).
   
   Instance level (`server.conf`):
   
   ```
   pinot.server.instance.lazy.load.enabled=true        # default false
   pinot.server.instance.lazy.sweep.interval.seconds=60
   pinot.server.instance.lazy.materialize.parallelism=4
   pinot.server.instance.lazy.materialize.timeout.seconds=60
   ```
   
   ## Observability
   
   New server metrics: `LAZY_SEGMENT_COLD_LOADS` (meter),
   `LAZY_SEGMENT_EVICTIONS` (meter), `LAZY_SEGMENT_LOAD_TIME_MS` (timer),
   `LAZY_STUBBED_SEGMENT_COUNT` (per-table gauge).
   
   ## Trade-offs
   
   - First query against a cold segment pays the download+load latency
     (observable via `LAZY_SEGMENT_LOAD_TIME_MS`). This is the fundamental trade
     and the reason the feature is strictly opt-in per table.
   - Deep-store egress increases for segments that oscillate between idle and
     active; `idleEvictionSeconds` and `deleteLocalOnEvict=false` are the tuning
     knobs.
   - The eviction sweeper is a single daemon thread per server, no-op unless
     the instance switch is on.
   
   ## Known limitations / discussion points
   
   Raised here openly so the design discussion can weigh them; we would address
   them in this PR or fast-follows per reviewer preference:
   
   1. **Table-size and quota reporting.** `acquireAllSegments` /
      `getAllSegmentsMetadata` iterate only materialized segments, so evicted
      segments are missing from the server's `/table/{t}/size` responses and
      storage-quota checks under-count lazy tables. Likely fix: include stub
      sizes from their ZK metadata in size/metadata endpoints.
   2. **Materialization holds the per-segment lock.** A slow cold download
      blocks Helix transitions (refresh/offload/drop) on that same segment until
      it completes; those transitions can time out on multi-GB segments. Likely
      fix: a per-segment materialize-in-progress future so waiters block on the
      future rather than the lock, or a dedicated lock for materialization.
   3. **Non-query callers of `acquireSegment` can trigger downloads.** Server
      metadata/tier/debug REST endpoints materialize stubs as a side effect,
      which can re-download evicted segments on monitoring polls. Likely fix: a
      non-materializing accessor for metadata-only callers.
   4. **Reload of stubbed segments.** Reload paths skip stubs today; with
      `deleteLocalOnEvict=false` a `forceDownload` reload does not invalidate an
      evicted segment's local copy. Likely fix: drop the local directory (and
      refresh stub metadata) for stubbed segments during reload.
   5. **The materialize wait is not query-deadline aware.** The multi-segment
      wait uses the fixed instance-level timeout rather than the query's
      remaining deadline (plumbing the deadline into `acquireSegments` is a
      `TableDataManager` interface change). Single-segment materialization
      (`acquireSegment`, the lone-stub fast path, and the rare eviction-race
      re-materialization) is deliberately synchronous first-query-pays with no
      bound.
   
   ## Implementation
   
   The change is contained: 14 files, ~870 lines added (of which 404 are tests),
   3 lines removed, rebased onto current master.
   
   - `pinot-spi`: new `LazyLoadConfig` (`BaseJsonConfig`), `TableConfig` 
plumbing,
     `InstanceDataManagerConfig` default methods (other implementors keep
     compiling).
   - `pinot-common`: `TableConfigSerDeUtils` ZK round-trip, three new metrics.
   - `pinot-segment-local`: `SegmentDataManager.lastAccessTimeMs`,
     `TableConfigUtils.validateLazyLoadConfig`.
   - `pinot-core`: stub registry, materialize-on-acquire and eviction in
     `BaseTableDataManager`; stub registration in `OfflineTableDataManager`.
   - `pinot-server`: instance config keys, eviction sweeper in
     `HelixInstanceDataManager`.
   
   Tests (`OfflineTableDataManagerLazyLoadTest`, TestNG, follows the
   `BaseTableDataManagerTest` pattern with a `file://` deep store): stub on
   assignment + materialize on both acquire paths, concurrent-acquire download
   dedupe (8 threads → 1 download), TTL eviction + reload loop, no eviction 
while
   a query is in flight, instance kill switch, and non-lazy regression.
   
   ## Production experience
   
   Running in production since June 2026 on a ReelMint k3s cluster with a MinIO
   deep store, including a 10 GB / 135M-row scale test. Cold-load and eviction
   behavior matched design; brokers required no changes.
   


-- 
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