Files
seaweedfs/weed/filer/reader_pattern.go
T
Chris LuandDevin 58aa9acfb0 s3: pin small ranged GETs to range reads
A ranged GET whose first read lands within SeqTolerance of offset 0 is
judged sequential immediately, and even a far-starting range could flip
back mid-request; either way readChunkSliceAt downloads each covered
chunk in full, multiplying disk reads for small ranged reads (measured
~7x). Pin random mode for ranged requests no larger than SeqTolerance so
all of the request's buffer reads stay range fetches. Larger ranges keep
the dynamic pattern, where whole-chunk fetches amortize.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-02 09:33:33 -07:00

78 lines
2.7 KiB
Go

package filer
import (
"sync/atomic"
)
type ReaderPattern struct {
isSequentialCounter int64
readFrontier int64 // highest (offset+size) observed across reads
pinnedRandom int64 // set when the caller already knows the reads are a partial range
}
const ModeChangeLimit = 3
// SeqTolerance: a read whose start is within this many bytes of the current read
// frontier still counts as sequential. Using a tolerance window rather than
// strict contiguity absorbs reordered/concurrent readahead (multiple ReadAt can
// be in flight at once) while still rejecting far random jumps.
const SeqTolerance = 8 << 20 // 8 MiB
// For streaming read: only cache the first chunk
// For random read: only fetch the requested range, instead of the whole chunk
func NewReaderPattern() *ReaderPattern {
return &ReaderPattern{
isSequentialCounter: 0,
readFrontier: 0,
}
}
func (rp *ReaderPattern) MonitorReadAt(offset int64, size int) {
// Advance the frontier to max(frontier, offset+size) and capture, in the same
// CAS loop, the pre-image this read is judged against. Reading the frontier
// inside the loop (rather than once up front) keeps `diff` below comparing
// against the freshest value even if a concurrent readahead advances the
// frontier while we loop. Lock-free, consistent with the rest of this type.
end := offset + int64(size)
var frontier int64
for {
frontier = atomic.LoadInt64(&rp.readFrontier)
if end <= frontier || atomic.CompareAndSwapInt64(&rp.readFrontier, frontier, end) {
break
}
}
// near = this read starts within SeqTolerance of where reads had reached.
// Hysteresis (the ±ModeChangeLimit counter) keeps a single outlier read from
// flipping the mode.
diff := offset - frontier
if diff < 0 {
diff = -diff
}
counter := atomic.LoadInt64(&rp.isSequentialCounter)
if diff <= SeqTolerance {
if counter < ModeChangeLimit {
atomic.AddInt64(&rp.isSequentialCounter, 1)
}
} else if counter <= 0 {
// Entering random mode is a strong verdict: drop to the bottom of
// the window so the contiguous tail of one ranged request cannot
// flip it back on the next buffer read and pay a whole-chunk fetch.
atomic.StoreInt64(&rp.isSequentialCounter, -ModeChangeLimit)
} else {
atomic.AddInt64(&rp.isSequentialCounter, -1)
}
}
func (rp *ReaderPattern) IsRandomMode() bool {
return atomic.LoadInt64(&rp.pinnedRandom) != 0 || atomic.LoadInt64(&rp.isSequentialCounter) < 0
}
// PinRandomMode pins the pattern to random for readers that already know
// they serve a partial range, e.g. a small ranged GET: contiguous buffer
// reads within the request must not escalate into whole-chunk fetches.
func (rp *ReaderPattern) PinRandomMode() {
atomic.StoreInt64(&rp.pinnedRandom, 1)
}