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 }