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  }