Files
tidb/pkg/util/gctuner/memory_limit_tuner_test.go
2024-07-23 09:46:34 +00:00

247 lines
8.5 KiB
Go

// 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 gctuner
import (
"runtime"
"runtime/debug"
"testing"
"time"
"github.com/pingcap/failpoint"
"github.com/pingcap/tidb/pkg/util/intest"
"github.com/pingcap/tidb/pkg/util/memory"
"github.com/stretchr/testify/require"
)
type mockAllocator struct {
m [][]byte
}
func (a *mockAllocator) alloc(bytes int) (handle int) {
sli := make([]byte, bytes)
a.m = append(a.m, sli)
return len(a.m) - 1
}
func (a *mockAllocator) free(handle int) {
a.m[handle] = nil
}
func (a *mockAllocator) freeAll() {
a.m = nil
runtime.GC()
}
func TestGlobalMemoryTuner(t *testing.T) {
require.True(t, intest.InTest)
require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/pkg/util/gctuner/testMemoryLimitTuner", "return(true)"))
defer func() {
require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/pkg/util/gctuner/testMemoryLimitTuner"))
}()
// Close GOGCTuner
gogcTuner := EnableGOGCTuner.Load()
EnableGOGCTuner.Store(false)
defer EnableGOGCTuner.Store(gogcTuner)
memory.ServerMemoryLimit.Store(1 << 30) // 1GB
GlobalMemoryLimitTuner.SetPercentage(0.8) // 1GB * 80% = 800MB
GlobalMemoryLimitTuner.UpdateMemoryLimit()
require.True(t, GlobalMemoryLimitTuner.isValidValueSet.Load())
defer func() {
// If test.count > 1, wait tuning finished.
require.Eventually(t, func() bool {
//nolint: all_revive
return GlobalMemoryLimitTuner.isValidValueSet.Load()
}, 5*time.Second, 100*time.Millisecond)
require.Eventually(t, func() bool {
//nolint: all_revive
return !GlobalMemoryLimitTuner.adjustPercentageInProgress.Load()
}, 5*time.Second, 100*time.Millisecond)
require.Eventually(t, func() bool {
//nolint: all_revive
return !GlobalMemoryLimitTuner.nextGCTriggeredByMemoryLimit.Load()
}, 5*time.Second, 100*time.Millisecond)
}()
allocator := &mockAllocator{}
defer allocator.freeAll()
r := &runtime.MemStats{}
getNowGCNum := func() uint32 {
runtime.ReadMemStats(r)
return r.NumGC
}
checkNextGCEqualMemoryLimit := func() {
runtime.ReadMemStats(r)
nextGC := r.NextGC
memoryLimit := GlobalMemoryLimitTuner.calcMemoryLimit(GlobalMemoryLimitTuner.GetPercentage())
// Refer to golang source code, nextGC = memoryLimit - nonHeapMemory - overageMemory - headroom
require.True(t, nextGC < uint64(memoryLimit))
}
memory600mb := allocator.alloc(600 << 20)
gcNum := getNowGCNum()
memory210mb := allocator.alloc(210 << 20)
require.Eventually(t, func() bool {
return GlobalMemoryLimitTuner.adjustPercentageInProgress.Load() && gcNum < getNowGCNum()
}, 5*time.Second, 100*time.Millisecond)
// Test waiting for reset
require.Eventually(t, func() bool {
return GlobalMemoryLimitTuner.calcMemoryLimit(fallbackPercentage) == debug.SetMemoryLimit(-1)
}, 5*time.Second, 100*time.Millisecond)
gcNum = getNowGCNum()
memory100mb := allocator.alloc(100 << 20)
require.Eventually(t, func() bool {
return gcNum == getNowGCNum()
}, 5*time.Second, 100*time.Millisecond) // No GC
allocator.free(memory210mb)
allocator.free(memory100mb)
runtime.GC()
// Trigger GC in 80% again
require.Eventually(t, func() bool {
return GlobalMemoryLimitTuner.calcMemoryLimit(GlobalMemoryLimitTuner.GetPercentage()) == debug.SetMemoryLimit(-1)
}, 5*time.Second, 100*time.Millisecond)
time.Sleep(100 * time.Millisecond)
gcNum = getNowGCNum()
checkNextGCEqualMemoryLimit()
memory210mb = allocator.alloc(210 << 20)
require.Eventually(t, func() bool {
return gcNum < getNowGCNum()
}, 5*time.Second, 100*time.Millisecond)
allocator.free(memory210mb)
allocator.free(memory600mb)
}
func TestIssue48741(t *testing.T) {
require.True(t, intest.InTest)
// Close GOGCTuner
gogcTuner := EnableGOGCTuner.Load()
EnableGOGCTuner.Store(false)
defer EnableGOGCTuner.Store(gogcTuner)
getMemoryLimitGCTotal := func() int64 {
return memory.MemoryLimitGCTotal.Load()
}
waitingTunningFinishFn := func() {
for GlobalMemoryLimitTuner.adjustPercentageInProgress.Load() {
time.Sleep(10 * time.Millisecond)
}
}
allocator := &mockAllocator{}
defer allocator.freeAll()
checkIfMemoryLimitIsModified := func() {
// Try to trigger GC by 1GB * 80% = 800MB (tidb_server_memory_limit * tidb_server_memory_limit_gc_trigger)
gcNum := getMemoryLimitGCTotal()
memory810mb := allocator.alloc(810 << 20)
require.Eventually(t,
// Wait for the GC triggered by memory810mb
func() bool {
return GlobalMemoryLimitTuner.adjustPercentageInProgress.Load() && gcNum < getMemoryLimitGCTotal()
},
500*time.Millisecond, 100*time.Millisecond)
// update memoryLimit, and sleep 500ms, let t.UpdateMemoryLimit() be called.
memory.ServerMemoryLimit.Store(1500 << 20) // 1.5 GB
time.Sleep(500 * time.Millisecond)
// UpdateMemoryLimit success during tunning.
require.True(t, GlobalMemoryLimitTuner.adjustPercentageInProgress.Load())
require.Equal(t, debug.SetMemoryLimit(-1), int64(1500<<20*80/100))
waitingTunningFinishFn()
// After the GC triggered by memory810mb.
gcNumAfterMemory810mb := getMemoryLimitGCTotal()
memory200mb := allocator.alloc(200 << 20)
time.Sleep(2 * time.Second)
// The heapInUse is less than 1.5GB * 80% = 1.2GB, so the gc will not be triggered.
require.Equal(t, gcNumAfterMemory810mb, getMemoryLimitGCTotal())
memory300mb := allocator.alloc(300 << 20)
require.Eventually(t,
// Wait for the GC triggered by memory300mb
func() bool {
return GlobalMemoryLimitTuner.adjustPercentageInProgress.Load() && gcNumAfterMemory810mb < getMemoryLimitGCTotal()
},
5*time.Second, 100*time.Millisecond)
// Sleep 500ms, let t.UpdateMemoryLimit() be called.
time.Sleep(500 * time.Millisecond)
// The memory limit will be 1.5GB * 110% during tunning.
require.Equal(t, debug.SetMemoryLimit(-1), int64(1500<<20*110/100))
require.True(t, GlobalMemoryLimitTuner.adjustPercentageInProgress.Load())
allocator.free(memory810mb)
allocator.free(memory200mb)
allocator.free(memory300mb)
}
checkIfMemoryLimitNotModified := func() {
// Try to trigger GC by 1GB * 80% = 800MB (tidb_server_memory_limit * tidb_server_memory_limit_gc_trigger)
gcNum := getMemoryLimitGCTotal()
memory810mb := allocator.alloc(810 << 20)
require.Eventually(t,
// Wait for the GC triggered by memory810mb
func() bool {
return GlobalMemoryLimitTuner.adjustPercentageInProgress.Load() && gcNum < getMemoryLimitGCTotal()
},
500*time.Millisecond, 100*time.Millisecond)
// During the process of adjusting the percentage, the memory limit will be set to 1GB * 110% = 1.1GB.
require.Equal(t, debug.SetMemoryLimit(-1), int64(1<<30*110/100))
gcNumAfterMemory810mb := getMemoryLimitGCTotal()
// After the GC triggered by memory810mb.
waitingTunningFinishFn()
require.Eventually(t,
// The GC will be trigged immediately after memoryLimit is set back to 1GB * 80% = 800MB.
func() bool {
return GlobalMemoryLimitTuner.adjustPercentageInProgress.Load() && gcNumAfterMemory810mb < getMemoryLimitGCTotal()
},
2*time.Second, 100*time.Millisecond)
allocator.free(memory810mb)
}
require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/pkg/util/gctuner/mockUpdateGlobalVarDuringAdjustPercentage", "return(true)"))
defer func() {
require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/pkg/util/gctuner/mockUpdateGlobalVarDuringAdjustPercentage"))
}()
memory.ServerMemoryLimit.Store(1 << 30) // 1GB
GlobalMemoryLimitTuner.SetPercentage(0.8) // 1GB * 80% = 800MB
GlobalMemoryLimitTuner.UpdateMemoryLimit()
require.Equal(t, debug.SetMemoryLimit(-1), int64(1<<30*80/100))
checkIfMemoryLimitNotModified()
waitingTunningFinishFn()
checkIfMemoryLimitIsModified()
}
func TestSetMemoryLimit(t *testing.T) {
GlobalMemoryLimitTuner.DisableAdjustMemoryLimit()
memory.ServerMemoryLimit.Store(1 << 30) // 1GB
GlobalMemoryLimitTuner.SetPercentage(0.8) // 1GB * 80% = 800MB
GlobalMemoryLimitTuner.UpdateMemoryLimit()
require.Equal(t, initGOMemoryLimitValue, debug.SetMemoryLimit(-1))
GlobalMemoryLimitTuner.EnableAdjustMemoryLimit()
GlobalMemoryLimitTuner.UpdateMemoryLimit()
require.Equal(t, int64(1<<30*80/100), debug.SetMemoryLimit(-1))
}