Skip to content

Commit

Permalink
feat: auto tuning gc (#112)
Browse files Browse the repository at this point in the history
* feat: auto tuning gc

* doc: gctuner
  • Loading branch information
joway authored Jan 17, 2022
1 parent 9a8af3e commit 647b4b3
Show file tree
Hide file tree
Showing 9 changed files with 463 additions and 3 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ go 1.16
require (
github.com/stretchr/testify v1.7.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4=
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe h1:W8vbETX/n8S6EmY0Pu4Ix7VvpsJUESTwl0oCK8MJOgk=
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
Expand Down
60 changes: 60 additions & 0 deletions util/gctuner/README.md
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)
```
57 changes: 57 additions & 0 deletions util/gctuner/finalizer.go
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)
}
51 changes: 51 additions & 0 deletions util/gctuner/finalizer_test.go
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)
}
26 changes: 26 additions & 0 deletions util/gctuner/mem.go
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
}
32 changes: 32 additions & 0 deletions util/gctuner/mem_test.go
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
}
141 changes: 141 additions & 0 deletions util/gctuner/tuner.go
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)
}
Loading

0 comments on commit 647b4b3

Please sign in to comment.