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 }