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 }