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

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package storecache
     5  
     6  import (
     7  	"context"
     8  	"reflect"
     9  	"sync"
    10  	"unsafe"
    11  
    12  	"github.com/go-kit/log"
    13  	"github.com/go-kit/log/level"
    14  	lru "github.com/hashicorp/golang-lru/simplelru"
    15  	"github.com/oklog/ulid"
    16  	"github.com/pkg/errors"
    17  	"github.com/prometheus/client_golang/prometheus"
    18  	"github.com/prometheus/client_golang/prometheus/promauto"
    19  	"github.com/prometheus/prometheus/model/labels"
    20  	"github.com/prometheus/prometheus/storage"
    21  	"gopkg.in/yaml.v2"
    22  
    23  	"github.com/thanos-io/thanos/pkg/model"
    24  )
    25  
    26  var (
    27  	DefaultInMemoryIndexCacheConfig = InMemoryIndexCacheConfig{
    28  		MaxSize:     250 * 1024 * 1024,
    29  		MaxItemSize: 125 * 1024 * 1024,
    30  	}
    31  )
    32  
    33  const maxInt = int(^uint(0) >> 1)
    34  
    35  type InMemoryIndexCache struct {
    36  	mtx sync.Mutex
    37  
    38  	logger           log.Logger
    39  	lru              *lru.LRU
    40  	maxSizeBytes     uint64
    41  	maxItemSizeBytes uint64
    42  
    43  	curSize uint64
    44  
    45  	evicted          *prometheus.CounterVec
    46  	added            *prometheus.CounterVec
    47  	current          *prometheus.GaugeVec
    48  	currentSize      *prometheus.GaugeVec
    49  	totalCurrentSize *prometheus.GaugeVec
    50  	overflow         *prometheus.CounterVec
    51  
    52  	commonMetrics *commonMetrics
    53  }
    54  
    55  // InMemoryIndexCacheConfig holds the in-memory index cache config.
    56  type InMemoryIndexCacheConfig struct {
    57  	// MaxSize represents overall maximum number of bytes cache can contain.
    58  	MaxSize model.Bytes `yaml:"max_size"`
    59  	// MaxItemSize represents maximum size of single item.
    60  	MaxItemSize model.Bytes `yaml:"max_item_size"`
    61  }
    62  
    63  // parseInMemoryIndexCacheConfig unmarshals a buffer into a InMemoryIndexCacheConfig with default values.
    64  func parseInMemoryIndexCacheConfig(conf []byte) (InMemoryIndexCacheConfig, error) {
    65  	config := DefaultInMemoryIndexCacheConfig
    66  	if err := yaml.Unmarshal(conf, &config); err != nil {
    67  		return InMemoryIndexCacheConfig{}, err
    68  	}
    69  
    70  	return config, nil
    71  }
    72  
    73  // NewInMemoryIndexCache creates a new thread-safe LRU cache for index entries and ensures the total cache
    74  // size approximately does not exceed maxBytes.
    75  func NewInMemoryIndexCache(logger log.Logger, commonMetrics *commonMetrics, reg prometheus.Registerer, conf []byte) (*InMemoryIndexCache, error) {
    76  	config, err := parseInMemoryIndexCacheConfig(conf)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	return NewInMemoryIndexCacheWithConfig(logger, commonMetrics, reg, config)
    82  }
    83  
    84  // NewInMemoryIndexCacheWithConfig creates a new thread-safe LRU cache for index entries and ensures the total cache
    85  // size approximately does not exceed maxBytes.
    86  func NewInMemoryIndexCacheWithConfig(logger log.Logger, commonMetrics *commonMetrics, reg prometheus.Registerer, config InMemoryIndexCacheConfig) (*InMemoryIndexCache, error) {
    87  	if config.MaxItemSize > config.MaxSize {
    88  		return nil, errors.Errorf("max item size (%v) cannot be bigger than overall cache size (%v)", config.MaxItemSize, config.MaxSize)
    89  	}
    90  
    91  	if commonMetrics == nil {
    92  		commonMetrics = newCommonMetrics(reg)
    93  	}
    94  
    95  	c := &InMemoryIndexCache{
    96  		logger:           logger,
    97  		maxSizeBytes:     uint64(config.MaxSize),
    98  		maxItemSizeBytes: uint64(config.MaxItemSize),
    99  		commonMetrics:    commonMetrics,
   100  	}
   101  
   102  	c.evicted = promauto.With(reg).NewCounterVec(prometheus.CounterOpts{
   103  		Name: "thanos_store_index_cache_items_evicted_total",
   104  		Help: "Total number of items that were evicted from the index cache.",
   105  	}, []string{"item_type"})
   106  	c.evicted.WithLabelValues(cacheTypePostings)
   107  	c.evicted.WithLabelValues(cacheTypeSeries)
   108  	c.evicted.WithLabelValues(cacheTypeExpandedPostings)
   109  
   110  	c.added = promauto.With(reg).NewCounterVec(prometheus.CounterOpts{
   111  		Name: "thanos_store_index_cache_items_added_total",
   112  		Help: "Total number of items that were added to the index cache.",
   113  	}, []string{"item_type"})
   114  	c.added.WithLabelValues(cacheTypePostings)
   115  	c.added.WithLabelValues(cacheTypeSeries)
   116  	c.added.WithLabelValues(cacheTypeExpandedPostings)
   117  
   118  	c.commonMetrics.requestTotal.WithLabelValues(cacheTypePostings)
   119  	c.commonMetrics.requestTotal.WithLabelValues(cacheTypeSeries)
   120  	c.commonMetrics.requestTotal.WithLabelValues(cacheTypeExpandedPostings)
   121  
   122  	c.overflow = promauto.With(reg).NewCounterVec(prometheus.CounterOpts{
   123  		Name: "thanos_store_index_cache_items_overflowed_total",
   124  		Help: "Total number of items that could not be added to the cache due to being too big.",
   125  	}, []string{"item_type"})
   126  	c.overflow.WithLabelValues(cacheTypePostings)
   127  	c.overflow.WithLabelValues(cacheTypeSeries)
   128  	c.overflow.WithLabelValues(cacheTypeExpandedPostings)
   129  
   130  	c.commonMetrics.hitsTotal.WithLabelValues(cacheTypePostings)
   131  	c.commonMetrics.hitsTotal.WithLabelValues(cacheTypeSeries)
   132  	c.commonMetrics.hitsTotal.WithLabelValues(cacheTypeExpandedPostings)
   133  
   134  	c.current = promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{
   135  		Name: "thanos_store_index_cache_items",
   136  		Help: "Current number of items in the index cache.",
   137  	}, []string{"item_type"})
   138  	c.current.WithLabelValues(cacheTypePostings)
   139  	c.current.WithLabelValues(cacheTypeSeries)
   140  	c.current.WithLabelValues(cacheTypeExpandedPostings)
   141  
   142  	c.currentSize = promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{
   143  		Name: "thanos_store_index_cache_items_size_bytes",
   144  		Help: "Current byte size of items in the index cache.",
   145  	}, []string{"item_type"})
   146  	c.currentSize.WithLabelValues(cacheTypePostings)
   147  	c.currentSize.WithLabelValues(cacheTypeSeries)
   148  	c.currentSize.WithLabelValues(cacheTypeExpandedPostings)
   149  
   150  	c.totalCurrentSize = promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{
   151  		Name: "thanos_store_index_cache_total_size_bytes",
   152  		Help: "Current byte size of items (both value and key) in the index cache.",
   153  	}, []string{"item_type"})
   154  	c.totalCurrentSize.WithLabelValues(cacheTypePostings)
   155  	c.totalCurrentSize.WithLabelValues(cacheTypeSeries)
   156  	c.totalCurrentSize.WithLabelValues(cacheTypeExpandedPostings)
   157  
   158  	_ = promauto.With(reg).NewGaugeFunc(prometheus.GaugeOpts{
   159  		Name: "thanos_store_index_cache_max_size_bytes",
   160  		Help: "Maximum number of bytes to be held in the index cache.",
   161  	}, func() float64 {
   162  		return float64(c.maxSizeBytes)
   163  	})
   164  	_ = promauto.With(reg).NewGaugeFunc(prometheus.GaugeOpts{
   165  		Name: "thanos_store_index_cache_max_item_size_bytes",
   166  		Help: "Maximum number of bytes for single entry to be held in the index cache.",
   167  	}, func() float64 {
   168  		return float64(c.maxItemSizeBytes)
   169  	})
   170  
   171  	// Initialize LRU cache with a high size limit since we will manage evictions ourselves
   172  	// based on stored size using `RemoveOldest` method.
   173  	l, err := lru.NewLRU(maxInt, c.onEvict)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	c.lru = l
   178  
   179  	level.Info(logger).Log(
   180  		"msg", "created in-memory index cache",
   181  		"maxItemSizeBytes", c.maxItemSizeBytes,
   182  		"maxSizeBytes", c.maxSizeBytes,
   183  		"maxItems", "maxInt",
   184  	)
   185  	return c, nil
   186  }
   187  
   188  func (c *InMemoryIndexCache) onEvict(key, val interface{}) {
   189  	k := key.(cacheKey).keyType()
   190  	entrySize := sliceHeaderSize + uint64(len(val.([]byte)))
   191  
   192  	c.evicted.WithLabelValues(k).Inc()
   193  	c.current.WithLabelValues(k).Dec()
   194  	c.currentSize.WithLabelValues(k).Sub(float64(entrySize))
   195  	c.totalCurrentSize.WithLabelValues(k).Sub(float64(entrySize + key.(cacheKey).size()))
   196  
   197  	c.curSize -= entrySize
   198  }
   199  
   200  func (c *InMemoryIndexCache) get(typ string, key cacheKey) ([]byte, bool) {
   201  	c.commonMetrics.requestTotal.WithLabelValues(typ).Inc()
   202  
   203  	c.mtx.Lock()
   204  	defer c.mtx.Unlock()
   205  
   206  	v, ok := c.lru.Get(key)
   207  	if !ok {
   208  		return nil, false
   209  	}
   210  	c.commonMetrics.hitsTotal.WithLabelValues(typ).Inc()
   211  	return v.([]byte), true
   212  }
   213  
   214  func (c *InMemoryIndexCache) set(typ string, key cacheKey, val []byte) {
   215  	var size = sliceHeaderSize + uint64(len(val))
   216  
   217  	c.mtx.Lock()
   218  	defer c.mtx.Unlock()
   219  
   220  	if _, ok := c.lru.Get(key); ok {
   221  		return
   222  	}
   223  
   224  	if !c.ensureFits(size, typ) {
   225  		c.overflow.WithLabelValues(typ).Inc()
   226  		return
   227  	}
   228  
   229  	// The caller may be passing in a sub-slice of a huge array. Copy the data
   230  	// to ensure we don't waste huge amounts of space for something small.
   231  	v := make([]byte, len(val))
   232  	copy(v, val)
   233  	c.lru.Add(key, v)
   234  
   235  	c.added.WithLabelValues(typ).Inc()
   236  	c.currentSize.WithLabelValues(typ).Add(float64(size))
   237  	c.totalCurrentSize.WithLabelValues(typ).Add(float64(size + key.size()))
   238  	c.current.WithLabelValues(typ).Inc()
   239  	c.curSize += size
   240  }
   241  
   242  // ensureFits tries to make sure that the passed slice will fit into the LRU cache.
   243  // Returns true if it will fit.
   244  func (c *InMemoryIndexCache) ensureFits(size uint64, typ string) bool {
   245  	if size > c.maxItemSizeBytes {
   246  		level.Debug(c.logger).Log(
   247  			"msg", "item bigger than maxItemSizeBytes. Ignoring..",
   248  			"maxItemSizeBytes", c.maxItemSizeBytes,
   249  			"maxSizeBytes", c.maxSizeBytes,
   250  			"curSize", c.curSize,
   251  			"itemSize", size,
   252  			"cacheType", typ,
   253  		)
   254  		return false
   255  	}
   256  
   257  	for c.curSize+size > c.maxSizeBytes {
   258  		if _, _, ok := c.lru.RemoveOldest(); !ok {
   259  			level.Error(c.logger).Log(
   260  				"msg", "LRU has nothing more to evict, but we still cannot allocate the item. Resetting cache.",
   261  				"maxItemSizeBytes", c.maxItemSizeBytes,
   262  				"maxSizeBytes", c.maxSizeBytes,
   263  				"curSize", c.curSize,
   264  				"itemSize", size,
   265  				"cacheType", typ,
   266  			)
   267  			c.reset()
   268  		}
   269  	}
   270  	return true
   271  }
   272  
   273  func (c *InMemoryIndexCache) reset() {
   274  	c.lru.Purge()
   275  	c.current.Reset()
   276  	c.currentSize.Reset()
   277  	c.totalCurrentSize.Reset()
   278  	c.curSize = 0
   279  }
   280  
   281  func copyString(s string) string {
   282  	var b []byte
   283  	h := (*reflect.SliceHeader)(unsafe.Pointer(&b))
   284  	h.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
   285  	h.Len = len(s)
   286  	h.Cap = len(s)
   287  	return string(b)
   288  }
   289  
   290  // copyToKey is required as underlying strings might be mmaped.
   291  func copyToKey(l labels.Label) cacheKeyPostings {
   292  	return cacheKeyPostings(labels.Label{Value: copyString(l.Value), Name: copyString(l.Name)})
   293  }
   294  
   295  // StorePostings sets the postings identified by the ulid and label to the value v,
   296  // if the postings already exists in the cache it is not mutated.
   297  func (c *InMemoryIndexCache) StorePostings(blockID ulid.ULID, l labels.Label, v []byte) {
   298  	c.commonMetrics.dataSizeBytes.WithLabelValues(cacheTypePostings).Observe(float64(len(v)))
   299  	c.set(cacheTypePostings, cacheKey{block: blockID.String(), key: copyToKey(l)}, v)
   300  }
   301  
   302  // FetchMultiPostings fetches multiple postings - each identified by a label -
   303  // and returns a map containing cache hits, along with a list of missing keys.
   304  func (c *InMemoryIndexCache) FetchMultiPostings(_ context.Context, blockID ulid.ULID, keys []labels.Label) (hits map[labels.Label][]byte, misses []labels.Label) {
   305  	hits = map[labels.Label][]byte{}
   306  
   307  	blockIDKey := blockID.String()
   308  	for _, key := range keys {
   309  		if b, ok := c.get(cacheTypePostings, cacheKey{blockIDKey, cacheKeyPostings(key), ""}); ok {
   310  			hits[key] = b
   311  			continue
   312  		}
   313  
   314  		misses = append(misses, key)
   315  	}
   316  
   317  	return hits, misses
   318  }
   319  
   320  // StoreExpandedPostings stores expanded postings for a set of label matchers.
   321  func (c *InMemoryIndexCache) StoreExpandedPostings(blockID ulid.ULID, matchers []*labels.Matcher, v []byte) {
   322  	c.commonMetrics.dataSizeBytes.WithLabelValues(cacheTypeExpandedPostings).Observe(float64(len(v)))
   323  	c.set(cacheTypeExpandedPostings, cacheKey{block: blockID.String(), key: cacheKeyExpandedPostings(labelMatchersToString(matchers))}, v)
   324  }
   325  
   326  // FetchExpandedPostings fetches expanded postings and returns cached data and a boolean value representing whether it is a cache hit or not.
   327  func (c *InMemoryIndexCache) FetchExpandedPostings(_ context.Context, blockID ulid.ULID, matchers []*labels.Matcher) ([]byte, bool) {
   328  	if b, ok := c.get(cacheTypeExpandedPostings, cacheKey{blockID.String(), cacheKeyExpandedPostings(labelMatchersToString(matchers)), ""}); ok {
   329  		return b, true
   330  	}
   331  	return nil, false
   332  }
   333  
   334  // StoreSeries sets the series identified by the ulid and id to the value v,
   335  // if the series already exists in the cache it is not mutated.
   336  func (c *InMemoryIndexCache) StoreSeries(blockID ulid.ULID, id storage.SeriesRef, v []byte) {
   337  	c.commonMetrics.dataSizeBytes.WithLabelValues(cacheTypeSeries).Observe(float64(len(v)))
   338  	c.set(cacheTypeSeries, cacheKey{blockID.String(), cacheKeySeries(id), ""}, v)
   339  }
   340  
   341  // FetchMultiSeries fetches multiple series - each identified by ID - from the cache
   342  // and returns a map containing cache hits, along with a list of missing IDs.
   343  func (c *InMemoryIndexCache) FetchMultiSeries(_ context.Context, blockID ulid.ULID, ids []storage.SeriesRef) (hits map[storage.SeriesRef][]byte, misses []storage.SeriesRef) {
   344  	hits = map[storage.SeriesRef][]byte{}
   345  
   346  	blockIDKey := blockID.String()
   347  	for _, id := range ids {
   348  		if b, ok := c.get(cacheTypeSeries, cacheKey{blockIDKey, cacheKeySeries(id), ""}); ok {
   349  			hits[id] = b
   350  			continue
   351  		}
   352  
   353  		misses = append(misses, id)
   354  	}
   355  
   356  	return hits, misses
   357  }