github.com/shuguocloud/go-zero@v1.3.0/core/load/adaptiveshedder.go (about)

     1  package load
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"math"
     7  	"sync/atomic"
     8  	"time"
     9  
    10  	"github.com/shuguocloud/go-zero/core/collection"
    11  	"github.com/shuguocloud/go-zero/core/logx"
    12  	"github.com/shuguocloud/go-zero/core/stat"
    13  	"github.com/shuguocloud/go-zero/core/syncx"
    14  	"github.com/shuguocloud/go-zero/core/timex"
    15  )
    16  
    17  const (
    18  	defaultBuckets = 50
    19  	defaultWindow  = time.Second * 5
    20  	// using 1000m notation, 900m is like 80%, keep it as var for unit test
    21  	defaultCpuThreshold = 900
    22  	defaultMinRt        = float64(time.Second / time.Millisecond)
    23  	// moving average hyperparameter beta for calculating requests on the fly
    24  	flyingBeta      = 0.9
    25  	coolOffDuration = time.Second
    26  )
    27  
    28  var (
    29  	// ErrServiceOverloaded is returned by Shedder.Allow when the service is overloaded.
    30  	ErrServiceOverloaded = errors.New("service overloaded")
    31  
    32  	// default to be enabled
    33  	enabled = syncx.ForAtomicBool(true)
    34  	// default to be enabled
    35  	logEnabled = syncx.ForAtomicBool(true)
    36  	// make it a variable for unit test
    37  	systemOverloadChecker = func(cpuThreshold int64) bool {
    38  		return stat.CpuUsage() >= cpuThreshold
    39  	}
    40  )
    41  
    42  type (
    43  	// A Promise interface is returned by Shedder.Allow to let callers tell
    44  	// whether the processing request is successful or not.
    45  	Promise interface {
    46  		// Pass lets the caller tell that the call is successful.
    47  		Pass()
    48  		// Fail lets the caller tell that the call is failed.
    49  		Fail()
    50  	}
    51  
    52  	// Shedder is the interface that wraps the Allow method.
    53  	Shedder interface {
    54  		// Allow returns the Promise if allowed, otherwise ErrServiceOverloaded.
    55  		Allow() (Promise, error)
    56  	}
    57  
    58  	// ShedderOption lets caller customize the Shedder.
    59  	ShedderOption func(opts *shedderOptions)
    60  
    61  	shedderOptions struct {
    62  		window       time.Duration
    63  		buckets      int
    64  		cpuThreshold int64
    65  	}
    66  
    67  	adaptiveShedder struct {
    68  		cpuThreshold    int64
    69  		windows         int64
    70  		flying          int64
    71  		avgFlying       float64
    72  		avgFlyingLock   syncx.SpinLock
    73  		dropTime        *syncx.AtomicDuration
    74  		droppedRecently *syncx.AtomicBool
    75  		passCounter     *collection.RollingWindow
    76  		rtCounter       *collection.RollingWindow
    77  	}
    78  )
    79  
    80  // Disable lets callers disable load shedding.
    81  func Disable() {
    82  	enabled.Set(false)
    83  }
    84  
    85  // DisableLog disables the stat logs for load shedding.
    86  func DisableLog() {
    87  	logEnabled.Set(false)
    88  }
    89  
    90  // NewAdaptiveShedder returns an adaptive shedder.
    91  // opts can be used to customize the Shedder.
    92  func NewAdaptiveShedder(opts ...ShedderOption) Shedder {
    93  	if !enabled.True() {
    94  		return newNopShedder()
    95  	}
    96  
    97  	options := shedderOptions{
    98  		window:       defaultWindow,
    99  		buckets:      defaultBuckets,
   100  		cpuThreshold: defaultCpuThreshold,
   101  	}
   102  	for _, opt := range opts {
   103  		opt(&options)
   104  	}
   105  	bucketDuration := options.window / time.Duration(options.buckets)
   106  	return &adaptiveShedder{
   107  		cpuThreshold:    options.cpuThreshold,
   108  		windows:         int64(time.Second / bucketDuration),
   109  		dropTime:        syncx.NewAtomicDuration(),
   110  		droppedRecently: syncx.NewAtomicBool(),
   111  		passCounter: collection.NewRollingWindow(options.buckets, bucketDuration,
   112  			collection.IgnoreCurrentBucket()),
   113  		rtCounter: collection.NewRollingWindow(options.buckets, bucketDuration,
   114  			collection.IgnoreCurrentBucket()),
   115  	}
   116  }
   117  
   118  // Allow implements Shedder.Allow.
   119  func (as *adaptiveShedder) Allow() (Promise, error) {
   120  	if as.shouldDrop() {
   121  		as.dropTime.Set(timex.Now())
   122  		as.droppedRecently.Set(true)
   123  
   124  		return nil, ErrServiceOverloaded
   125  	}
   126  
   127  	as.addFlying(1)
   128  
   129  	return &promise{
   130  		start:   timex.Now(),
   131  		shedder: as,
   132  	}, nil
   133  }
   134  
   135  func (as *adaptiveShedder) addFlying(delta int64) {
   136  	flying := atomic.AddInt64(&as.flying, delta)
   137  	// update avgFlying when the request is finished.
   138  	// this strategy makes avgFlying have a little bit lag against flying, and smoother.
   139  	// when the flying requests increase rapidly, avgFlying increase slower, accept more requests.
   140  	// when the flying requests drop rapidly, avgFlying drop slower, accept less requests.
   141  	// it makes the service to serve as more requests as possible.
   142  	if delta < 0 {
   143  		as.avgFlyingLock.Lock()
   144  		as.avgFlying = as.avgFlying*flyingBeta + float64(flying)*(1-flyingBeta)
   145  		as.avgFlyingLock.Unlock()
   146  	}
   147  }
   148  
   149  func (as *adaptiveShedder) highThru() bool {
   150  	as.avgFlyingLock.Lock()
   151  	avgFlying := as.avgFlying
   152  	as.avgFlyingLock.Unlock()
   153  	maxFlight := as.maxFlight()
   154  	return int64(avgFlying) > maxFlight && atomic.LoadInt64(&as.flying) > maxFlight
   155  }
   156  
   157  func (as *adaptiveShedder) maxFlight() int64 {
   158  	// windows = buckets per second
   159  	// maxQPS = maxPASS * windows
   160  	// minRT = min average response time in milliseconds
   161  	// maxQPS * minRT / milliseconds_per_second
   162  	return int64(math.Max(1, float64(as.maxPass()*as.windows)*(as.minRt()/1e3)))
   163  }
   164  
   165  func (as *adaptiveShedder) maxPass() int64 {
   166  	var result float64 = 1
   167  
   168  	as.passCounter.Reduce(func(b *collection.Bucket) {
   169  		if b.Sum > result {
   170  			result = b.Sum
   171  		}
   172  	})
   173  
   174  	return int64(result)
   175  }
   176  
   177  func (as *adaptiveShedder) minRt() float64 {
   178  	result := defaultMinRt
   179  
   180  	as.rtCounter.Reduce(func(b *collection.Bucket) {
   181  		if b.Count <= 0 {
   182  			return
   183  		}
   184  
   185  		avg := math.Round(b.Sum / float64(b.Count))
   186  		if avg < result {
   187  			result = avg
   188  		}
   189  	})
   190  
   191  	return result
   192  }
   193  
   194  func (as *adaptiveShedder) shouldDrop() bool {
   195  	if as.systemOverloaded() || as.stillHot() {
   196  		if as.highThru() {
   197  			flying := atomic.LoadInt64(&as.flying)
   198  			as.avgFlyingLock.Lock()
   199  			avgFlying := as.avgFlying
   200  			as.avgFlyingLock.Unlock()
   201  			msg := fmt.Sprintf(
   202  				"dropreq, cpu: %d, maxPass: %d, minRt: %.2f, hot: %t, flying: %d, avgFlying: %.2f",
   203  				stat.CpuUsage(), as.maxPass(), as.minRt(), as.stillHot(), flying, avgFlying)
   204  			logx.Error(msg)
   205  			stat.Report(msg)
   206  			return true
   207  		}
   208  	}
   209  
   210  	return false
   211  }
   212  
   213  func (as *adaptiveShedder) stillHot() bool {
   214  	if !as.droppedRecently.True() {
   215  		return false
   216  	}
   217  
   218  	dropTime := as.dropTime.Load()
   219  	if dropTime == 0 {
   220  		return false
   221  	}
   222  
   223  	hot := timex.Since(dropTime) < coolOffDuration
   224  	if !hot {
   225  		as.droppedRecently.Set(false)
   226  	}
   227  
   228  	return hot
   229  }
   230  
   231  func (as *adaptiveShedder) systemOverloaded() bool {
   232  	return systemOverloadChecker(as.cpuThreshold)
   233  }
   234  
   235  // WithBuckets customizes the Shedder with given number of buckets.
   236  func WithBuckets(buckets int) ShedderOption {
   237  	return func(opts *shedderOptions) {
   238  		opts.buckets = buckets
   239  	}
   240  }
   241  
   242  // WithCpuThreshold customizes the Shedder with given cpu threshold.
   243  func WithCpuThreshold(threshold int64) ShedderOption {
   244  	return func(opts *shedderOptions) {
   245  		opts.cpuThreshold = threshold
   246  	}
   247  }
   248  
   249  // WithWindow customizes the Shedder with given
   250  func WithWindow(window time.Duration) ShedderOption {
   251  	return func(opts *shedderOptions) {
   252  		opts.window = window
   253  	}
   254  }
   255  
   256  type promise struct {
   257  	start   time.Duration
   258  	shedder *adaptiveShedder
   259  }
   260  
   261  func (p *promise) Fail() {
   262  	p.shedder.addFlying(-1)
   263  }
   264  
   265  func (p *promise) Pass() {
   266  	rt := float64(timex.Since(p.start)) / float64(time.Millisecond)
   267  	p.shedder.addFlying(-1)
   268  	p.shedder.rtCounter.Add(math.Ceil(rt))
   269  	p.shedder.passCounter.Add(1)
   270  }