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 }