-
Notifications
You must be signed in to change notification settings - Fork 5.7k
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
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
4b65e3c
finalizer
wshwsh12 d2f00d2
temp
wshwsh12 9bfe357
Merge remote-tracking branch 'upstream/master' into auto-gc-80
wshwsh12 ec3cf48
temp2
wshwsh12 2506a14
demo
wshwsh12 33dfdb0
fix name
wshwsh12 a2ed5f2
Merge branch 'master' into auto-gc-80
wshwsh12 5cb85ee
fix lint
wshwsh12 65fa37c
add ut
wshwsh12 77870f3
rename
wshwsh12 01116aa
fmt
wshwsh12 73b9678
Merge branch 'master' into auto-gc-80
wshwsh12 f47abc0
add license
wshwsh12 4764c92
address comments
wshwsh12 58fa721
Merge remote-tracking branch 'upstream/master' into auto-gc-80
wshwsh12 035a1e7
clean code
wshwsh12 7b8a72f
address comments
wshwsh12 6cb6a26
clean
wshwsh12 2ebcd0c
fix lint
wshwsh12 2321b61
address comments
wshwsh12 0c39f03
address comments
wshwsh12 fd76ecb
Merge branch 'master' into auto-gc-80
ti-chi-bot File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to check whether |
||
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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why check ok ?
There was a problem hiding this comment.
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)