Skip to content

Commit

Permalink
feat:gc tuner
Browse files Browse the repository at this point in the history
  • Loading branch information
songzhibin97 committed May 17, 2022
1 parent ff45159 commit 9deaa6a
Show file tree
Hide file tree
Showing 7 changed files with 514 additions and 0 deletions.
67 changes: 67 additions & 0 deletions gctuner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 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)

// Friendly input
gctuner.TuningWithFromHuman("1g")

// Auto
// There may be problems with multiple services in one pod.
gctuner.TuningWithAuto(false) // Is it a container? Incoming Boolean
```
44 changes: 44 additions & 0 deletions gctuner/finalizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package gctuner

import (
"runtime"
"sync/atomic"
)

type finalizerCallback func()

type finalizer struct {
ref *finalizerRef
callback finalizerCallback
stopped int32
}

func (f *finalizer) stop() {
atomic.StoreInt32(&f.stopped, 1)
}

type finalizerRef struct {
parent *finalizer
}

func finalizerHandler(f *finalizerRef) {
// stop calling callback

if atomic.LoadInt32(&f.parent.stopped) == 1 {
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
}
37 changes: 37 additions & 0 deletions gctuner/finalizer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package gctuner

import (
"runtime"
"sync/atomic"
"testing"

"github.com/stretchr/testify/assert"
)

type testState struct {
count int32
}

func TestFinalizer(t *testing.T) {
maxCount := int32(16)
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)
}
12 changes: 12 additions & 0 deletions gctuner/mem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package gctuner

import (
"runtime"
)

var memStats runtime.MemStats

func readMemoryInuse() uint64 {
runtime.ReadMemStats(&memStats)
return memStats.HeapInuse
}
18 changes: 18 additions & 0 deletions gctuner/mem_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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
}
Loading

0 comments on commit 9deaa6a

Please sign in to comment.