github.com/thanos-io/thanos@v0.32.5/pkg/cache/inmemory.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package cache
     5  
     6  import (
     7  	"context"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/go-kit/log"
    12  	"github.com/go-kit/log/level"
    13  	lru "github.com/hashicorp/golang-lru/simplelru"
    14  	"github.com/pkg/errors"
    15  	"github.com/prometheus/client_golang/prometheus"
    16  	"github.com/prometheus/client_golang/prometheus/promauto"
    17  	"gopkg.in/yaml.v2"
    18  
    19  	"github.com/thanos-io/thanos/pkg/model"
    20  )
    21  
    22  var (
    23  	DefaultInMemoryCacheConfig = InMemoryCacheConfig{
    24  		MaxSize:     250 * 1024 * 1024,
    25  		MaxItemSize: 125 * 1024 * 1024,
    26  	}
    27  )
    28  
    29  const (
    30  	maxInt = int(^uint(0) >> 1)
    31  )
    32  
    33  // InMemoryCacheConfig holds the in-memory cache config.
    34  type InMemoryCacheConfig struct {
    35  	// MaxSize represents overall maximum number of bytes cache can contain.
    36  	MaxSize model.Bytes `yaml:"max_size"`
    37  	// MaxItemSize represents maximum size of single item.
    38  	MaxItemSize model.Bytes `yaml:"max_item_size"`
    39  }
    40  
    41  type InMemoryCache struct {
    42  	logger           log.Logger
    43  	maxSizeBytes     uint64
    44  	maxItemSizeBytes uint64
    45  	name             string
    46  
    47  	mtx         sync.Mutex
    48  	curSize     uint64
    49  	lru         *lru.LRU
    50  	evicted     prometheus.Counter
    51  	requests    prometheus.Counter
    52  	hits        prometheus.Counter
    53  	hitsExpired prometheus.Counter
    54  	// The input cache value would be copied to an inmemory array
    55  	// instead of simply using the one sent by the caller.
    56  	added            prometheus.Counter
    57  	current          prometheus.Gauge
    58  	currentSize      prometheus.Gauge
    59  	totalCurrentSize prometheus.Gauge
    60  	overflow         prometheus.Counter
    61  }
    62  
    63  type cacheDataWithTTLWrapper struct {
    64  	data []byte
    65  	// The objects that are over the TTL are not destroyed eagerly.
    66  	// When there is a hit for an item that is over the TTL, the object is removed from the cache
    67  	// and null is returned.
    68  	// There is ongoing effort to integrate TTL within the Hashicorp golang cache itself.
    69  	// This https://github.com/hashicorp/golang-lru/pull/41 can be used here once complete.
    70  	expiryTime time.Time
    71  }
    72  
    73  // parseInMemoryCacheConfig unmarshals a buffer into a InMemoryCacheConfig with default values.
    74  func parseInMemoryCacheConfig(conf []byte) (InMemoryCacheConfig, error) {
    75  	config := DefaultInMemoryCacheConfig
    76  	if err := yaml.Unmarshal(conf, &config); err != nil {
    77  		return InMemoryCacheConfig{}, err
    78  	}
    79  
    80  	return config, nil
    81  }
    82  
    83  // NewInMemoryCache creates a new thread-safe LRU cache and ensures the total cache
    84  // size approximately does not exceed maxBytes.
    85  func NewInMemoryCache(name string, logger log.Logger, reg prometheus.Registerer, conf []byte) (*InMemoryCache, error) {
    86  	config, err := parseInMemoryCacheConfig(conf)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	return NewInMemoryCacheWithConfig(name, logger, reg, config)
    92  }
    93  
    94  // NewInMemoryCacheWithConfig creates a new thread-safe LRU cache and ensures the total cache
    95  // size approximately does not exceed maxBytes.
    96  func NewInMemoryCacheWithConfig(name string, logger log.Logger, reg prometheus.Registerer, config InMemoryCacheConfig) (*InMemoryCache, error) {
    97  	if config.MaxItemSize > config.MaxSize {
    98  		return nil, errors.Errorf("max item size (%v) cannot be bigger than overall cache size (%v)", config.MaxItemSize, config.MaxSize)
    99  	}
   100  
   101  	c := &InMemoryCache{
   102  		logger:           logger,
   103  		maxSizeBytes:     uint64(config.MaxSize),
   104  		maxItemSizeBytes: uint64(config.MaxItemSize),
   105  		name:             name,
   106  	}
   107  
   108  	c.evicted = promauto.With(reg).NewCounter(prometheus.CounterOpts{
   109  		Name:        "thanos_cache_inmemory_items_evicted_total",
   110  		Help:        "Total number of items that were evicted from the inmemory cache.",
   111  		ConstLabels: prometheus.Labels{"name": name},
   112  	})
   113  
   114  	c.added = promauto.With(reg).NewCounter(prometheus.CounterOpts{
   115  		Name:        "thanos_cache_inmemory_items_added_total",
   116  		Help:        "Total number of items that were added to the inmemory cache.",
   117  		ConstLabels: prometheus.Labels{"name": name},
   118  	})
   119  
   120  	c.requests = promauto.With(reg).NewCounter(prometheus.CounterOpts{
   121  		Name:        "thanos_cache_inmemory_requests_total",
   122  		Help:        "Total number of requests to the inmemory cache.",
   123  		ConstLabels: prometheus.Labels{"name": name},
   124  	})
   125  
   126  	c.hitsExpired = promauto.With(reg).NewCounter(prometheus.CounterOpts{
   127  		Name:        "thanos_cache_inmemory_hits_on_expired_data_total",
   128  		Help:        "Total number of requests to the inmemory cache that were a hit but needed to be evicted due to TTL.",
   129  		ConstLabels: prometheus.Labels{"name": name},
   130  	})
   131  
   132  	c.overflow = promauto.With(reg).NewCounter(prometheus.CounterOpts{
   133  		Name:        "thanos_cache_inmemory_items_overflowed_total",
   134  		Help:        "Total number of items that could not be added to the inmemory cache due to being too big.",
   135  		ConstLabels: prometheus.Labels{"name": name},
   136  	})
   137  
   138  	c.hits = promauto.With(reg).NewCounter(prometheus.CounterOpts{
   139  		Name:        "thanos_cache_inmemory_hits_total",
   140  		Help:        "Total number of requests to the inmemory cache that were a hit.",
   141  		ConstLabels: prometheus.Labels{"name": name},
   142  	})
   143  
   144  	c.current = promauto.With(reg).NewGauge(prometheus.GaugeOpts{
   145  		Name:        "thanos_cache_inmemory_items",
   146  		Help:        "Current number of items in the inmemory cache.",
   147  		ConstLabels: prometheus.Labels{"name": name},
   148  	})
   149  
   150  	c.currentSize = promauto.With(reg).NewGauge(prometheus.GaugeOpts{
   151  		Name:        "thanos_cache_inmemory_items_size_bytes",
   152  		Help:        "Current byte size of items in the inmemory cache.",
   153  		ConstLabels: prometheus.Labels{"name": name},
   154  	})
   155  
   156  	c.totalCurrentSize = promauto.With(reg).NewGauge(prometheus.GaugeOpts{
   157  		Name:        "thanos_cache_inmemory_total_size_bytes",
   158  		Help:        "Current byte size of items (both value and key) in the inmemory cache.",
   159  		ConstLabels: prometheus.Labels{"name": name},
   160  	})
   161  
   162  	_ = promauto.With(reg).NewGaugeFunc(prometheus.GaugeOpts{
   163  		Name:        "thanos_cache_inmemory_max_size_bytes",
   164  		Help:        "Maximum number of bytes to be held in the inmemory cache.",
   165  		ConstLabels: prometheus.Labels{"name": name},
   166  	}, func() float64 {
   167  		return float64(c.maxSizeBytes)
   168  	})
   169  	_ = promauto.With(reg).NewGaugeFunc(prometheus.GaugeOpts{
   170  		Name:        "thanos_cache_inmemory_max_item_size_bytes",
   171  		Help:        "Maximum number of bytes for single entry to be held in the inmemory cache.",
   172  		ConstLabels: prometheus.Labels{"name": name},
   173  	}, func() float64 {
   174  		return float64(c.maxItemSizeBytes)
   175  	})
   176  
   177  	// Initialize LRU cache with a high size limit since we will manage evictions ourselves
   178  	// based on stored size using `RemoveOldest` method.
   179  	l, err := lru.NewLRU(maxInt, c.onEvict)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	c.lru = l
   184  
   185  	level.Info(logger).Log(
   186  		"msg", "created in-memory inmemory cache",
   187  		"maxItemSizeBytes", c.maxItemSizeBytes,
   188  		"maxSizeBytes", c.maxSizeBytes,
   189  		"maxItems", "maxInt",
   190  	)
   191  	return c, nil
   192  }
   193  
   194  func (c *InMemoryCache) onEvict(key, val interface{}) {
   195  	keySize := uint64(len(key.(string)))
   196  	entrySize := uint64(len(val.(cacheDataWithTTLWrapper).data))
   197  
   198  	c.evicted.Inc()
   199  	c.current.Dec()
   200  	c.currentSize.Sub(float64(entrySize))
   201  	c.totalCurrentSize.Sub(float64(keySize + entrySize))
   202  
   203  	c.curSize -= entrySize
   204  }
   205  
   206  func (c *InMemoryCache) get(key string) ([]byte, bool) {
   207  	c.requests.Inc()
   208  	c.mtx.Lock()
   209  	defer c.mtx.Unlock()
   210  
   211  	v, ok := c.lru.Get(key)
   212  	if !ok {
   213  		return nil, false
   214  	}
   215  	// If the present time is greater than the TTL for the object from cache, the object will be
   216  	// removed from the cache and a nil will be returned
   217  	if time.Now().After(v.(cacheDataWithTTLWrapper).expiryTime) {
   218  		c.hitsExpired.Inc()
   219  		c.lru.Remove(key)
   220  		return nil, false
   221  	}
   222  	c.hits.Inc()
   223  	return v.(cacheDataWithTTLWrapper).data, true
   224  }
   225  
   226  func (c *InMemoryCache) set(key string, val []byte, ttl time.Duration) {
   227  	var size = uint64(len(val))
   228  	keySize := uint64(len(key))
   229  
   230  	c.mtx.Lock()
   231  	defer c.mtx.Unlock()
   232  
   233  	if _, ok := c.lru.Get(key); ok {
   234  		return
   235  	}
   236  
   237  	if !c.ensureFits(size) {
   238  		c.overflow.Inc()
   239  		return
   240  	}
   241  
   242  	// The caller may be passing in a sub-slice of a huge array. Copy the data
   243  	// to ensure we don't waste huge amounts of space for something small.
   244  	v := make([]byte, len(val))
   245  	copy(v, val)
   246  	c.lru.Add(key, cacheDataWithTTLWrapper{data: v, expiryTime: time.Now().Add(ttl)})
   247  
   248  	c.added.Inc()
   249  	c.currentSize.Add(float64(size))
   250  	c.totalCurrentSize.Add(float64(keySize + size))
   251  	c.current.Inc()
   252  	c.curSize += size
   253  }
   254  
   255  // ensureFits tries to make sure that the passed slice will fit into the LRU cache.
   256  // Returns true if it will fit.
   257  func (c *InMemoryCache) ensureFits(size uint64) bool {
   258  	if size > c.maxItemSizeBytes {
   259  		level.Debug(c.logger).Log(
   260  			"msg", "item bigger than maxItemSizeBytes. Ignoring..",
   261  			"maxItemSizeBytes", c.maxItemSizeBytes,
   262  			"maxSizeBytes", c.maxSizeBytes,
   263  			"curSize", c.curSize,
   264  			"itemSize", size,
   265  		)
   266  		return false
   267  	}
   268  
   269  	for c.curSize+size > c.maxSizeBytes {
   270  		if _, _, ok := c.lru.RemoveOldest(); !ok {
   271  			level.Error(c.logger).Log(
   272  				"msg", "LRU has nothing more to evict, but we still cannot allocate the item. Resetting cache.",
   273  				"maxItemSizeBytes", c.maxItemSizeBytes,
   274  				"maxSizeBytes", c.maxSizeBytes,
   275  				"curSize", c.curSize,
   276  				"itemSize", size,
   277  			)
   278  			c.reset()
   279  		}
   280  	}
   281  	return true
   282  }
   283  
   284  func (c *InMemoryCache) reset() {
   285  	c.lru.Purge()
   286  	c.current.Set(0)
   287  	c.currentSize.Set(0)
   288  	c.totalCurrentSize.Set(0)
   289  	c.curSize = 0
   290  }
   291  
   292  func (c *InMemoryCache) Store(data map[string][]byte, ttl time.Duration) {
   293  	for key, val := range data {
   294  		c.set(key, val, ttl)
   295  	}
   296  }
   297  
   298  // Fetch fetches multiple keys and returns a map containing cache hits
   299  // In case of error, it logs and return an empty cache hits map.
   300  func (c *InMemoryCache) Fetch(ctx context.Context, keys []string) map[string][]byte {
   301  	results := make(map[string][]byte)
   302  	for _, key := range keys {
   303  		if b, ok := c.get(key); ok {
   304  			results[key] = b
   305  		}
   306  	}
   307  	return results
   308  }
   309  
   310  func (c *InMemoryCache) Name() string {
   311  	return c.name
   312  }