github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/querycache/query_cache.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package querycache
    12  
    13  import (
    14  	"fmt"
    15  	"math/rand"
    16  
    17  	"github.com/cockroachdb/cockroach/pkg/sql/opt/memo"
    18  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    19  	"github.com/cockroachdb/cockroach/pkg/util/syncutil"
    20  )
    21  
    22  // C is a query cache, keyed on SQL statement strings (which can contain
    23  // placeholders).
    24  //
    25  // A cache can be used by multiple threads in parallel; however each different
    26  // context must use its own Session.
    27  type C struct {
    28  	totalMem int64
    29  
    30  	mu struct {
    31  		syncutil.Mutex
    32  
    33  		availableMem int64
    34  
    35  		// Sentinel list entries. All entries are part of either the used or the
    36  		// free circular list. Any entry in the used list has a corresponding entry
    37  		// in the map. The used list is in MRU order.
    38  		used, free entry
    39  
    40  		// Map with an entry for each used entry.
    41  		m map[string]*entry
    42  	}
    43  }
    44  
    45  // avgCachedSize is used to preallocate the number of "slots" in the cache.
    46  // Specifically, the cache will be able to store at most
    47  // (<size> / avgCachedSize) queries, even if their memory usage is small.
    48  const avgCachedSize = 1024
    49  
    50  // We disallow very large queries from being added to the cache.
    51  const maxCachedSize = 128 * 1024
    52  
    53  // CachedData is the data associated with a cache entry.
    54  type CachedData struct {
    55  	SQL  string
    56  	Memo *memo.Memo
    57  	// PrepareMetadata is set for prepare queries. In this case the memo contains
    58  	// unassigned placeholders. For non-prepared queries, it is nil.
    59  	PrepareMetadata *sqlbase.PrepareMetadata
    60  	// IsCorrelated memoizes whether the query contained correlated
    61  	// subqueries during planning (prior to de-correlation).
    62  	IsCorrelated bool
    63  }
    64  
    65  func (cd *CachedData) memoryEstimate() int64 {
    66  	res := int64(len(cd.SQL)) + cd.Memo.MemoryEstimate()
    67  	if cd.PrepareMetadata != nil {
    68  		res += cd.PrepareMetadata.MemoryEstimate()
    69  	}
    70  	return res
    71  }
    72  
    73  // entry in a circular linked list.
    74  type entry struct {
    75  	CachedData
    76  
    77  	// Linked list pointers.
    78  	prev, next *entry
    79  }
    80  
    81  // clear resets the CachedData in the entry.
    82  func (e *entry) clear() {
    83  	e.CachedData = CachedData{}
    84  }
    85  
    86  // remove removes the entry from the linked list it is part of.
    87  func (e *entry) remove() {
    88  	e.prev.next = e.next
    89  	e.next.prev = e.prev
    90  	e.prev = nil
    91  	e.next = nil
    92  }
    93  
    94  func (e *entry) insertAfter(a *entry) {
    95  	b := a.next
    96  
    97  	e.prev = a
    98  	e.next = b
    99  
   100  	a.next = e
   101  	b.prev = e
   102  }
   103  
   104  // New creates a query cache of the given size.
   105  func New(memorySize int64) *C {
   106  	if memorySize < avgCachedSize {
   107  		memorySize = avgCachedSize
   108  	}
   109  	numEntries := memorySize / avgCachedSize
   110  	c := &C{totalMem: memorySize}
   111  	c.mu.availableMem = memorySize
   112  	c.mu.m = make(map[string]*entry, numEntries)
   113  	entries := make([]entry, numEntries)
   114  	// The used list is empty.
   115  	c.mu.used.next = &c.mu.used
   116  	c.mu.used.prev = &c.mu.used
   117  	// Make a linked list of entries, starting with the sentinel.
   118  	c.mu.free.next = &entries[0]
   119  	c.mu.free.prev = &entries[numEntries-1]
   120  	for i := range entries {
   121  		if i > 0 {
   122  			entries[i].prev = &entries[i-1]
   123  		} else {
   124  			entries[i].prev = &c.mu.free
   125  		}
   126  		if i+1 < len(entries) {
   127  			entries[i].next = &entries[i+1]
   128  		} else {
   129  			entries[i].next = &c.mu.free
   130  		}
   131  	}
   132  	return c
   133  }
   134  
   135  // Find returns the entry for the given query, if it is in the cache.
   136  //
   137  // If any cached data needs to be updated, it must be done via Add. In
   138  // particular, PrepareMetadata in the returned CachedData must not be modified.
   139  func (c *C) Find(session *Session, sql string) (_ CachedData, ok bool) {
   140  	c.mu.Lock()
   141  	defer c.mu.Unlock()
   142  
   143  	e := c.mu.m[sql]
   144  	if e == nil {
   145  		session.registerMiss()
   146  		return CachedData{}, false
   147  	}
   148  	session.registerHit()
   149  	// Move the entry to the front of the used list.
   150  	e.remove()
   151  	e.insertAfter(&c.mu.used)
   152  	return e.CachedData, true
   153  }
   154  
   155  // Add adds an entry to the cache (possibly evicting some other entry). If the
   156  // cache already has a corresponding entry for d.SQL, it is updated.
   157  // Note: d.PrepareMetadata cannot be modified once this method is called.
   158  func (c *C) Add(session *Session, d *CachedData) {
   159  	if session.highMissRatio() {
   160  		// If the recent miss ratio in this session is high, we want to avoid the
   161  		// overhead of moving things in and out of the cache. But we do want the
   162  		// cache to "recover" if the workload becomes cacheable again. So we still
   163  		// add the entry, but only once in a while.
   164  		if session.r == nil {
   165  			session.r = rand.New(rand.NewSource(1 /* seed */))
   166  		}
   167  		if session.r.Intn(100) != 0 {
   168  			return
   169  		}
   170  	}
   171  	mem := d.memoryEstimate()
   172  	if d.SQL == "" || mem > maxCachedSize || mem > c.totalMem {
   173  		return
   174  	}
   175  
   176  	c.mu.Lock()
   177  	defer c.mu.Unlock()
   178  
   179  	e, ok := c.mu.m[d.SQL]
   180  	if ok {
   181  		// The query already exists in the cache.
   182  		e.remove()
   183  		c.mu.availableMem += e.memoryEstimate()
   184  	} else {
   185  		// Get an entry to use for this query.
   186  		e = c.getEntry()
   187  		c.mu.m[d.SQL] = e
   188  	}
   189  
   190  	e.CachedData = *d
   191  
   192  	// Evict more entries if necessary.
   193  	c.makeSpace(mem)
   194  	c.mu.availableMem -= mem
   195  
   196  	// Insert the entry at the front of the used list.
   197  	e.insertAfter(&c.mu.used)
   198  }
   199  
   200  // makeSpace evicts entries from the used list until we have enough free space.
   201  func (c *C) makeSpace(needed int64) {
   202  	for c.mu.availableMem < needed {
   203  		// Evict entries as necessary, putting them in the free list.
   204  		c.evict().insertAfter(&c.mu.free)
   205  	}
   206  }
   207  
   208  // Evicts the last item in the used list.
   209  func (c *C) evict() *entry {
   210  	e := c.mu.used.prev
   211  	if e == &c.mu.used {
   212  		panic("no more used entries")
   213  	}
   214  	e.remove()
   215  	c.mu.availableMem += e.memoryEstimate()
   216  	delete(c.mu.m, e.SQL)
   217  	e.clear()
   218  
   219  	return e
   220  }
   221  
   222  // getEntry returns an entry that can be used for adding a new query to the
   223  // cache. If there are free entries, one is returned; otherwise, a used entry is
   224  // evicted.
   225  func (c *C) getEntry() *entry {
   226  	if e := c.mu.free.next; e != &c.mu.free {
   227  		e.remove()
   228  		return e
   229  	}
   230  	// No free entries, we must evict an entry.
   231  	return c.evict()
   232  }
   233  
   234  // Clear removes all the entries from the cache.
   235  func (c *C) Clear() {
   236  	c.mu.Lock()
   237  	defer c.mu.Unlock()
   238  
   239  	// Clear the map.
   240  	for sql, e := range c.mu.m {
   241  
   242  		c.mu.availableMem += e.memoryEstimate()
   243  		delete(c.mu.m, sql)
   244  		e.remove()
   245  		e.clear()
   246  		e.insertAfter(&c.mu.free)
   247  	}
   248  }
   249  
   250  // Purge removes the entry for the given query, if it exists.
   251  func (c *C) Purge(sql string) {
   252  	c.mu.Lock()
   253  	defer c.mu.Unlock()
   254  
   255  	if e := c.mu.m[sql]; e != nil {
   256  		c.mu.availableMem += e.memoryEstimate()
   257  		delete(c.mu.m, sql)
   258  		e.clear()
   259  		e.remove()
   260  		e.insertAfter(&c.mu.free)
   261  	}
   262  }
   263  
   264  // check performs various assertions on the internal consistency of the cache
   265  // structures. Used by testing code.
   266  func (c *C) check() {
   267  	c.mu.Lock()
   268  	defer c.mu.Unlock()
   269  
   270  	// Verify that all entries in the used list have a corresponding entry in the
   271  	// map, and that the memory accounting adds up.
   272  	numUsed := 0
   273  	memUsed := int64(0)
   274  	for e := c.mu.used.next; e != &c.mu.used; e = e.next {
   275  		numUsed++
   276  		memUsed += e.memoryEstimate()
   277  		if e.SQL == "" {
   278  			panic(fmt.Sprintf("used entry with empty SQL"))
   279  		}
   280  		if me, ok := c.mu.m[e.SQL]; !ok {
   281  			panic(fmt.Sprintf("used entry %s not in map", e.SQL))
   282  		} else if e != me {
   283  			panic(fmt.Sprintf("map entry for %s doesn't match used entry", e.SQL))
   284  		}
   285  	}
   286  
   287  	if numUsed != len(c.mu.m) {
   288  		panic(fmt.Sprintf("map length %d doesn't match used list size %d", len(c.mu.m), numUsed))
   289  	}
   290  
   291  	if memUsed+c.mu.availableMem != c.totalMem {
   292  		panic(fmt.Sprintf(
   293  			"memory usage doesn't add up: used=%d available=%d total=%d",
   294  			memUsed, c.mu.availableMem, c.totalMem,
   295  		))
   296  	}
   297  }
   298  
   299  // Session stores internal information related to a single session. A session
   300  // cannot be used by multiple threads in parallel.
   301  type Session struct {
   302  	// missRatioMMA is a running average of the recent miss ratio. This is a
   303  	// Modified Moving Average, which is an exponential moving average with factor
   304  	// 1/N. See:
   305  	//   https://en.wikipedia.org/wiki/Moving_average#Modified_moving_average.
   306  	//
   307  	// To avoid unnecessary floating point operations, the value is scaled by
   308  	// mmaScale and stored as an integer (specifically, a value of mmaScale means
   309  	// a 100% miss ratio).
   310  	missRatioMMA int64
   311  
   312  	// Initialized lazily as needed.
   313  	r *rand.Rand
   314  }
   315  
   316  // mmaN is the N factor and is chosen so that the miss ratio doesn't reach the
   317  // threshold until we've seen on the order of a thousand queries (we don't want
   318  // to reach the limit before we even get a chance to fill up the cache).
   319  const mmaN = 1024
   320  const mmaScale = 1000000000
   321  
   322  // Init initializes or resets a Session.
   323  func (s *Session) Init() {
   324  	s.missRatioMMA = 0
   325  	s.r = nil
   326  }
   327  
   328  func (s *Session) registerHit() {
   329  	s.missRatioMMA = s.missRatioMMA * (mmaN - 1) / mmaN
   330  }
   331  
   332  func (s *Session) registerMiss() {
   333  	s.missRatioMMA = (s.missRatioMMA*(mmaN-1) + mmaScale) / mmaN
   334  }
   335  
   336  // highMissRatio returns true if the recent average miss ratio is above a
   337  // certain threshold (80%).
   338  func (s *Session) highMissRatio() bool {
   339  	const threshold = mmaScale * 80 / 100
   340  	return s.missRatioMMA > threshold
   341  }