mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-01 18:00:46 +01:00
s3: drop implicit reader cache budget that throttled S3 GETs (#11384)
* fix(filer): leave reader cache unbounded without an explicit budget NewReaderCache silently installed a 256MiB ReaderCacheBudget when the caller passed none. Only weed mount opts into a budget; every other caller (S3 gateway, WebDAV, query engine, mq logstore) inherited the cap. Under ~90 concurrent S3 GETs of medium objects, prefetch wants far more than 64 chunk buffers, so reserve() serialized chunk fetches, clients timed out and retried, and the retry re-downloaded chunks the cancelled request had already fetched. A nil budget now means unbounded, restoring the pre-4.47 behavior for callers that never asked for a memory cap; reserve/complete/release are nil-safe. The mount path is unchanged and still enforces -readerCacheSizeMB. Fixes #11380 * feat(s3): expose -s3.readerCacheSizeMB reader buffer budget Operators who want the S3 gateway read path memory-bounded can now opt in: -s3.readerCacheSizeMB on weed filer/server/mini and -readerCacheSizeMB on standalone weed s3, matching the mount flag. The default 0 keeps the unbounded pre-4.47 behavior; a positive value installs a shared ReaderCacheBudget across in-flight and retained chunk buffers for all S3 GETs. * fix(filer): validate chunk size before consulting the reader budget A nil budget returned early and skipped the negative chunkSize check, letting a corrupted size reach mem.Allocate and panic. Also drop the command-specific flag prefix from the S3 validation error since standalone weed s3 exposes the option as -readerCacheSizeMB. * filer: drop chunk buffers once fully consumed ReaderCache retained every completed chunk buffer in the downloaders map until the slot limit evicted it, so buffers lingered after all readers finished with them. Track attached readers on each SingleChunkCacher and remove the cacher when the last reader consumes the buffer to its end. In-flight download deduplication and the prefetch handoff are unchanged: a buffer always survives until fully read, partial reads keep it available, and an attached reader pins a consumed buffer until it detaches. Repeat reads now go through the chunk cache where enabled, or refetch. * filer: drop consumed buffers on last detach, rechecked under cache lock Two review findings on the drop-on-consume change: - Removal only fired when the detaching reader itself reached the chunk end. If the end-reaching reader finished first and the last remaining reader did a partial read or cancelled, the consumed buffer and its budget reservation lingered until eviction. Track a persistent consumed flag instead, so any end-reaching read marks the buffer and the last detach drops it. - remove() checked only map identity, so a reader attaching between the reader count hitting zero and removal could attach to a cacher that was then deleted underneath it. removeConsumed() re-checks identity, readers == 0, and consumed under the ReaderCache lock; a raced attach keeps the cacher and its own detach retries the removal.
This commit is contained in:
1 parent
87ee3b63a2
commit
c72eda50a8
9 files changed
+245
-12
No files matched your search
@@ -33,6 +33,8 @@ type ReaderCache struct {
|
||||
|
||||
type SingleChunkCacher struct {
|
||||
completedTimeNew int64
|
||||
readers int32
|
||||
consumed int32
|
||||
sync.Mutex
|
||||
parent *ReaderCache
|
||||
chunkFileId string
|
||||
@@ -52,9 +54,6 @@ func NewReaderCache(limit int, chunkCache chunk_cache.ChunkCache, lookupFileIdFn
|
||||
if len(budgets) > 0 {
|
||||
budget = budgets[0]
|
||||
}
|
||||
if budget == nil {
|
||||
budget = NewReaderCacheBudget(DefaultReaderCacheMemoryLimit)
|
||||
}
|
||||
return &ReaderCache{
|
||||
limit: limit,
|
||||
budget: budget,
|
||||
@@ -130,6 +129,7 @@ retry:
|
||||
// concurrent destroy() (error eviction here, LRU, or UnCache) cannot
|
||||
// start wg.Wait() on a zero counter while this read is about to register.
|
||||
cacher.wg.Add(1)
|
||||
atomic.AddInt32(&cacher.readers, 1)
|
||||
rc.Unlock()
|
||||
n, err := cacher.readChunkAt(ctx, buffer, offset)
|
||||
if n > 0 || err != nil {
|
||||
@@ -174,6 +174,7 @@ retry:
|
||||
<-cacher.cacheStartedCh
|
||||
rc.downloaders[fileId] = cacher
|
||||
cacher.wg.Add(1)
|
||||
atomic.AddInt32(&cacher.readers, 1)
|
||||
rc.Unlock()
|
||||
|
||||
return cacher.readChunkAt(ctx, buffer, offset)
|
||||
@@ -191,11 +192,32 @@ func (rc *ReaderCache) UnCache(fileId string) {
|
||||
|
||||
func (rc *ReaderCache) remove(downloader *SingleChunkCacher) {
|
||||
rc.Lock()
|
||||
if rc.downloaders[downloader.chunkFileId] == downloader {
|
||||
removed := rc.downloaders[downloader.chunkFileId] == downloader
|
||||
if removed {
|
||||
delete(rc.downloaders, downloader.chunkFileId)
|
||||
}
|
||||
rc.Unlock()
|
||||
downloader.destroy()
|
||||
if removed {
|
||||
downloader.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
// removeConsumed drops a cacher once its buffer was fully read and no
|
||||
// readers remain attached. The checks run under the ReaderCache lock so a
|
||||
// reader attaching at the same time either wins (the cacher stays and that
|
||||
// reader's detach retries the removal) or misses the map and refetches.
|
||||
func (rc *ReaderCache) removeConsumed(downloader *SingleChunkCacher) {
|
||||
rc.Lock()
|
||||
removed := rc.downloaders[downloader.chunkFileId] == downloader &&
|
||||
atomic.LoadInt32(&downloader.readers) == 0 &&
|
||||
atomic.LoadInt32(&downloader.consumed) != 0
|
||||
if removed {
|
||||
delete(rc.downloaders, downloader.chunkFileId)
|
||||
}
|
||||
rc.Unlock()
|
||||
if removed {
|
||||
downloader.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *ReaderCache) destroy() {
|
||||
@@ -333,8 +355,12 @@ func (s *SingleChunkCacher) destroy() {
|
||||
// The ctx parameter allows the reader to cancel its wait (but the download continues
|
||||
// for other readers - see comment in startCaching about shared resource semantics).
|
||||
// The caller must s.wg.Add(1) under the ReaderCache lock before calling; this only releases it.
|
||||
func (s *SingleChunkCacher) readChunkAt(ctx context.Context, buf []byte, offset int64) (int, error) {
|
||||
defer s.wg.Done()
|
||||
func (s *SingleChunkCacher) readChunkAt(ctx context.Context, buf []byte, offset int64) (n int, err error) {
|
||||
defer func() {
|
||||
s.wg.Done()
|
||||
atomic.AddInt32(&s.readers, -1)
|
||||
s.parent.removeConsumed(s)
|
||||
}()
|
||||
|
||||
// Wait for download to complete, but allow reader cancellation.
|
||||
// Prioritize checking done first - if data is already available,
|
||||
@@ -364,5 +390,9 @@ func (s *SingleChunkCacher) readChunkAt(ctx context.Context, buf []byte, offset
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return copy(buf, s.data[offset:]), nil
|
||||
n = copy(buf, s.data[offset:])
|
||||
if offset+int64(n) == int64(len(s.data)) {
|
||||
atomic.StoreInt32(&s.consumed, 1)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
Reference in new issue
Block a user