github.com/bytedance/gopkg@v0.0.0-20240514070511-01b2cbcf35e1/util/gctuner/tuner.go (about)

     1  // Copyright 2022 ByteDance Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gctuner
    16  
    17  import (
    18  	"math"
    19  	"os"
    20  	"runtime/debug"
    21  	"strconv"
    22  	"sync/atomic"
    23  )
    24  
    25  var (
    26  	maxGCPercent uint32 = 500
    27  	minGCPercent uint32 = 50
    28  )
    29  
    30  var defaultGCPercent uint32 = 100
    31  
    32  func init() {
    33  	gogcEnv := os.Getenv("GOGC")
    34  	gogc, err := strconv.ParseInt(gogcEnv, 10, 32)
    35  	if err != nil {
    36  		return
    37  	}
    38  	defaultGCPercent = uint32(gogc)
    39  }
    40  
    41  // Tuning sets the threshold of heap which will be respect by gc tuner.
    42  // When Tuning, the env GOGC will not be take effect.
    43  // threshold: disable tuning if threshold == 0
    44  func Tuning(threshold uint64) {
    45  	// disable gc tuner if percent is zero
    46  	if threshold <= 0 && globalTuner != nil {
    47  		globalTuner.stop()
    48  		globalTuner = nil
    49  		return
    50  	}
    51  
    52  	if globalTuner == nil {
    53  		globalTuner = newTuner(threshold)
    54  		return
    55  	}
    56  	globalTuner.setThreshold(threshold)
    57  }
    58  
    59  // GetGCPercent returns the current GCPercent.
    60  func GetGCPercent() uint32 {
    61  	if globalTuner == nil {
    62  		return defaultGCPercent
    63  	}
    64  	return globalTuner.getGCPercent()
    65  }
    66  
    67  // GetMaxGCPercent returns the max gc percent value.
    68  func GetMaxGCPercent() uint32 {
    69  	return atomic.LoadUint32(&maxGCPercent)
    70  }
    71  
    72  // SetMaxGCPercent sets the new max gc percent value.
    73  func SetMaxGCPercent(n uint32) uint32 {
    74  	return atomic.SwapUint32(&maxGCPercent, n)
    75  }
    76  
    77  // GetMinGCPercent returns the min gc percent value.
    78  func GetMinGCPercent() uint32 {
    79  	return atomic.LoadUint32(&minGCPercent)
    80  }
    81  
    82  // SetMinGCPercent sets the new min gc percent value.
    83  func SetMinGCPercent(n uint32) uint32 {
    84  	return atomic.SwapUint32(&minGCPercent, n)
    85  }
    86  
    87  // only allow one gc tuner in one process
    88  var globalTuner *tuner = nil
    89  
    90  /* Heap
    91   _______________  => limit: host/cgroup memory hard limit
    92  |               |
    93  |---------------| => threshold: increase GCPercent when gc_trigger < threshold
    94  |               |
    95  |---------------| => gc_trigger: heap_live + heap_live * GCPercent / 100
    96  |               |
    97  |---------------|
    98  |   heap_live   |
    99  |_______________|
   100  
   101  Go runtime only trigger GC when hit gc_trigger which affected by GCPercent and heap_live.
   102  So we can change GCPercent dynamically to tuning GC performance.
   103  */
   104  type tuner struct {
   105  	finalizer *finalizer
   106  	gcPercent uint32
   107  	threshold uint64 // high water level, in bytes
   108  }
   109  
   110  // tuning check the memory inuse and tune GC percent dynamically.
   111  // Go runtime ensure that it will be called serially.
   112  func (t *tuner) tuning() {
   113  	inuse := readMemoryInuse()
   114  	threshold := t.getThreshold()
   115  	// stop gc tuning
   116  	if threshold <= 0 {
   117  		return
   118  	}
   119  	t.setGCPercent(calcGCPercent(inuse, threshold))
   120  	return
   121  }
   122  
   123  // threshold = inuse + inuse * (gcPercent / 100)
   124  // => gcPercent = (threshold - inuse) / inuse * 100
   125  // if threshold < inuse*2, so gcPercent < 100, and GC positively to avoid OOM
   126  // if threshold > inuse*2, so gcPercent > 100, and GC negatively to reduce GC times
   127  func calcGCPercent(inuse, threshold uint64) uint32 {
   128  	// invalid params
   129  	if inuse == 0 || threshold == 0 {
   130  		return defaultGCPercent
   131  	}
   132  	// inuse heap larger than threshold, use min percent
   133  	if threshold <= inuse {
   134  		return minGCPercent
   135  	}
   136  	gcPercent := uint32(math.Floor(float64(threshold-inuse) / float64(inuse) * 100))
   137  	if gcPercent < minGCPercent {
   138  		return minGCPercent
   139  	} else if gcPercent > maxGCPercent {
   140  		return maxGCPercent
   141  	}
   142  	return gcPercent
   143  }
   144  
   145  func newTuner(threshold uint64) *tuner {
   146  	t := &tuner{
   147  		gcPercent: defaultGCPercent,
   148  		threshold: threshold,
   149  	}
   150  	t.finalizer = newFinalizer(t.tuning) // start tuning
   151  	return t
   152  }
   153  
   154  func (t *tuner) stop() {
   155  	t.finalizer.stop()
   156  }
   157  
   158  func (t *tuner) setThreshold(threshold uint64) {
   159  	atomic.StoreUint64(&t.threshold, threshold)
   160  }
   161  
   162  func (t *tuner) getThreshold() uint64 {
   163  	return atomic.LoadUint64(&t.threshold)
   164  }
   165  
   166  func (t *tuner) setGCPercent(percent uint32) uint32 {
   167  	atomic.StoreUint32(&t.gcPercent, percent)
   168  	return uint32(debug.SetGCPercent(int(percent)))
   169  }
   170  
   171  func (t *tuner) getGCPercent() uint32 {
   172  	return atomic.LoadUint32(&t.gcPercent)
   173  }