From 58aa9acfb04039114aaf8e5197d97b7a5c4582a0 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 2 Oct 2026 09:33:33 -0700 Subject: [PATCH] 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> --- weed/filer/reader_at.go | 6 ++++++ weed/filer/reader_pattern.go | 10 +++++++++- weed/filer/reader_pattern_test.go | 13 +++++++++++++ weed/s3api/s3api_object_handlers.go | 6 ++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/weed/filer/reader_at.go b/weed/filer/reader_at.go index dfc57e88da..73a3ea8e93 100644 --- a/weed/filer/reader_at.go +++ b/weed/filer/reader_at.go @@ -180,6 +180,12 @@ func (c *ChunkReadAt) ReleaseStream() { c.readerCache.releaseStream(&c.stream) } +// PinRandomMode pins this reader to range fetches; used by callers that +// already know the request is a partial read, e.g. a small ranged GET. +func (c *ChunkReadAt) PinRandomMode() { + c.readerPattern.PinRandomMode() +} + func (c *ChunkReadAt) ReadAt(p []byte, offset int64) (n int, err error) { c.readerPattern.MonitorReadAt(offset, len(p)) diff --git a/weed/filer/reader_pattern.go b/weed/filer/reader_pattern.go index 65184722fc..a59694de4b 100644 --- a/weed/filer/reader_pattern.go +++ b/weed/filer/reader_pattern.go @@ -7,6 +7,7 @@ import ( 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 @@ -65,5 +66,12 @@ func (rp *ReaderPattern) MonitorReadAt(offset int64, size int) { } func (rp *ReaderPattern) IsRandomMode() bool { - return atomic.LoadInt64(&rp.isSequentialCounter) < 0 + 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) } diff --git a/weed/filer/reader_pattern_test.go b/weed/filer/reader_pattern_test.go index 0b4bea5c46..142ec5ff35 100644 --- a/weed/filer/reader_pattern_test.go +++ b/weed/filer/reader_pattern_test.go @@ -123,3 +123,16 @@ func TestReaderPatternRangedReadStaysRandom(t *testing.T) { } } + +// A reader pinned random (a known small ranged request) must stay random +// for every buffer of the request, near or far. +func TestReaderPatternPinnedRandom(t *testing.T) { + rp := NewReaderPattern() + rp.PinRandomMode() + for i := int64(0); i < 10; i++ { + rp.MonitorReadAt(i*256*1024, 256*1024) // contiguous from the start + if !rp.IsRandomMode() { + t.Fatalf("pinned reader flipped to sequential at read %d", i+1) + } + } +} diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index 36b19d166f..881111f177 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -1104,6 +1104,12 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R chunkViews := filer.ViewFromVisibleIntervals(visibleIntervals, offset, size) reader := filer.NewChunkReaderAtFromClient(ctx, s3a.readerCache, chunkViews, totalSize, filer.DefaultPrefetchCount) defer reader.ReleaseStream() + // A small ranged request reads back less than a whole chunk; pin random + // mode so every buffer fetch stays a range read instead of downloading + // each covered chunk in full. + if isRangeRequest && size <= filer.SeqTolerance { + reader.PinRandomMode() + } streamPrepTime = time.Since(tStreamPrep) // A cached chunk whose volume server is down, or whose needle was evicted and