Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

*: Support to trigger GC when memory usage is large. #38179

Merged
merged 22 commits into from
Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 27 additions & 0 deletions sessionctx/variable/sysvar.go
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ var defaultSysVars = []*SysVar{
if !on {
gctuner.SetDefaultGOGC()
}
gctuner.GlobalMemoryLimitTuner.UpdateMemoryLimit()
return nil
}},
{Scope: ScopeGlobal, Name: TiDBEnableTelemetry, Value: BoolToOnOff(DefTiDBEnableTelemetry), Type: TypeBool},
Expand Down Expand Up @@ -759,6 +760,7 @@ var defaultSysVars = []*SysVar{
return err
}
memory.ServerMemoryLimit.Store(intVal)
gctuner.GlobalMemoryLimitTuner.UpdateMemoryLimit()
return nil
},
},
Expand Down Expand Up @@ -786,6 +788,31 @@ var defaultSysVars = []*SysVar{
return nil
},
},
{Scope: ScopeGlobal, Name: TiDBServerMemoryLimitGCTrigger, Value: strconv.FormatFloat(DefTiDBServerMemoryLimitGCTrigger, 'f', -1, 64), Type: TypeFloat, MinValue: 0, MaxValue: math.MaxUint64,
GetGlobal: func(s *SessionVars) (string, error) {
return strconv.FormatFloat(gctuner.GlobalMemoryLimitTuner.GetPercentage(), 'f', -1, 64), nil
},
Validation: func(s *SessionVars, normalizedValue string, originalValue string, scope ScopeFlag) (string, error) {
floatValue, err := strconv.ParseFloat(normalizedValue, 64)
if err != nil {
return "", err
}
if floatValue < 0.51 && floatValue > 1 { // 51% ~ 100%
s.StmtCtx.AppendWarning(ErrTruncatedWrongValue.GenWithStackByArgs(TiDBServerMemoryLimitGCTrigger, originalValue))
floatValue = DefTiDBServerMemoryLimitGCTrigger
}
return strconv.FormatFloat(floatValue, 'f', -1, 64), nil
},
SetGlobal: func(s *SessionVars, val string) error {
floatValue, err := strconv.ParseFloat(val, 64)
if err != nil {
return err
}
gctuner.GlobalMemoryLimitTuner.SetPercentage(floatValue)
gctuner.GlobalMemoryLimitTuner.UpdateMemoryLimit()
return nil
},
},
{Scope: ScopeGlobal, Name: TiDBEnableColumnTracking, Value: BoolToOnOff(DefTiDBEnableColumnTracking), Type: TypeBool, GetGlobal: func(s *SessionVars) (string, error) {
return BoolToOnOff(EnableColumnTracking.Load()), nil
}, SetGlobal: func(s *SessionVars, val string) error {
Expand Down
3 changes: 3 additions & 0 deletions sessionctx/variable/tidb_vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,8 @@ const (
TiDBServerMemoryLimit = "tidb_server_memory_limit"
// TiDBServerMemoryLimitSessMinSize indicates the minimal memory used of a session, that becomes a candidate for session kill.
TiDBServerMemoryLimitSessMinSize = "tidb_server_memory_limit_sess_min_size"
// TiDBServerMemoryLimitGCTrigger indicates the gc percentage of the TiDBServerMemoryLimit.
TiDBServerMemoryLimitGCTrigger = "tidb_server_memory_limit_gc_trigger"
// TiDBEnableGOGCTuner is to enable GOGC tuner. it can tuner GOGC
TiDBEnableGOGCTuner = "tidb_enable_gogc_tuner"
)
Expand Down Expand Up @@ -1050,6 +1052,7 @@ const (
DefTiDBOptRangeMaxSize = 0
DefTiDBCostModelVer = 1
DefTiDBServerMemoryLimitSessMinSize = 128 << 20
DefTiDBServerMemoryLimitGCTrigger = 0.7
DefTiDBEnableGOGCTuner = true
)

Expand Down
127 changes: 127 additions & 0 deletions util/gctuner/memory_limit_tuner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// 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 (
"math"
"runtime"
"runtime/debug"
"time"

"github.com/pingcap/failpoint"
"github.com/pingcap/tidb/util"
"github.com/pingcap/tidb/util/memory"
atomicutil "go.uber.org/atomic"
)

// GlobalMemoryLimitTuner only allow one memory limit tuner in one process
var GlobalMemoryLimitTuner = &memoryLimitTuner{}

// Go runtime trigger GC when hit memory limit which managed via runtime/debug.SetMemoryLimit.
// So we can change memory limit dynamically to avoid frequent GC when memory usage is greater than the limit.
type memoryLimitTuner struct {
finalizer *finalizer
isTuning atomicutil.Bool
percentage atomicutil.Float64
waitingReset atomicutil.Bool
nextGCTriggeredByMemoryLimit bool
}

// tuning check the memory nextGC and judge whether this GC is trigger by memory limit.
// Go runtime ensure that it will be called serially.
func (t *memoryLimitTuner) tuning() {
if !t.isTuning.Load() {
return
}
r := &runtime.MemStats{}
runtime.ReadMemStats(r)
gogc := util.GetGOGC()
ratio := float64(100+gogc) / 100
// This `if` checks whether the **last** GC was triggered by MemoryLimit as far as possible.
// If the **last** GC was triggered by MemoryLimit, we'll set MemoryLimit to MAXVALUE to return control back to GOGC
// to avoid frequent GC when memory usage fluctuates above and below MemoryLimit.
// The logic we judge whether the **last** GC was triggered by MemoryLimit is as follows:
// suppose `NextGC` = `HeapInUse * (100 + GOGC) / 100)`,
// - If NextGC < MemoryLimit, the **next** GC will **not** be triggered by MemoryLimit thus we do not care about
// why the **last** GC is triggered. And MemoryLimit will not be reset this time.
// - Only if NextGC >= MemoryLimit , the **next** GC will be triggered by MemoryLimit. Thus, we need to reset
// MemoryLimit after the **next** GC happens if needed.
if float64(r.HeapInuse)*ratio > float64(debug.SetMemoryLimit(-1)) {
if t.nextGCTriggeredByMemoryLimit && t.waitingReset.CompareAndSwap(false, true) {
go func() {
debug.SetMemoryLimit(math.MaxInt64)
resetInterval := 1 * time.Minute // Wait 1 minute and set back, to avoid frequent GC
failpoint.Inject("testMemoryLimitTuner", func(val failpoint.Value) {
if val, ok := val.(bool); val && ok {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why check ok ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For linter, type assertion must be checked (forcetypeassert)

resetInterval = 1 * time.Second
}
})
time.Sleep(resetInterval)
debug.SetMemoryLimit(t.calcMemoryLimit())
for !t.waitingReset.CompareAndSwap(true, false) {
continue
}
}()
}
t.nextGCTriggeredByMemoryLimit = true
} else {
t.nextGCTriggeredByMemoryLimit = false
}
}

func (t *memoryLimitTuner) start() {
t.finalizer = newFinalizer(t.tuning) // start tuning
}

func (t *memoryLimitTuner) stop() {
t.finalizer.stop()
}

// SetPercentage set the percentage for memory limit tuner.
func (t *memoryLimitTuner) SetPercentage(percentage float64) {
t.percentage.Store(percentage)
}

// GetPercentage get the percentage from memory limit tuner.
func (t *memoryLimitTuner) GetPercentage() float64 {
return t.percentage.Load()
}

// UpdateMemoryLimit updates the memory limit.
// This function should be called when `tidb_server_memory_limit` or `tidb_server_memory_limit_gc_trigger` is modified.
func (t *memoryLimitTuner) UpdateMemoryLimit() {
var memoryLimit int64 = math.MaxInt64
if !EnableGOGCTuner.Load() {
memoryLimit = t.calcMemoryLimit()
}
if memoryLimit == math.MaxInt64 {
t.isTuning.Store(false)
} else {
t.isTuning.Store(true)
}
debug.SetMemoryLimit(memoryLimit)
}

func (t *memoryLimitTuner) calcMemoryLimit() int64 {
memoryLimit := int64(float64(memory.ServerMemoryLimit.Load()) * t.percentage.Load()) // `tidb_server_memory_limit` * `tidb_server_memory_limit_gc_trigger`
if memoryLimit == 0 {
memoryLimit = math.MaxInt64
}
return memoryLimit
}

func init() {
GlobalMemoryLimitTuner.start()
}
114 changes: 114 additions & 0 deletions util/gctuner/memory_limit_tuner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// 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 (
"math"
"runtime"
"runtime/debug"
"testing"
"time"

"github.com/pingcap/failpoint"
"github.com/pingcap/tidb/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.NoError(t, failpoint.Enable("github.com/pingcap/tidb/util/gctuner/testMemoryLimitTuner", "return(true)"))
defer func() {
require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/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.isTuning.Load())
defer func() {
time.Sleep(1 * time.Second) // If test.count > 1, wait tuning finished.
require.True(t, GlobalMemoryLimitTuner.isTuning.Load())
require.False(t, GlobalMemoryLimitTuner.waitingReset.Load())
require.Equal(t, GlobalMemoryLimitTuner.nextGCTriggeredByMemoryLimit, false)
}()

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()
// In golang source, nextGC = memoryLimit - three parts memory. So check 90%~100% here.
require.True(t, nextGC < uint64(memoryLimit))
require.True(t, nextGC > uint64(memoryLimit)/10*9)
}

