-
Notifications
You must be signed in to change notification settings - Fork 232
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: auto tuning gc * doc: gctuner
- Loading branch information
Showing
9 changed files
with
463 additions
and
3 deletions.
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,60 @@ | ||
# gctuner | ||
|
||
## Introduction | ||
|
||
Inspired | ||
by [How We Saved 70K Cores Across 30 Mission-Critical Services (Large-Scale, Semi-Automated Go GC Tuning @Uber)](https://eng.uber.com/how-we-saved-70k-cores-across-30-mission-critical-services/) | ||
. | ||
|
||
```text | ||
_______________ => limit: host/cgroup memory hard limit | ||
| | | ||
|---------------| => gc_trigger: heap_live + heap_live * GCPercent / 100 | ||
| | | ||
|---------------| | ||
| heap_live | | ||
|_______________| | ||
``` | ||
|
||
Go runtime trigger GC when hit `gc_trigger` which affected by `GCPercent` and `heap_live`. | ||
|
||
Assuming that we have stable traffic, our application will always have 100 MB live heap, so the runtime will trigger GC | ||
once heap hits 200 MB(by default GOGC=100). The heap size will be changed like: `100MB => 200MB => 100MB => 200MB => ...`. | ||
|
||
But in real production, our application may have 4 GB memory resources, so no need to GC so frequently. | ||
|
||
The gctuner helps to change the GOGC(GCPercent) dynamically at runtime, set the appropriate GCPercent according to current | ||
memory usage. | ||
|
||
### How it works | ||
|
||
```text | ||
_______________ => limit: host/cgroup memory hard limit | ||
| | | ||
|---------------| => threshold: increase GCPercent when gc_trigger < threshold | ||
| | | ||
|---------------| => gc_trigger: heap_live + heap_live * GCPercent / 100 | ||
| | | ||
|---------------| | ||
| heap_live | | ||
|_______________| | ||
threshold = inuse + inuse * (gcPercent / 100) | ||
=> gcPercent = (threshold - inuse) / inuse * 100 | ||
if threshold < 2*inuse, so gcPercent < 100, and GC positively to avoid OOM | ||
if threshold > 2*inuse, so gcPercent > 100, and GC negatively to reduce GC times | ||
``` | ||
|
||
## Usage | ||
|
||
The recommended threshold is 70% of the memory limit. | ||
|
||
```go | ||
|
||
// Get mem limit from the host machine or cgroup file. | ||
limit := 4 * 1024 * 1024 * 1024 | ||
threshold := limit * 0.7 | ||
|
||
gctuner.Tuning(threshold) | ||
``` |
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,57 @@ | ||
// Copyright 2022 ByteDance 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" | ||
"sync/atomic" | ||
) | ||
|
||
type finalizerCallback func() | ||
|
||
type finalizer struct { | ||
ref *finalizerRef | ||
callback finalizerCallback | ||
stopped int32 | ||
} | ||
|
||
type finalizerRef struct { | ||
parent *finalizer | ||
} | ||
|
||
func finalizerHandler(f *finalizerRef) { | ||
// stop calling callback | ||
if atomic.LoadInt32(&f.parent.stopped) > 0 { | ||
return | ||
} | ||
f.parent.callback() | ||
runtime.SetFinalizer(f, finalizerHandler) | ||
} | ||
|
||
// newFinalizer return a finalizer object and caller should save it to make sure it will not be gc. | ||
// the go runtime promise the callback function should be called every gc time. | ||
func newFinalizer(callback finalizerCallback) *finalizer { | ||
f := &finalizer{ | ||
callback: callback, | ||
} | ||
f.ref = &finalizerRef{parent: f} | ||
runtime.SetFinalizer(f.ref, finalizerHandler) | ||
f.ref = nil // trigger gc | ||
return f | ||
} | ||
|
||
func (f *finalizer) stop() { | ||
atomic.StoreInt32(&f.stopped, 1) | ||
} |
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,51 @@ | ||
// Copyright 2022 ByteDance 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" | ||
"sync/atomic" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
type testState struct { | ||
count int32 | ||
} | ||
|
||
func TestFinalizer(t *testing.T) { | ||
maxCount := int32(1024) | ||
is := assert.New(t) | ||
state := &testState{} | ||
f := newFinalizer(func() { | ||
n := atomic.AddInt32(&state.count, 1) | ||
if n > maxCount { | ||
t.Fatalf("cannot exec finalizer callback after f has been gc") | ||
} | ||
}) | ||
for i := int32(1); i <= maxCount; i++ { | ||
runtime.GC() | ||
is.Equal(atomic.LoadInt32(&state.count), i) | ||
} | ||
is.Nil(f.ref) | ||
|
||
f.stop() | ||
is.Equal(atomic.LoadInt32(&state.count), maxCount) | ||
runtime.GC() | ||
is.Equal(atomic.LoadInt32(&state.count), maxCount) | ||
runtime.GC() | ||
is.Equal(atomic.LoadInt32(&state.count), maxCount) | ||
} |
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,26 @@ | ||
// Copyright 2022 ByteDance 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" | ||
) | ||
|
||
var memStats runtime.MemStats | ||
|
||
func readMemoryInuse() uint64 { | ||
runtime.ReadMemStats(&memStats) | ||
return memStats.HeapInuse | ||
} |
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,32 @@ | ||
// Copyright 2022 ByteDance 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 ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestMem(t *testing.T) { | ||
is := assert.New(t) | ||
const mb = 1024 * 1024 | ||
|
||
heap := make([]byte, 100*mb+1) | ||
inuse := readMemoryInuse() | ||
t.Logf("mem inuse: %d MB", inuse/mb) | ||
is.GreaterOrEqual(inuse, uint64(100*mb)) | ||
heap[0] = 0 | ||
} |
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,141 @@ | ||
// Copyright 2022 ByteDance 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" | ||
"os" | ||
"runtime/debug" | ||
"strconv" | ||
"sync/atomic" | ||
) | ||
|
||
const ( | ||
MaxGCPercent = 500 | ||
MinGCPercent = 50 | ||
) | ||
|
||
var defaultGCPercent = 100 | ||
|
||
func init() { | ||
gogcEnv := os.Getenv("GOGC") | ||
gogc, err := strconv.ParseInt(gogcEnv, 10, 32) | ||
if err != nil { | ||
return | ||
} | ||
defaultGCPercent = int(gogc) | ||
} | ||
|
||
// Tuning sets the threshold of heap which will be respect by gc tuner. | ||
// When Tuning, the env GOGC will not be take effect. | ||
// threshold: disable tuning if threshold <= 0 | ||
func Tuning(threshold uint64) { | ||
// disable gc tuner if percent is zero | ||
if threshold <= 0 && globalTuner != nil { | ||
globalTuner.stop() | ||
globalTuner = nil | ||
return | ||
} | ||
|
||
if globalTuner == nil { | ||
globalTuner = newTuner(threshold) | ||
return | ||
} | ||
globalTuner.setThreshold(threshold) | ||
} | ||
|
||
// only allow one gc tuner in one process | ||
var globalTuner *tuner = nil | ||
|
||
/* Heap | ||
_______________ => limit: host/cgroup memory hard limit | ||
| | | ||
|---------------| => threshold: increase GCPercent when gc_trigger < threshold | ||
| | | ||
|---------------| => gc_trigger: heap_live + heap_live * GCPercent / 100 | ||
| | | ||
|---------------| | ||
| heap_live | | ||
|_______________| | ||
Go runtime only trigger GC when hit gc_trigger which affected by GCPercent and heap_live. | ||
So we can change GCPercent dynamically to tuning GC performance. | ||
*/ | ||
type tuner struct { | ||
finalizer *finalizer | ||
gcPercent int | ||
threshold uint64 // high water level, in bytes | ||
} | ||
|
||
// tuning check the memory inuse and tune GC percent dynamically. | ||
// Go runtime ensure that it will be called serially. | ||
func (t *tuner) tuning() { | ||
inuse := readMemoryInuse() | ||
threshold := t.getThreshold() | ||
// stop gc tuning | ||
if threshold <= 0 { | ||
return | ||
} | ||
t.setGCPercent(calcGCPercent(inuse, threshold)) | ||
return | ||
} | ||
|
||
// threshold = inuse + inuse * (gcPercent / 100) | ||
// => gcPercent = (threshold - inuse) / inuse * 100 | ||
// if threshold < inuse*2, so gcPercent < 100, and GC positively to avoid OOM | ||
// if threshold > inuse*2, so gcPercent > 100, and GC negatively to reduce GC times | ||
func calcGCPercent(inuse, threshold uint64) int { | ||
// invalid params | ||
if inuse == 0 || threshold == 0 { | ||
return defaultGCPercent | ||
} | ||
// inuse heap larger than threshold, use min percent | ||
if threshold <= inuse { | ||
return MinGCPercent | ||
} | ||
gcPercent := math.Floor(float64(threshold-inuse) / float64(inuse) * 100) | ||
if gcPercent < MinGCPercent { | ||
return MinGCPercent | ||
} else if gcPercent > MaxGCPercent { | ||
return MaxGCPercent | ||
} | ||
return int(gcPercent) | ||
} | ||
|
||
func newTuner(threshold uint64) *tuner { | ||
t := &tuner{ | ||
gcPercent: defaultGCPercent, | ||
threshold: threshold, | ||
} | ||
t.finalizer = newFinalizer(t.tuning) // start tuning | ||
return t | ||
} | ||
|
||
func (t *tuner) stop() { | ||
t.finalizer.stop() | ||
} | ||
|
||
func (t *tuner) setThreshold(threshold uint64) { | ||
atomic.StoreUint64(&t.threshold, threshold) | ||
} | ||
|
||
func (t *tuner) getThreshold() uint64 { | ||
return atomic.LoadUint64(&t.threshold) | ||
} | ||
|
||
func (t *tuner) setGCPercent(percent int) int { | ||
t.gcPercent = percent | ||
return debug.SetGCPercent(percent) | ||
} |
Oops, something went wrong.