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 }