memory600mb := allocator.alloc(600 << 20)
gcNum := getNowGCNum()

memory210mb := allocator.alloc(210 << 20)
time.Sleep(100 * time.Millisecond)
require.True(t, GlobalMemoryLimitTuner.waitingReset.Load())
require.True(t, gcNum < getNowGCNum())
// Test waiting for reset
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check whether waitingReset is true here?

time.Sleep(500 * time.Millisecond)
require.Equal(t, int64(math.MaxInt64), debug.SetMemoryLimit(-1))
gcNum = getNowGCNum()
memory100mb := allocator.alloc(100 << 20)
time.Sleep(100 * time.Millisecond)
require.Equal(t, gcNum, getNowGCNum()) // No GC

allocator.free(memory210mb)
allocator.free(memory100mb)
runtime.GC()
// Trigger GC in 80% again
time.Sleep(500 * time.Millisecond)
require.Equal(t, GlobalMemoryLimitTuner.calcMemoryLimit(), debug.SetMemoryLimit(-1))
time.Sleep(100 * time.Millisecond)
gcNum = getNowGCNum()
checkNextGCEqualMemoryLimit()
memory210mb = allocator.alloc(210 << 20)
time.Sleep(100 * time.Millisecond)
require.True(t, gcNum < getNowGCNum())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this GC trigger by memory_limit?

allocator.free(memory210mb)
allocator.free(memory600mb)
}