github.com/songzhibin97/gkit@v1.2.13/gctuner/tuner.go (about)

     1  // Package gctuner implements https://github.com/bytedance/gopkg
     2  package gctuner
     3  
     4  import (
     5  	"fmt"
     6  	"github.com/docker/go-units"
     7  	mem_util "github.com/shirou/gopsutil/mem"
     8  	"io/ioutil"
     9  	"math"
    10  	"os"
    11  	"runtime/debug"
    12  	"strconv"
    13  	"strings"
    14  	"sync/atomic"
    15  )
    16  
    17  var (
    18  	maxGCPercent uint32 = 500
    19  	minGCPercent uint32 = 50
    20  )
    21  
    22  var defaultGCPercent uint32 = 100
    23  
    24  func init() {
    25  	gogcEnv := os.Getenv("GOGC")
    26  	gogc, err := strconv.ParseInt(gogcEnv, 10, 32)
    27  	if err != nil {
    28  		return
    29  	}
    30  	defaultGCPercent = uint32(gogc)
    31  }
    32  
    33  // Tuning sets the threshold of heap which will be respect by gc tuner.
    34  // When Tuning, the env GOGC will not be take effect.
    35  // threshold: disable tuning if threshold == 0
    36  func Tuning(threshold uint64) {
    37  	// disable gc tuner if percent is zero
    38  	if threshold <= 0 && globalTuner != nil {
    39  		globalTuner.stop()
    40  		globalTuner = nil
    41  		return
    42  	}
    43  
    44  	if globalTuner == nil {
    45  		globalTuner = newTuner(threshold)
    46  		return
    47  	}
    48  	globalTuner.setThreshold(threshold)
    49  }
    50  
    51  // GetGCPercent returns the current GCPercent.
    52  func GetGCPercent() uint32 {
    53  	if globalTuner == nil {
    54  		return defaultGCPercent
    55  	}
    56  	return globalTuner.getGCPercent()
    57  }
    58  
    59  // GetMaxGCPercent returns the max gc percent value.
    60  func GetMaxGCPercent() uint32 {
    61  	return atomic.LoadUint32(&maxGCPercent)
    62  }
    63  
    64  // SetMaxGCPercent sets the new max gc percent value.
    65  func SetMaxGCPercent(n uint32) uint32 {
    66  	return atomic.SwapUint32(&maxGCPercent, n)
    67  }
    68  
    69  // GetMinGCPercent returns the min gc percent value.
    70  func GetMinGCPercent() uint32 {
    71  	return atomic.LoadUint32(&minGCPercent)
    72  }
    73  
    74  // SetMinGCPercent sets the new min gc percent value.
    75  func SetMinGCPercent(n uint32) uint32 {
    76  	return atomic.SwapUint32(&minGCPercent, n)
    77  }
    78  
    79  // only allow one gc tuner in one process
    80  var globalTuner *tuner = nil
    81  
    82  /* Heap
    83   _______________  => limit: host/cgroup memory hard limit
    84  |               |
    85  |---------------| => threshold: increase GCPercent when gc_trigger < threshold
    86  |               |
    87  |---------------| => gc_trigger: heap_live + heap_live * GCPercent / 100
    88  |               |
    89  |---------------|
    90  |   heap_live   |
    91  |_______________|
    92  
    93  Go runtime only trigger GC when hit gc_trigger which affected by GCPercent and heap_live.
    94  So we can change GCPercent dynamically to tuning GC performance.
    95  */
    96  type tuner struct {
    97  	finalizer *finalizer
    98  	gcPercent uint32
    99  	threshold uint64 // high water level, in bytes
   100  }
   101  
   102  // tuning check the memory inuse and tune GC percent dynamically.
   103  // Go runtime ensure that it will be called serially.
   104  func (t *tuner) tuning() {
   105  	inuse := readMemoryInuse()
   106  	threshold := t.getThreshold()
   107  	// stop gc tuning
   108  	if threshold <= 0 {
   109  		return
   110  	}
   111  	t.setGCPercent(calcGCPercent(inuse, threshold))
   112  	return
   113  }
   114  
   115  // threshold = inuse + inuse * (gcPercent / 100)
   116  // => gcPercent = (threshold - inuse) / inuse * 100
   117  // if threshold < inuse*2, so gcPercent < 100, and GC positively to avoid OOM
   118  // if threshold > inuse*2, so gcPercent > 100, and GC negatively to reduce GC times
   119  func calcGCPercent(inuse, threshold uint64) uint32 {
   120  	// invalid params
   121  	if inuse == 0 || threshold == 0 {
   122  		return defaultGCPercent
   123  	}
   124  	// inuse heap larger than threshold, use min percent
   125  	if threshold <= inuse {
   126  		return minGCPercent
   127  	}
   128  	gcPercent := uint32(math.Floor(float64(threshold-inuse) / float64(inuse) * 100))
   129  	if gcPercent < minGCPercent {
   130  		return minGCPercent
   131  	} else if gcPercent > maxGCPercent {
   132  		return maxGCPercent
   133  	}
   134  	return gcPercent
   135  }
   136  
   137  func newTuner(threshold uint64) *tuner {
   138  	t := &tuner{
   139  		gcPercent: defaultGCPercent,
   140  		threshold: threshold,
   141  	}
   142  	t.finalizer = newFinalizer(t.tuning) // start tuning
   143  	return t
   144  }
   145  
   146  func (t *tuner) stop() {
   147  	t.finalizer.stop()
   148  }
   149  
   150  func (t *tuner) setThreshold(threshold uint64) {
   151  	atomic.StoreUint64(&t.threshold, threshold)
   152  }
   153  
   154  func (t *tuner) getThreshold() uint64 {
   155  	return atomic.LoadUint64(&t.threshold)
   156  }
   157  
   158  func (t *tuner) setGCPercent(percent uint32) uint32 {
   159  	atomic.StoreUint32(&t.gcPercent, percent)
   160  	return uint32(debug.SetGCPercent(int(percent)))
   161  }
   162  
   163  func (t *tuner) getGCPercent() uint32 {
   164  	return atomic.LoadUint32(&t.gcPercent)
   165  }
   166  
   167  // TuningWithFromHuman
   168  // eg. "b/B", "k/K" "kb/Kb" "mb/Mb", "gb/Gb" "tb/Tb" "pb/Pb".
   169  func TuningWithFromHuman(threshold string) {
   170  	parseThreshold, err := units.FromHumanSize(threshold)
   171  	if err != nil {
   172  		fmt.Println("format err:", err)
   173  		return
   174  	}
   175  	Tuning(uint64(parseThreshold))
   176  }
   177  
   178  // TuningWithAuto By automatic calculation of the total amount
   179  func TuningWithAuto(isContainer bool) {
   180  	var (
   181  		threshold uint64
   182  		err       error
   183  	)
   184  	if isContainer {
   185  		threshold, err = getCGroupMemoryLimit()
   186  	} else {
   187  		threshold, err = getNormalMemoryLimit()
   188  	}
   189  	if err != nil {
   190  		fmt.Println("get memery err:", err)
   191  		return
   192  	}
   193  	Tuning(uint64(float64(threshold) * 0.7))
   194  }
   195  
   196  const cgroupMemLimitPath = "/sys/fs/cgroup/memory/memory.limit_in_bytes"
   197  
   198  func getCGroupMemoryLimit() (uint64, error) {
   199  	usage, err := readUint(cgroupMemLimitPath)
   200  	if err != nil {
   201  		return 0, err
   202  	}
   203  	machineMemory, err := mem_util.VirtualMemory()
   204  	if err != nil {
   205  		return 0, err
   206  	}
   207  	limit := uint64(math.Min(float64(usage), float64(machineMemory.Total)))
   208  	return limit, nil
   209  }
   210  
   211  func getNormalMemoryLimit() (uint64, error) {
   212  	machineMemory, err := mem_util.VirtualMemory()
   213  	if err != nil {
   214  		return 0, err
   215  	}
   216  	return machineMemory.Total, nil
   217  }
   218  
   219  // copied from https://github.com/containerd/cgroups/blob/318312a373405e5e91134d8063d04d59768a1bff/utils.go#L251
   220  func parseUint(s string, base, bitSize int) (uint64, error) {
   221  	v, err := strconv.ParseUint(s, base, bitSize)
   222  	if err != nil {
   223  		intValue, intErr := strconv.ParseInt(s, base, bitSize)
   224  		// 1. Handle negative values greater than MinInt64 (and)
   225  		// 2. Handle negative values lesser than MinInt64
   226  		if intErr == nil && intValue < 0 {
   227  			return 0, nil
   228  		} else if intErr != nil &&
   229  			intErr.(*strconv.NumError).Err == strconv.ErrRange &&
   230  			intValue < 0 {
   231  			return 0, nil
   232  		}
   233  		return 0, err
   234  	}
   235  	return v, nil
   236  }
   237  
   238  func readUint(path string) (uint64, error) {
   239  	v, err := ioutil.ReadFile(path)
   240  	if err != nil {
   241  		return 0, err
   242  	}
   243  	return parseUint(strings.TrimSpace(string(v)), 10, 64)
   244  }