google.golang.org/grpc@v1.72.2/balancer/rls/internal/adaptive/adaptive.go (about) 1 /* 2 * 3 * Copyright 2020 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 // Package adaptive provides functionality for adaptive client-side throttling. 20 package adaptive 21 22 import ( 23 rand "math/rand/v2" 24 "sync" 25 "time" 26 ) 27 28 // For overriding in unittests. 29 var ( 30 timeNowFunc = time.Now 31 randFunc = rand.Float64 32 ) 33 34 const ( 35 defaultDuration = 30 * time.Second 36 defaultBins = 100 37 defaultRatioForAccepts = 2.0 38 defaultRequestsPadding = 8.0 39 ) 40 41 // Throttler implements a client-side throttling recommendation system. All 42 // methods are safe for concurrent use by multiple goroutines. 43 // 44 // The throttler has the following knobs for which we will use defaults for 45 // now. If there is a need to make them configurable at a later point in time, 46 // support for the same will be added. 47 // - Duration: amount of recent history that will be taken into account for 48 // making client-side throttling decisions. A default of 30 seconds is used. 49 // - Bins: number of bins to be used for bucketing historical data. A default 50 // of 100 is used. 51 // - RatioForAccepts: ratio by which accepts are multiplied, typically a value 52 // slightly larger than 1.0. This is used to make the throttler behave as if 53 // the backend had accepted more requests than it actually has, which lets us 54 // err on the side of sending to the backend more requests than we think it 55 // will accept for the sake of speeding up the propagation of state. A 56 // default of 2.0 is used. 57 // - RequestsPadding: is used to decrease the (client-side) throttling 58 // probability in the low QPS regime (to speed up propagation of state), as 59 // well as to safeguard against hitting a client-side throttling probability 60 // of 100%. The weight of this value decreases as the number of requests in 61 // recent history grows. A default of 8 is used. 62 // 63 // The adaptive throttler attempts to estimate the probability that a request 64 // will be throttled using recent history. Server requests (both throttled and 65 // accepted) are registered with the throttler (via the RegisterBackendResponse 66 // method), which then recommends client-side throttling (via the 67 // ShouldThrottle method) with probability given by: 68 // (requests - RatioForAccepts * accepts) / (requests + RequestsPadding) 69 type Throttler struct { 70 ratioForAccepts float64 71 requestsPadding float64 72 73 // Number of total accepts and throttles in the lookback period. 74 mu sync.Mutex 75 accepts *lookback 76 throttles *lookback 77 } 78 79 // New initializes a new adaptive throttler with the default values. 80 func New() *Throttler { 81 return newWithArgs(defaultDuration, defaultBins, defaultRatioForAccepts, defaultRequestsPadding) 82 } 83 84 // newWithArgs initializes a new adaptive throttler with the provided values. 85 // Used only in unittests. 86 func newWithArgs(duration time.Duration, bins int64, ratioForAccepts, requestsPadding float64) *Throttler { 87 return &Throttler{ 88 ratioForAccepts: ratioForAccepts, 89 requestsPadding: requestsPadding, 90 accepts: newLookback(bins, duration), 91 throttles: newLookback(bins, duration), 92 } 93 } 94 95 // ShouldThrottle returns a probabilistic estimate of whether the server would 96 // throttle the next request. This should be called for every request before 97 // allowing it to hit the network. If the returned value is true, the request 98 // should be aborted immediately (as if it had been throttled by the server). 99 func (t *Throttler) ShouldThrottle() bool { 100 randomProbability := randFunc() 101 now := timeNowFunc() 102 103 t.mu.Lock() 104 defer t.mu.Unlock() 105 106 accepts, throttles := float64(t.accepts.sum(now)), float64(t.throttles.sum(now)) 107 requests := accepts + throttles 108 throttleProbability := (requests - t.ratioForAccepts*accepts) / (requests + t.requestsPadding) 109 if throttleProbability <= randomProbability { 110 return false 111 } 112 113 t.throttles.add(now, 1) 114 return true 115 } 116 117 // RegisterBackendResponse registers a response received from the backend for a 118 // request allowed by ShouldThrottle. This should be called for every response 119 // received from the backend (i.e., once for each request for which 120 // ShouldThrottle returned false). 121 func (t *Throttler) RegisterBackendResponse(throttled bool) { 122 now := timeNowFunc() 123 124 t.mu.Lock() 125 if throttled { 126 t.throttles.add(now, 1) 127 } else { 128 t.accepts.add(now, 1) 129 } 130 t.mu.Unlock() 131 }