-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ff45159
commit 9deaa6a
Showing
7 changed files
with
514 additions
and
0 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
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 | ||
``` |
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,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 | ||
} |
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,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) | ||
} |
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,12 @@ | ||
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,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 | ||
} |
Oops, something went wrong.