SSD Prefetch-on-Exist#
Related: RFC #2213, promotion-on-hit (PR #2071).
Goal#
is_exist / batch_is_exist with ExistOptions.prefetch_to_memory = true may
start a best-effort SSD→DRAM promotion for keys that only have a LOCAL_DISK
replica, so a follow-up get() can be served from DRAM. Prefetch must not
change exist/get semantics, must not block the caller, and may be dropped on
any failure. If the disk replica is remote, the holder node reads its own SSD
into its own DRAM.
The feature is off by default (enable_ssd_prefetch = false); with the switch
off the exist/get paths behave exactly as before.
Path#
is_exist(..., prefetch_to_memory=true) # synchronous, no RPC added
→ SsdPrefetcher::triggerSsdPrefetch # throttle.reserve() dedup
→ prefetch thread pool (fixed size 4)
→ 128-key chunks: BatchQueryReadOnly # no lease, no promotion-on-hit
→ classify: SSD-only (LOCAL_DISK complete, no MEMORY), size > 0
→ local holder: RegisterPrefetchTask + FileStorage::PrefetchKeys
→ remote holder: prefetch_offload_object RPC (carries the keys'
tenant; a shared holder promotes under the object's own tenant)
→ holder promotes locally
PrefetchKeys shares the promotion execution chain with promotion-on-hit
(PromoteOneKeyFromLocalDisk: PromotionAllocStart → staging AllocateBatch
→ BatchLoad → PromotionWrite → NotifyPromotionSuccess). It does not
reuse promotion-on-hit admission (frequency sketch, DRAM watermark gate), the
lease-on-query, or the holder’s promotion heartbeat mailbox.
Metadata queries use the read-only admin replica RPCs
(GetReplicaListForAdmin / BatchGetReplicaListForAdmin, also registered on
the master RPC server), so prefetch never grants a lease, never triggers
promotion-on-hit, and never counts valid_get metrics as a side effect.
Master side: RegisterPrefetchTask#
New additive RPC (older masters reject it and prefetch degrades to a no-op). Semantics differ from promotion-on-hit admission:
tenant-aware: the key is looked up under the caller’s tenant;
holder-only:
holder_id == client_id, others getINVALID_PARAMS;no frequency/watermark admission, no heartbeat mailbox push;
shares the
promotion_in_flight_/promotion_queue_limit_cap with promotion-on-hit (both compete for the same DRAM);idempotency is explicit: if a MEMORY replica or an in-flight promotion task already exists, the call fails with
PROMOTION_ALREADY_EXISTS, which the caller treats as “skip quietly”, not as an error. Prefetch and promotion-on-hit therefore cannot double-promote the same key: both paths allocate from the same per-keypromotion_tasksentry.
NotifyPromotionSuccess with from_prefetch = true grants the same read
lease (default_kv_lease_ttl) as exist/get, so the promoted DRAM replica
survives until the follow-up get(). If the exist→get gap can exceed the hard
lease, raise --default_kv_lease_ttl; note that larger leases can raise Put
NO_AVAILABLE_HANDLE under a high DRAM watermark (a capacity trade-off, not a
prefetch bug).
Throttle, threads, failure handling#
Per-client PrefetchThrottle (sharded, lock-free fast path per shard):
ssd_prefetch_dedup_ttl_sec(default 30): a key prefetches at most once per window. Applies at trigger time (before any RPC) so hot probes on DRAM-resident keys do not cause metadata-query storms; the async job still re-classifies precisely afterBatchQueryReadOnly.Failed keys retry after the (shorter) cooldown window, not the full TTL.
ssd_prefetch_cooldown_sec(default 5): when promotion fails withNO_AVAILABLE_HANDLE(DRAM saturated), prefetch backs off so it stops competing with eviction/offload, which are what actually frees DRAM. 0 disables the backoff.All prefetch work runs on a fixed-size thread pool (4 workers). If the pool is down (shutdown), the job is dropped — prefetch never spawns a thread per probe and never falls back to unbounded detached threads. Client teardown drains the pool before releasing resources:
Shutdown()refuses new work, then runs queued and in-flight jobs to completion and joins the workers, because jobs captureshared_ptrs locked at submission and must not outlive the client/segment teardown beneath them.Every failure past
PromotionAllocStartreleases the master-side staged state eagerly (NotifyPromotionFailure, RAII guard) instead of waiting for the reaper TTL.
get() demand-side kick#
Any get whose best replica is LOCAL_DISK — single-key or batched — fires
one TriggerPrefetch(disk_keys, ignore_cooldown=true) before the transfer. The
bypass covers only the client-side throttle backoff — never a master-side
gate (the shared in-flight cap, holder check, and dedup TTL all still
apply). Rationale: an in-flight get is the strongest hotness signal there
is, and a request whose exist-triggered promotion was dropped by a
saturated backoff window should not pay SSD reads with no retry. The kick
is cheap (one dedup lookup + one pool enqueue, no RPC on the caller’s
thread) and never blocks the get: the batch proceeds to the SSD read
immediately, and a promoted replica benefits the next get.
An earlier revision of this design also waited on in-flight promotions from
the get path (ssd_get_wait_ms). Decomposition measurements (store-level
get latency and engine TTFT, pure and mixed batches, DRAM-constrained)
showed the wait never turned the pending get into a DRAM read in the tested
regimes and added latency instead, so the wait was removed and the kick
kept, decoupled from any wait knob. The data is in the PR description.
Measured TTFT results and the A/B methodology (fill → overflow → settle → measure) are in PR #4272’s description.
Detailed design#
The four pieces below are the load-bearing components. Everything here maps one-to-one onto the code named in Code map.
Component architecture#
classDiagram
class SsdPrefetcher {
+TriggerPrefetch(keys)
+RunLocalPrefetch(keys, sizes)
-ThreadPool prefetch_pool_
}
class PrefetchThrottle {
+reserve(keys) keys
+enterCooldown()
+inCooldown() bool
-Shard shards_
}
class FileStorage {
+PrefetchKeys(keys, sizes, dram_pressure, on_key_done)
+LookupLocalObjectSize(key) size
-PromoteOneKeyFromLocalDisk(key, tenant_id, size, dram_pressure)
}
class MasterService {
+RegisterPrefetchTask(client_id, key, tenant_id)
+NotifyPromotionSuccess(client_id, key, tenant_id)
-promotion_tasks
}
class Client {
+RegisterPrefetchTask(key)
+BatchQueryReadOnly(keys)
}
SsdPrefetcher --> PrefetchThrottle
SsdPrefetcher --> Client : metadata and register
SsdPrefetcher --> FileStorage : execute promotion
SsdPrefetcher ..> SsdPrefetcher : prefetch_offload_object RPC to remote holder
Client --> MasterService : additive RPCs
FileStorage --> Client : promotion chain calls
End-to-end trigger flow (exist probe)#
sequenceDiagram
participant E as Engine
participant P as SsdPrefetcher
participant T as PrefetchThrottle
participant M as Master
participant H as Holder
E->>P: TriggerPrefetch(keys), sync, no RPC
P->>T: reserve(keys), TTL dedup for local and remote alike
T-->>P: unseen subset
P->>P: enqueue pool job, caller returns here
loop per 128-key chunk
P->>M: BatchQueryReadOnly, no lease, no promotion, no metrics
M-->>P: replica descriptors
P->>P: ClassifySsdPrefetchRoute, SSD-only, COMPLETE, size above 0
end
alt local holder
P->>H: LookupLocalObjectSize, authoritative
H->>M: RegisterPrefetchTask with holder client_id and tenant
M-->>H: OK, or PROMOTION_ALREADY_EXISTS meaning skip quietly
H->>H: PrefetchKeys then PromoteOneKeyFromLocalDisk
H->>M: NotifyPromotionSuccess, grant read lease from_prefetch
else remote holder
P->>H: prefetch_offload_object RPC, additive, old peers drop
Note over H: holder runs the same local branch<br/>with its own client_id
end
PrefetchThrottle#
Sharded (16) per-key state machine; lazy expiry (per-entry deadline plus an amortized per-shard sweep), so a probe is O(batch) not O(table).
stateDiagram-v2
[*] --> kTriggered : reserve()
kTriggered --> kInFlight : RegisterPrefetchTask OK
kTriggered --> kAlreadyResident : query shows MEMORY
kTriggered --> kDelegated : remote holder RPC sent
kInFlight --> kCompleted : NotifyPromotionSuccess
kInFlight --> kFailed : any step failed
kFailed --> [*] : cooldown-length backoff, then retryable
kCompleted --> [*] : dedup TTL, then re-prefetchable
kAlreadyResident --> [*] : dedup TTL
kDelegated --> [*] : dedup TTL, holder owns execution state
Dedup windows per state (see EntryExpired): healthy states block
re-triggering for ssd_prefetch_dedup_ttl_sec (default 30 s); kFailed
only for ssd_prefetch_cooldown_sec (default 5 s) so transient failures
retry quickly. The memory-pressure cooldown is opened when
PromotionAllocStart returns NO_AVAILABLE_HANDLE: while active,
TriggerPrefetch is a no-op — prefetch (which adds DRAM) yields to
eviction/offload (which frees DRAM).
Master: RegisterPrefetchTask#
flowchart TD
A[client_id, key, tenant] --> B{serving guard?}
B -- no --> X1[UNAVAILABLE_IN_CURRENT_STATUS]
B -- yes --> C{object exists?}
C -- no --> X2[OBJECT_NOT_FOUND]
C -- yes --> D{Put in flight?<br/>InProcessing}
D -- yes --> X3[REPLICA_IS_NOT_READY]
D -- no --> E{MEMORY replica or<br/>promotion task exists?}
E -- yes --> X4[PROMOTION_ALREADY_EXISTS<br/>caller skips quietly]
E -- no --> F{COMPLETE LOCAL_DISK source?}
F -- no --> X3
F -- yes --> G{"holder_id == client_id?"}
G -- no --> X5[INVALID_PARAMS]
G -- yes --> H{promotion_in_flight<br/>queue limit?}
H -- full --> X6[KEYS_ULTRA_LIMIT]
H -- pass --> I[pin source refcnt<br/>emplace PromotionTask from_prefetch=true]
The task deliberately skips promotion-on-hit admission (frequency sketch,
watermark) and the holder’s heartbeat mailbox — an explicit probe is the
hotness signal — but shares the promotion_in_flight_ cap and the
per-key promotion_tasks entry with promotion-on-hit, so the two paths
cannot double-promote a key. NotifyPromotionSuccess on a
from_prefetch task grants the normal default_kv_lease_ttl read lease:
the DRAM replica survives until the follow-up get.
FileStorage execution chain#
PromoteOneKeyFromLocalDisk is shared verbatim by promotion-on-hit
(ProcessPromotionTasks) and prefetch (PrefetchKeys):
sequenceDiagram
participant F as FileStorage
participant C as Client
participant M as Master
F->>C: PromotionAllocStart(key, tenant, size)
alt NO_AVAILABLE_HANDLE
C-->>F: error, set dram_pressure, throttle enters cooldown
end
Note over F: PromotionStateGuard armed (RAII) —<br/>any later failure triggers NotifyPromotionFailure
F->>F: AllocateBatch staging, then BatchLoad SSD read
F->>C: PromotionWrite, TE write into staged replica
F->>C: NotifyPromotionSuccess
C->>M: replica COMPLETE, lease granted from_prefetch
Note over F: guard.Dismiss()
Per-key failures never propagate (best-effort): they are logged, counted on
SsdMetric.prefetch_complete/fail_total, and reported through the
completion callback that drives the throttle states above.
exist/get wiring#
exist:
ExistOptions.prefetch_to_memory+enable_ssd_prefetch→ TriggerPrefetch. Synchronous cost: one dedup lookup + one pool enqueue.get: a batch headed for SSD gets one demand-side kick (see “get() demand-side kick” above) that bypasses only the client-side throttle backoff, then proceeds to the transfer immediately — the get path never waits on promotions.
Rolling-upgrade compatibility#
No existing RPC signature changes.
RegisterPrefetchTaskandprefetch_offload_objectare additive: old masters/holders reject them, and prefetch degrades to a no-op.The read-only replica queries reuse the pre-existing admin RPC handlers.
Configuration#
key |
default |
meaning |
|---|---|---|
|
false |
master switch (client config) |
|
5 |
DRAM-pressure backoff; 0 disables |
|
30 |
per-key trigger dedup TTL; 0 disables |
Requires enable_ssd_offload + ssd_offload_path (prefetch promotes from the
local SSD tier; without offload there are no LOCAL_DISK replicas).
Code map#
prefetch_throttle.h— throttle state machine (sharded).master_service.*—RegisterPrefetchTask,PromotionTask::from_prefetch, prefetch lease inNotifyPromotionSuccess.rpc_service.*/master_client.*/client_service.*— RPC wiring.file_storage.*—PrefetchKeys,PromoteOneKeyFromLocalDisk(shared with promotion-on-hit),LookupLocalObjectSize.storage_backend.*—GetObjectDataSize.ssd_prefetcher.*— trigger/dedup/classify/delegate and the prefetch pool.real_client.*— exist-path trigger, get-side kick.replica.h(ExistOptions),store_c,store_py,dummy_client— API plumbing.