github.com/sequix/cortex@v1.1.6/pkg/chunk/storage/caching_index_client.go (about) 1 package storage 2 3 import ( 4 "context" 5 "sync" 6 "time" 7 8 "github.com/go-kit/kit/log/level" 9 proto "github.com/golang/protobuf/proto" 10 "github.com/prometheus/client_golang/prometheus" 11 "github.com/prometheus/client_golang/prometheus/promauto" 12 "github.com/weaveworks/common/user" 13 14 "github.com/sequix/cortex/pkg/chunk" 15 "github.com/sequix/cortex/pkg/chunk/cache" 16 chunk_util "github.com/sequix/cortex/pkg/chunk/util" 17 "github.com/sequix/cortex/pkg/util" 18 "github.com/sequix/cortex/pkg/util/spanlogger" 19 "github.com/sequix/cortex/pkg/util/validation" 20 ) 21 22 var ( 23 cacheCorruptErrs = promauto.NewCounter(prometheus.CounterOpts{ 24 Name: "querier_index_cache_corruptions_total", 25 Help: "The number of cache corruptions for the index cache.", 26 }) 27 cacheHits = promauto.NewCounter(prometheus.CounterOpts{ 28 Name: "querier_index_cache_hits_total", 29 Help: "The number of cache hits for the index cache.", 30 }) 31 cacheGets = promauto.NewCounter(prometheus.CounterOpts{ 32 Name: "querier_index_cache_gets_total", 33 Help: "The number of gets for the index cache.", 34 }) 35 cachePuts = promauto.NewCounter(prometheus.CounterOpts{ 36 Name: "querier_index_cache_puts_total", 37 Help: "The number of puts for the index cache.", 38 }) 39 cacheEncodeErrs = promauto.NewCounter(prometheus.CounterOpts{ 40 Name: "querier_index_cache_encode_errors_total", 41 Help: "The number of errors for the index cache while encoding the body.", 42 }) 43 ) 44 45 type cachingIndexClient struct { 46 chunk.IndexClient 47 cache cache.Cache 48 validity time.Duration 49 limits *validation.Overrides 50 } 51 52 func newCachingIndexClient(client chunk.IndexClient, c cache.Cache, validity time.Duration, limits *validation.Overrides) chunk.IndexClient { 53 if c == nil { 54 return client 55 } 56 57 return &cachingIndexClient{ 58 IndexClient: client, 59 cache: cache.NewSnappy(c), 60 validity: validity, 61 limits: limits, 62 } 63 } 64 65 func (s *cachingIndexClient) Stop() { 66 s.cache.Stop() 67 } 68 69 func (s *cachingIndexClient) QueryPages(ctx context.Context, queries []chunk.IndexQuery, callback func(chunk.IndexQuery, chunk.ReadBatch) (shouldContinue bool)) error { 70 // We cache the entire row, so filter client side. 71 callback = chunk_util.QueryFilter(callback) 72 73 userID, err := user.ExtractOrgID(ctx) 74 if err != nil { 75 return err 76 } 77 cardinalityLimit := int32(s.limits.CardinalityLimit(userID)) 78 79 // Build list of keys to lookup in the cache. 80 keys := make([]string, 0, len(queries)) 81 queriesByKey := make(map[string][]chunk.IndexQuery, len(queries)) 82 for _, query := range queries { 83 key := queryKey(query) 84 keys = append(keys, key) 85 queriesByKey[key] = append(queriesByKey[key], query) 86 } 87 88 batches, misses := s.cacheFetch(ctx, keys) 89 for _, batch := range batches { 90 if cardinalityLimit > 0 && batch.Cardinality > cardinalityLimit { 91 return chunk.CardinalityExceededError{ 92 Size: batch.Cardinality, 93 Limit: cardinalityLimit, 94 } 95 } 96 97 queries := queriesByKey[batch.Key] 98 for _, query := range queries { 99 callback(query, batch) 100 } 101 } 102 103 if len(misses) == 0 { 104 return nil 105 } 106 107 // Build list of cachable queries for the queries that missed the cache. 108 var ( 109 resultsMtx sync.Mutex 110 results = make(map[string]ReadBatch, len(misses)) 111 cacheableMissed = make([]chunk.IndexQuery, 0, len(misses)) 112 expiryTime = time.Now().Add(s.validity) 113 ) 114 115 for _, key := range misses { 116 // Only need to consider one of the queries as they have the same table & hash. 117 queries := queriesByKey[key] 118 cacheableMissed = append(cacheableMissed, chunk.IndexQuery{ 119 TableName: queries[0].TableName, 120 HashValue: queries[0].HashValue, 121 }) 122 123 rb := ReadBatch{ 124 Key: key, 125 Expiry: expiryTime.UnixNano(), 126 } 127 128 // If the query is cacheable forever, nil the expiry. 129 if queries[0].Immutable { 130 rb.Expiry = 0 131 } 132 133 results[key] = rb 134 } 135 136 err = s.IndexClient.QueryPages(ctx, cacheableMissed, func(cacheableQuery chunk.IndexQuery, r chunk.ReadBatch) bool { 137 resultsMtx.Lock() 138 defer resultsMtx.Unlock() 139 key := queryKey(cacheableQuery) 140 existing := results[key] 141 for iter := r.Iterator(); iter.Next(); { 142 existing.Entries = append(existing.Entries, Entry{Column: iter.RangeValue(), Value: iter.Value()}) 143 } 144 results[key] = existing 145 return true 146 }) 147 if err != nil { 148 return err 149 } 150 151 { 152 resultsMtx.Lock() 153 defer resultsMtx.Unlock() 154 keys := make([]string, 0, len(results)) 155 batches := make([]ReadBatch, 0, len(results)) 156 var cardinalityErr error 157 for key, batch := range results { 158 cardinality := int32(len(batch.Entries)) 159 if cardinalityLimit > 0 && cardinality > cardinalityLimit { 160 batch.Cardinality = cardinality 161 batch.Entries = nil 162 cardinalityErr = chunk.CardinalityExceededError{ 163 Size: cardinality, 164 Limit: cardinalityLimit, 165 } 166 } 167 168 keys = append(keys, key) 169 batches = append(batches, batch) 170 if cardinalityErr != nil { 171 continue 172 } 173 174 queries := queriesByKey[key] 175 for _, query := range queries { 176 callback(query, batch) 177 } 178 } 179 s.cacheStore(ctx, keys, batches) 180 return cardinalityErr 181 } 182 } 183 184 // Iterator implements chunk.ReadBatch. 185 func (b ReadBatch) Iterator() chunk.ReadBatchIterator { 186 return &readBatchIterator{ 187 index: -1, 188 readBatch: b, 189 } 190 } 191 192 type readBatchIterator struct { 193 index int 194 readBatch ReadBatch 195 } 196 197 // Len implements chunk.ReadBatchIterator. 198 func (b *readBatchIterator) Next() bool { 199 b.index++ 200 return b.index < len(b.readBatch.Entries) 201 } 202 203 // RangeValue implements chunk.ReadBatchIterator. 204 func (b *readBatchIterator) RangeValue() []byte { 205 return b.readBatch.Entries[b.index].Column 206 } 207 208 // Value implements chunk.ReadBatchIterator. 209 func (b *readBatchIterator) Value() []byte { 210 return b.readBatch.Entries[b.index].Value 211 } 212 213 func queryKey(q chunk.IndexQuery) string { 214 const sep = "\xff" 215 return q.TableName + sep + q.HashValue 216 } 217 218 func (s *cachingIndexClient) cacheStore(ctx context.Context, keys []string, batches []ReadBatch) { 219 cachePuts.Add(float64(len(keys))) 220 221 // We're doing the hashing to handle unicode and key len properly. 222 // Memcache fails for unicode keys and keys longer than 250 Bytes. 223 hashed := make([]string, 0, len(keys)) 224 bufs := make([][]byte, 0, len(batches)) 225 for i := range keys { 226 hashed = append(hashed, cache.HashKey(keys[i])) 227 out, err := proto.Marshal(&batches[i]) 228 if err != nil { 229 level.Warn(util.Logger).Log("msg", "error marshalling ReadBatch", "err", err) 230 cacheEncodeErrs.Inc() 231 return 232 } 233 bufs = append(bufs, out) 234 } 235 236 s.cache.Store(ctx, hashed, bufs) 237 return 238 } 239 240 func (s *cachingIndexClient) cacheFetch(ctx context.Context, keys []string) (batches []ReadBatch, missed []string) { 241 log, ctx := spanlogger.New(ctx, "cachingIndexClient.cacheFetch") 242 defer log.Finish() 243 244 cacheGets.Add(float64(len(keys))) 245 246 // Build a map from hash -> key; NB there can be collisions here; we'll fetch 247 // the last hash. 248 hashedKeys := make(map[string]string, len(keys)) 249 for _, key := range keys { 250 hashedKeys[cache.HashKey(key)] = key 251 } 252 253 // Build a list of hashes; could be less than keys due to collisions. 254 hashes := make([]string, 0, len(keys)) 255 for hash := range hashedKeys { 256 hashes = append(hashes, hash) 257 } 258 259 // Look up the hashes in a single batch. If we get an error, we just "miss" all 260 // of the keys. Eventually I want to push all the errors to the leafs of the cache 261 // tree, to the caches only return found & missed. 262 foundHashes, bufs, _ := s.cache.Fetch(ctx, hashes) 263 264 // Reverse the hash, unmarshal the index entries, check we got what we expected 265 // and that its still valid. 266 batches = make([]ReadBatch, 0, len(foundHashes)) 267 for j, foundHash := range foundHashes { 268 key := hashedKeys[foundHash] 269 var readBatch ReadBatch 270 271 if err := proto.Unmarshal(bufs[j], &readBatch); err != nil { 272 level.Warn(log).Log("msg", "error unmarshalling index entry from cache", "err", err) 273 cacheCorruptErrs.Inc() 274 continue 275 } 276 277 // Make sure the hash(key) is not a collision in the cache by looking at the 278 // key in the value. 279 if key != readBatch.Key { 280 level.Debug(log).Log("msg", "dropping index cache entry due to key collision", "key", key, "readBatch.Key", readBatch.Key, "expiry") 281 continue 282 } 283 284 if readBatch.Expiry != 0 && time.Now().After(time.Unix(0, readBatch.Expiry)) { 285 continue 286 } 287 288 cacheHits.Inc() 289 batches = append(batches, readBatch) 290 } 291 292 // Finally work out what we're missing. 293 misses := make(map[string]struct{}, len(keys)) 294 for _, key := range keys { 295 misses[key] = struct{}{} 296 } 297 for i := range batches { 298 delete(misses, batches[i].Key) 299 } 300 missed = make([]string, 0, len(misses)) 301 for miss := range misses { 302 missed = append(missed, miss) 303 } 304 305 level.Debug(log).Log("hits", len(batches), "misses", len(misses)) 306 return batches, missed 307 }