Add cache eviction low and high watermarks (#8958)

To allow better control the cache eviction process.

Introduce MINIO_CACHE_WATERMARK_LOW and 
MINIO_CACHE_WATERMARK_HIGH env. variables to specify 
when to stop/start cache eviction process. 

Deprecate MINIO_CACHE_EXPIRY environment variable. Cache 
gc sweeps at 30 minute intervals whenever high watermark is
reached to clear least recently accessed entries in the cache
until sufficient space is cleared to reach the low watermark.

Garbage collection uses an adaptive file scoring approach based
on last access time, with greater weights assigned to larger
objects and those with more hits to find the candidates for eviction.

Thanks to @klauspost for this file scoring algorithm

Co-authored-by: Klaus Post <klauspost@minio.io>
This commit is contained in:
poornas
2020-02-23 05:33:39 -08:00
committed by GitHub
parent 51a9d1bdb7
commit 224b4f13b8
18 changed files with 585 additions and 175 deletions

View File

@@ -38,7 +38,8 @@ import (
)
const (
cacheBlkSize = int64(1 * 1024 * 1024)
cacheBlkSize = int64(1 * 1024 * 1024)
cacheGCInterval = time.Minute * 30
)
// CacheStorageInfo - represents total, free capacity of
@@ -174,7 +175,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
if c.isCacheExclude(bucket, object) || c.skipCache() {
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
}
var cc cacheControl
var cc *cacheControl
var cacheObjSize int64
// fetch diskCache if object is currently cached or nearest available cache drive
dcache, err := c.getCacheToLoc(ctx, bucket, object)
@@ -191,8 +192,8 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
}
}
cc = cacheControlOpts(cacheReader.ObjInfo)
if (!cc.isEmpty() && !cc.isStale(cacheReader.ObjInfo.ModTime)) ||
cc.onlyIfCached {
if cc != nil && (!cc.isStale(cacheReader.ObjInfo.ModTime) ||
cc.onlyIfCached) {
// This is a cache hit, mark it so
bytesServed := cacheReader.ObjInfo.Size
if rs != nil {
@@ -259,11 +260,8 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
c.cacheStats.incMiss()
// Since we got here, we are serving the request from backend,
// and also adding the object to the cache.
if !dcache.diskUsageLow() {
select {
case dcache.purgeChan <- struct{}{}:
default:
}
if dcache.diskUsageHigh() {
dcache.incGCCounter()
}
bkReader, bkErr := c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
@@ -330,12 +328,12 @@ func (c *cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string,
if err != nil {
return getObjectInfoFn(ctx, bucket, object, opts)
}
var cc cacheControl
var cc *cacheControl
// if cache control setting is valid, avoid HEAD operation to backend
cachedObjInfo, _, cerr := dcache.Stat(ctx, bucket, object)
if cerr == nil {
cc = cacheControlOpts(cachedObjInfo)
if !cc.isStale(cachedObjInfo.ModTime) {
if cc == nil || (cc != nil && !cc.isStale(cachedObjInfo.ModTime)) {
// This is a cache hit, mark it so
c.cacheStats.incHit()
return cachedObjInfo, nil
@@ -522,15 +520,10 @@ func newCache(config cache.Config) ([]*diskCache, bool, error) {
if quota == 0 {
quota = config.Quota
}
cache, err := newDiskCache(dir, config.Expiry, quota, config.After)
cache, err := newDiskCache(dir, quota, config.After, config.WatermarkLow, config.WatermarkHigh)
if err != nil {
return nil, false, err
}
// Start the purging go-routine for entries that have expired if no migration in progress
if !migrating {
go cache.purge()
}
caches = append(caches, cache)
}
return caches, migrating, nil
@@ -577,13 +570,12 @@ func (c *cacheObjects) migrateCacheFromV1toV2(ctx context.Context) {
}
errCnt := 0
for index, err := range g.Wait() {
for _, err := range g.Wait() {
if err != nil {
errCnt++
logger.LogIf(ctx, err)
continue
}
go c.cache[index].purge()
}
if errCnt > 0 {
@@ -697,5 +689,38 @@ func newServerCacheObjects(ctx context.Context, config cache.Config) (CacheObjec
if migrateSw {
go c.migrateCacheFromV1toV2(ctx)
}
go c.gc(ctx, GlobalServiceDoneCh)
return c, nil
}
func (c *cacheObjects) gc(ctx context.Context, doneCh chan struct{}) {
ticker := time.NewTicker(cacheGCInterval)
var gcLock sync.Mutex
defer ticker.Stop()
for {
select {
case <-doneCh:
return
case <-ticker.C:
if c.migrating {
continue
}
var wg sync.WaitGroup
for _, dcache := range c.cache {
if dcache.gcCount() == 0 {
continue
}
gcLock.Lock()
wg.Add(1)
go func(d *diskCache, l *sync.Mutex) {
defer wg.Done()
d.resetGCCounter()
d.purge(ctx, doneCh)
l.Unlock()
}(dcache, &gcLock)
}
wg.Wait()
}
}
}