github.com/lingyao2333/mo-zero@v1.4.1/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/lingyao2333/mo-zero/core/collection"
    11  	"github.com/lingyao2333/mo-zero/core/logx"
    12  	"github.com/lingyao2333/mo-zero/core/stat"
    13  	"github.com/lingyao2333/mo-zero/core/syncx"
    14  	"github.com/lingyao2333/mo-zero/core/timex"
    15  )
    16  
    17  const (
    18  	defaultBuckets = 50
    19  	defaultWindow  = time.Second * 5
    20  	// using 1000m notation, 900m is like 90%, 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  		overloadTime    *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  		overloadTime:    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.droppedRecently.Set(true)
   122  
   123  		return nil, ErrServiceOverloaded
   124  	}
   125  
   126  	as.addFlying(1)
   127  
   128  	return &promise{
   129  		start:   timex.Now(),
   130  		shedder: as,
   131  	}, nil
   132  }
   133  
   134  func (as *adaptiveShedder) addFlying(delta int64) {
   135  	flying := atomic.AddInt64(&as.flying, delta)
   136  	// update avgFlying when the request is finished.
   137  	// this strategy makes avgFlying have a little bit lag against flying, and smoother.
   138  	// when the flying requests increase rapidly, avgFlying increase slower, accept more requests.
   139  	// when the flying requests drop rapidly, avgFlying drop slower, accept less requests.
   140  	// it makes the service to serve as more requests as possible.
   141  	if delta < 0 {
   142  		as.avgFlyingLock.Lock()
   143  		as.avgFlying = as.avgFlying*flyingBeta + float64(flying)*(1-flyingBeta)
   144  		as.avgFlyingLock.Unlock()
   145  	}
   146  }
   147  
   148  func (as *adaptiveShedder) highThru() bool {
   149  	as.avgFlyingLock.Lock()
   150  	avgFlying := as.avgFlying
   151  	as.avgFlyingLock.Unlock()
   152  	maxFlight := as.maxFlight()
   153  	return int64(avgFlying) > maxFlight && atomic.LoadInt64(&as.flying) > maxFlight
   154  }
   155  
   156  func (as *adaptiveShedder) maxFlight() int64 {
   157  	// windows = buckets per second
   158  	// maxQPS = maxPASS * windows
   159  	// minRT = min average response time in milliseconds
   160  	// maxQPS * minRT / milliseconds_per_second
   161  	return int64(math.Max(1, float64(as.maxPass()*as.windows)*(as.minRt()/1e3)))
   162  }
   163  
   164  func (as *adaptiveShedder) maxPass() int64 {
   165  	var result float64 = 1
   166  
   167  	as.passCounter.Reduce(func(b *collection.Bucket) {
   168  		if b.Sum > result {
   169  			result = b.Sum
   170  		}
   171  	})
   172  
   173  	return int64(result)
   174  }
   175  
   176  func (as *adaptiveShedder) minRt() float64 {
   177  	result := defaultMinRt
   178  
   179  	as.rtCounter.Reduce(func(b *collection.Bucket) {
   180  		if b.Count <= 0 {
   181  			return
   182  		}
   183  
   184  		avg := math.Round(b.Sum / float64(b.Count))
   185  		if avg < result {
   186  			result = avg
   187  		}
   188  	})
   189  
   190  	return result
   191  }
   192  
   193  func (as *adaptiveShedder) shouldDrop() bool {
   194  	if as.systemOverloaded() || as.stillHot() {
   195  		if as.highThru() {
   196  			flying := atomic.LoadInt64(&as.flying)
   197  			as.avgFlyingLock.Lock()
   198  			avgFlying := as.avgFlying
   199  			as.avgFlyingLock.Unlock()
   200  			msg := fmt.Sprintf(
   201  				"dropreq, cpu: %d, maxPass: %d, minRt: %.2f, hot: %t, flying: %d, avgFlying: %.2f",
   202  				stat.CpuUsage(), as.maxPass(), as.minRt(), as.stillHot(), flying, avgFlying)
   203  			logx.Error(msg)
   204  			stat.Report(msg)
   205  			return true
   206  		}
   207  	}
   208  
   209  	return false
   210  }
   211  
   212  func (as *adaptiveShedder) stillHot() bool {
   213  	if !as.droppedRecently.True() {
   214  		return false
   215  	}
   216  
   217  	overloadTime := as.overloadTime.Load()
   218  	if overloadTime == 0 {
   219  		return false
   220  	}
   221  
   222  	if timex.Since(overloadTime) < coolOffDuration {
   223  		return true
   224  	}
   225  
   226  	as.droppedRecently.Set(false)
   227  	return false
   228  }
   229  
   230  func (as *adaptiveShedder) systemOverloaded() bool {
   231  	if !systemOverloadChecker(as.cpuThreshold) {
   232  		return false
   233  	}
   234  
   235  	as.overloadTime.Set(timex.Now())
   236  	return true
   237  }
   238  
   239  // WithBuckets customizes the Shedder with given number of buckets.
   240  func WithBuckets(buckets int) ShedderOption {
   241  	return func(opts *shedderOptions) {
   242  		opts.buckets = buckets
   243  	}
   244  }
   245  
   246  // WithCpuThreshold customizes the Shedder with given cpu threshold.
   247  func WithCpuThreshold(threshold int64) ShedderOption {
   248  	return func(opts *shedderOptions) {
   249  		opts.cpuThreshold = threshold
   250  	}
   251  }
   252  
   253  // WithWindow customizes the Shedder with given
   254  func WithWindow(window time.Duration) ShedderOption {
   255  	return func(opts *shedderOptions) {
   256  		opts.window = window
   257  	}
   258  }
   259  
   260  type promise struct {
   261  	start   time.Duration
   262  	shedder *adaptiveShedder
   263  }
   264  
   265  func (p *promise) Fail() {
   266  	p.shedder.addFlying(-1)
   267  }
   268  
   269  func (p *promise) Pass() {
   270  	rt := float64(timex.Since(p.start)) / float64(time.Millisecond)
   271  	p.shedder.addFlying(-1)
   272  	p.shedder.rtCounter.Add(math.Ceil(rt))
   273  	p.shedder.passCounter.Add(1)
   274  }