planner: implement dedicated LRU for plan cache (#37267)
ref pingcap/tidb#36598
This commit is contained in:
@ -29,6 +29,7 @@ go_library(
|
||||
"physical_plans.go",
|
||||
"plan.go",
|
||||
"plan_cache.go",
|
||||
"plan_cache_lru.go",
|
||||
"plan_cache_utils.go",
|
||||
"plan_cost.go",
|
||||
"plan_cost_detail.go",
|
||||
@ -175,6 +176,7 @@ go_test(
|
||||
"partition_pruning_test.go",
|
||||
"physical_plan_test.go",
|
||||
"physical_plan_trace_test.go",
|
||||
"plan_cache_lru_test.go",
|
||||
"plan_cache_test.go",
|
||||
"plan_cache_utils_test.go",
|
||||
"plan_cost_detail_test.go",
|
||||
|
||||
175
planner/core/plan_cache_lru.go
Normal file
175
planner/core/plan_cache_lru.go
Normal file
@ -0,0 +1,175 @@
|
||||
// Package core Copyright 2022 PingCAP, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package core
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
|
||||
"github.com/pingcap/errors"
|
||||
"github.com/pingcap/tidb/types"
|
||||
"github.com/pingcap/tidb/util/hack"
|
||||
"github.com/pingcap/tidb/util/kvcache"
|
||||
)
|
||||
|
||||
// planCacheEntry wraps Key and Value. It's the value of list.Element.
|
||||
type planCacheEntry struct {
|
||||
PlanKey kvcache.Key
|
||||
PlanValue kvcache.Value
|
||||
}
|
||||
|
||||
// LRUPlanCache is a dedicated least recently used cache, JUST use for plan cache.
|
||||
type LRUPlanCache struct {
|
||||
capacity uint
|
||||
size uint
|
||||
// buckets replace the map in general LRU
|
||||
buckets map[string]map[*list.Element]struct{}
|
||||
lruList *list.List
|
||||
// lock make cache thread safe
|
||||
lock sync.Mutex
|
||||
|
||||
// pickFromBucket get one element from bucket. The LRUPlanCache can not work if it is nil
|
||||
pickFromBucket func(map[*list.Element]struct{}, []*types.FieldType) (*list.Element, bool)
|
||||
// onEvict will be called if any eviction happened, only for test use now
|
||||
onEvict func(kvcache.Key, kvcache.Value)
|
||||
}
|
||||
|
||||
// NewLRUPlanCache creates a PCLRUCache object, whose capacity is "capacity".
|
||||
// NOTE: "capacity" should be a positive value.
|
||||
func NewLRUPlanCache(capacity uint,
|
||||
pickFromBucket func(map[*list.Element]struct{}, []*types.FieldType) (*list.Element, bool)) (*LRUPlanCache, error) {
|
||||
if capacity < 1 {
|
||||
return nil, errors.New("capacity of LRU Cache should be at least 1")
|
||||
}
|
||||
return &LRUPlanCache{
|
||||
capacity: capacity,
|
||||
size: 0,
|
||||
buckets: make(map[string]map[*list.Element]struct{}, 1), //Generally one query has one plan
|
||||
lruList: list.New(),
|
||||
pickFromBucket: pickFromBucket,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get tries to find the corresponding value according to the given key.
|
||||
func (l *LRUPlanCache) Get(key kvcache.Key, paramTypes []*types.FieldType) (value kvcache.Value, ok bool) {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
bucket, bucketExist := l.buckets[string(hack.String(key.Hash()))]
|
||||
if bucketExist {
|
||||
if element, exist := l.pickFromBucket(bucket, paramTypes); exist {
|
||||
l.lruList.MoveToFront(element)
|
||||
return element.Value.(*planCacheEntry).PlanValue, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Put puts the (key, value) pair into the LRU Cache.
|
||||
func (l *LRUPlanCache) Put(key kvcache.Key, value kvcache.Value, paramTypes []*types.FieldType) {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
hash := string(key.Hash())
|
||||
bucket, bucketExist := l.buckets[hash]
|
||||
if bucketExist {
|
||||
if element, exist := l.pickFromBucket(bucket, paramTypes); exist {
|
||||
element.Value.(*planCacheEntry).PlanValue = value
|
||||
l.lruList.MoveToFront(element)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
l.buckets[hash] = make(map[*list.Element]struct{}, 1)
|
||||
}
|
||||
|
||||
newCacheEntry := &planCacheEntry{
|
||||
PlanKey: key,
|
||||
PlanValue: value,
|
||||
}
|
||||
element := l.lruList.PushFront(newCacheEntry)
|
||||
l.buckets[hash][element] = struct{}{}
|
||||
l.size++
|
||||
if l.size > l.capacity {
|
||||
l.removeOldest()
|
||||
}
|
||||
}
|
||||
|
||||
// Delete deletes the multi-values from the LRU Cache.
|
||||
func (l *LRUPlanCache) Delete(key kvcache.Key) {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
hash := hack.String(key.Hash())
|
||||
bucket, bucketExist := l.buckets[string(hash)]
|
||||
if bucketExist {
|
||||
for element := range bucket {
|
||||
l.lruList.Remove(element)
|
||||
l.size--
|
||||
}
|
||||
delete(l.buckets, string(hash))
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteAll deletes all elements from the LRU Cache.
|
||||
func (l *LRUPlanCache) DeleteAll() {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
for lru := l.lruList.Back(); lru != nil; lru = l.lruList.Back() {
|
||||
l.lruList.Remove(lru)
|
||||
l.size--
|
||||
}
|
||||
l.buckets = make(map[string]map[*list.Element]struct{}, 1)
|
||||
}
|
||||
|
||||
// Size gets the current cache size.
|
||||
func (l *LRUPlanCache) Size() int {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
return int(l.size)
|
||||
}
|
||||
|
||||
// SetCapacity sets capacity of the cache.
|
||||
func (l *LRUPlanCache) SetCapacity(capacity uint) error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if capacity < 1 {
|
||||
return errors.New("capacity of lru cache should be at least 1")
|
||||
}
|
||||
l.capacity = capacity
|
||||
for l.size > l.capacity {
|
||||
l.removeOldest()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeOldest removes the oldest element from the cache.
|
||||
func (l *LRUPlanCache) removeOldest() {
|
||||
lru := l.lruList.Back()
|
||||
if l.onEvict != nil {
|
||||
l.onEvict(lru.Value.(*planCacheEntry).PlanKey, lru.Value.(*planCacheEntry).PlanValue)
|
||||
}
|
||||
|
||||
l.lruList.Remove(lru)
|
||||
l.removeFromBucket(lru)
|
||||
l.size--
|
||||
}
|
||||
|
||||
// removeFromBucket remove element from bucket
|
||||
func (l *LRUPlanCache) removeFromBucket(element *list.Element) {
|
||||
bucket := l.buckets[string(hack.String(element.Value.(*planCacheEntry).PlanKey.Hash()))]
|
||||
delete(bucket, element)
|
||||
}
|
||||
327
planner/core/plan_cache_lru_test.go
Normal file
327
planner/core/plan_cache_lru_test.go
Normal file
@ -0,0 +1,327 @@
|
||||
// Package core Copyright 2022 PingCAP, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package core
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"testing"
|
||||
|
||||
"github.com/pingcap/tidb/parser/mysql"
|
||||
"github.com/pingcap/tidb/types"
|
||||
"github.com/pingcap/tidb/util/hack"
|
||||
"github.com/pingcap/tidb/util/kvcache"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockCacheKey struct {
|
||||
hash []byte
|
||||
key int64
|
||||
}
|
||||
|
||||
func (mk *mockCacheKey) Hash() []byte {
|
||||
if mk.hash != nil {
|
||||
return mk.hash
|
||||
}
|
||||
mk.hash = make([]byte, 8)
|
||||
for i := uint(0); i < 8; i++ {
|
||||
mk.hash[i] = byte((mk.key >> ((i - 1) * 8)) & 0xff)
|
||||
}
|
||||
return mk.hash
|
||||
}
|
||||
|
||||
func newMockHashKey(key int64) *mockCacheKey {
|
||||
return &mockCacheKey{
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
||||
type fakePlan struct {
|
||||
plan int64
|
||||
tps []*types.FieldType
|
||||
}
|
||||
|
||||
func pickFromBucket(bucket map[*list.Element]struct{}, ptypes []*types.FieldType) (*list.Element, bool) {
|
||||
for element := range bucket {
|
||||
itemsA := element.Value.(*planCacheEntry).PlanValue.(*fakePlan).tps
|
||||
flag := true
|
||||
for j := 0; j < len(itemsA); j++ {
|
||||
if itemsA[j] != ptypes[j] {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if flag {
|
||||
return element, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func TestLRUPCPut(t *testing.T) {
|
||||
// test initialize
|
||||
lruA, errA := NewLRUPlanCache(0, pickFromBucket)
|
||||
require.Nil(t, lruA)
|
||||
require.Error(t, errA, "capacity of LRU Cache should be at least 1")
|
||||
|
||||
maxMemDroppedKv := make(map[kvcache.Key]kvcache.Value)
|
||||
lru, err := NewLRUPlanCache(3, pickFromBucket)
|
||||
lru.onEvict = func(key kvcache.Key, value kvcache.Value) {
|
||||
maxMemDroppedKv[key] = value
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(3), lru.capacity)
|
||||
|
||||
keys := make([]*mockCacheKey, 5)
|
||||
vals := make([]*fakePlan, 5)
|
||||
pTypes := [][]*types.FieldType{{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeEnum)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDate)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeLong)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeInt24)},
|
||||
}
|
||||
|
||||
// one key corresponding to multi values
|
||||
for i := 0; i < 5; i++ {
|
||||
keys[i] = newMockHashKey(1)
|
||||
vals[i] = &fakePlan{
|
||||
plan: int64(i),
|
||||
tps: pTypes[i],
|
||||
}
|
||||
lru.Put(keys[i], vals[i], pTypes[i])
|
||||
}
|
||||
require.Equal(t, lru.size, lru.capacity)
|
||||
require.Equal(t, uint(3), lru.size)
|
||||
|
||||
// test for non-existent elements
|
||||
require.Len(t, maxMemDroppedKv, 2)
|
||||
for i := 0; i < 2; i++ {
|
||||
bucket, exist := lru.buckets[string(hack.String(keys[i].Hash()))]
|
||||
require.True(t, exist)
|
||||
for element := range bucket {
|
||||
require.NotEqual(t, vals[i], element.Value.(*planCacheEntry).PlanValue)
|
||||
}
|
||||
require.Equal(t, vals[i], maxMemDroppedKv[keys[i]])
|
||||
}
|
||||
|
||||
// test for existent elements
|
||||
root := lru.lruList.Front()
|
||||
require.NotNil(t, root)
|
||||
for i := 4; i >= 2; i-- {
|
||||
entry, ok := root.Value.(*planCacheEntry)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, entry)
|
||||
|
||||
// test key
|
||||
key := entry.PlanKey
|
||||
require.NotNil(t, key)
|
||||
require.Equal(t, keys[i], key)
|
||||
|
||||
bucket, exist := lru.buckets[string(hack.String(keys[i].Hash()))]
|
||||
require.True(t, exist)
|
||||
element, exist := lru.pickFromBucket(bucket, pTypes[i])
|
||||
require.NotNil(t, element)
|
||||
require.True(t, exist)
|
||||
require.Equal(t, root, element)
|
||||
|
||||
// test value
|
||||
value, ok := entry.PlanValue.(*fakePlan)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, vals[i], value)
|
||||
|
||||
root = root.Next()
|
||||
}
|
||||
|
||||
// test for end of double-linked list
|
||||
require.Nil(t, root)
|
||||
}
|
||||
|
||||
func TestLRUPCGet(t *testing.T) {
|
||||
lru, err := NewLRUPlanCache(3, pickFromBucket)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := make([]*mockCacheKey, 5)
|
||||
vals := make([]*fakePlan, 5)
|
||||
pTypes := [][]*types.FieldType{{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeEnum)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDate)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeLong)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeInt24)},
|
||||
}
|
||||
// 5 bucket
|
||||
for i := 0; i < 5; i++ {
|
||||
keys[i] = newMockHashKey(int64(i % 4))
|
||||
vals[i] = &fakePlan{
|
||||
plan: int64(i),
|
||||
tps: pTypes[i],
|
||||
}
|
||||
lru.Put(keys[i], vals[i], pTypes[i])
|
||||
}
|
||||
|
||||
// test for non-existent elements
|
||||
for i := 0; i < 2; i++ {
|
||||
value, exists := lru.Get(keys[i], pTypes[i])
|
||||
require.False(t, exists)
|
||||
require.Nil(t, value)
|
||||
}
|
||||
|
||||
for i := 2; i < 5; i++ {
|
||||
value, exists := lru.Get(keys[i], pTypes[i])
|
||||
require.True(t, exists)
|
||||
require.NotNil(t, value)
|
||||
require.Equal(t, vals[i], value)
|
||||
require.Equal(t, uint(3), lru.size)
|
||||
require.Equal(t, uint(3), lru.capacity)
|
||||
|
||||
root := lru.lruList.Front()
|
||||
require.NotNil(t, root)
|
||||
|
||||
entry, ok := root.Value.(*planCacheEntry)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, keys[i], entry.PlanKey)
|
||||
|
||||
value, ok = entry.PlanValue.(*fakePlan)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, vals[i], value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUPCDelete(t *testing.T) {
|
||||
lru, err := NewLRUPlanCache(3, pickFromBucket)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := make([]*mockCacheKey, 3)
|
||||
vals := make([]*fakePlan, 3)
|
||||
pTypes := [][]*types.FieldType{{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeEnum)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDate)},
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
keys[i] = newMockHashKey(int64(i))
|
||||
vals[i] = &fakePlan{
|
||||
plan: int64(i),
|
||||
tps: pTypes[i],
|
||||
}
|
||||
lru.Put(keys[i], vals[i], pTypes[i])
|
||||
}
|
||||
require.Equal(t, 3, int(lru.size))
|
||||
|
||||
lru.Delete(keys[1])
|
||||
value, exists := lru.Get(keys[1], pTypes[1])
|
||||
require.False(t, exists)
|
||||
require.Nil(t, value)
|
||||
require.Equal(t, 2, int(lru.size))
|
||||
|
||||
_, exists = lru.Get(keys[0], pTypes[0])
|
||||
require.True(t, exists)
|
||||
|
||||
_, exists = lru.Get(keys[2], pTypes[2])
|
||||
require.True(t, exists)
|
||||
}
|
||||
|
||||
func TestLRUPCDeleteAll(t *testing.T) {
|
||||
lru, err := NewLRUPlanCache(3, pickFromBucket)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := make([]*mockCacheKey, 3)
|
||||
vals := make([]*fakePlan, 3)
|
||||
pTypes := [][]*types.FieldType{{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeEnum)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDate)},
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
keys[i] = newMockHashKey(int64(i))
|
||||
vals[i] = &fakePlan{
|
||||
plan: int64(i),
|
||||
tps: pTypes[i],
|
||||
}
|
||||
lru.Put(keys[i], vals[i], pTypes[i])
|
||||
}
|
||||
require.Equal(t, 3, int(lru.size))
|
||||
|
||||
lru.DeleteAll()
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
value, exists := lru.Get(keys[i], pTypes[i])
|
||||
require.False(t, exists)
|
||||
require.Nil(t, value)
|
||||
require.Equal(t, 0, int(lru.size))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUPCSetCapacity(t *testing.T) {
|
||||
maxMemDroppedKv := make(map[kvcache.Key]kvcache.Value)
|
||||
lru, err := NewLRUPlanCache(5, pickFromBucket)
|
||||
lru.onEvict = func(key kvcache.Key, value kvcache.Value) {
|
||||
maxMemDroppedKv[key] = value
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint(5), lru.capacity)
|
||||
|
||||
keys := make([]*mockCacheKey, 5)
|
||||
vals := make([]*fakePlan, 5)
|
||||
pTypes := [][]*types.FieldType{{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDouble)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeEnum)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeDate)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeLong)},
|
||||
{types.NewFieldType(mysql.TypeFloat), types.NewFieldType(mysql.TypeInt24)},
|
||||
}
|
||||
|
||||
// one key corresponding to multi values
|
||||
for i := 0; i < 5; i++ {
|
||||
keys[i] = newMockHashKey(1)
|
||||
vals[i] = &fakePlan{
|
||||
plan: int64(i),
|
||||
tps: pTypes[i],
|
||||
}
|
||||
lru.Put(keys[i], vals[i], pTypes[i])
|
||||
}
|
||||
require.Equal(t, lru.size, lru.capacity)
|
||||
require.Equal(t, uint(5), lru.size)
|
||||
|
||||
err = lru.SetCapacity(3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// test for non-existent elements
|
||||
require.Len(t, maxMemDroppedKv, 2)
|
||||
for i := 0; i < 2; i++ {
|
||||
bucket, exist := lru.buckets[string(hack.String(keys[i].Hash()))]
|
||||
require.True(t, exist)
|
||||
for element := range bucket {
|
||||
require.NotEqual(t, vals[i], element.Value.(*planCacheEntry).PlanValue)
|
||||
}
|
||||
require.Equal(t, vals[i], maxMemDroppedKv[keys[i]])
|
||||
}
|
||||
|
||||
// test for existent elements
|
||||
root := lru.lruList.Front()
|
||||
require.NotNil(t, root)
|
||||
for i := 4; i >= 2; i-- {
|
||||
entry, ok := root.Value.(*planCacheEntry)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, entry)
|
||||
|
||||
// test value
|
||||
value, ok := entry.PlanValue.(*fakePlan)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, vals[i], value)
|
||||
|
||||
root = root.Next()
|
||||
}
|
||||
|
||||
// test for end of double-linked list
|
||||
require.Nil(t, root)
|
||||
|
||||
err = lru.SetCapacity(0)
|
||||
require.Error(t, err, "capacity of lru cache should be at least 1")
|
||||
}
|
||||
Reference in New Issue
Block a user