github.com/grafana/pyroscope@v1.18.0/pkg/segmentwriter/client/distributor/placement/adaptiveplacement/load_balancing.go (about) 1 package adaptiveplacement 2 3 import ( 4 "errors" 5 "fmt" 6 "math" 7 "math/rand" 8 9 "github.com/grafana/pyroscope/pkg/segmentwriter/client/distributor/placement" 10 "github.com/grafana/pyroscope/pkg/segmentwriter/client/distributor/placement/adaptiveplacement/adaptive_placementpb" 11 ) 12 13 type LoadBalancing string 14 15 const ( 16 FingerprintLoadBalancing LoadBalancing = "fingerprint" 17 RoundRobinLoadBalancing LoadBalancing = "round-robin" 18 DynamicLoadBalancing LoadBalancing = "dynamic" 19 ) 20 21 var ErrLoadBalancing = errors.New("invalid load balancing option") 22 23 var loadBalancingOptions = []LoadBalancing{ 24 FingerprintLoadBalancing, 25 RoundRobinLoadBalancing, 26 DynamicLoadBalancing, 27 } 28 29 const validOptionsString = "valid options: fingerprint, round-robin, dynamic" 30 31 func (lb *LoadBalancing) Set(text string) error { 32 x := LoadBalancing(text) 33 for _, name := range loadBalancingOptions { 34 if x == name { 35 *lb = x 36 return nil 37 } 38 } 39 return fmt.Errorf("%w: %s; %s", ErrLoadBalancing, x, validOptionsString) 40 } 41 42 func (lb *LoadBalancing) String() string { return string(*lb) } 43 44 func (lb LoadBalancing) proto() adaptive_placementpb.LoadBalancing { 45 switch lb { 46 default: 47 return adaptive_placementpb.LoadBalancing_LOAD_BALANCING_UNSPECIFIED 48 case DynamicLoadBalancing: 49 return adaptive_placementpb.LoadBalancing_LOAD_BALANCING_UNSPECIFIED 50 case RoundRobinLoadBalancing: 51 return adaptive_placementpb.LoadBalancing_LOAD_BALANCING_ROUND_ROBIN 52 case FingerprintLoadBalancing: 53 return adaptive_placementpb.LoadBalancing_LOAD_BALANCING_FINGERPRINT 54 } 55 } 56 57 func loadBalancingFromProto(lb adaptive_placementpb.LoadBalancing) LoadBalancing { 58 switch lb { 59 default: 60 return FingerprintLoadBalancing 61 case adaptive_placementpb.LoadBalancing_LOAD_BALANCING_ROUND_ROBIN: 62 return RoundRobinLoadBalancing 63 } 64 } 65 66 func (lb LoadBalancing) pick(k placement.Key) func(int) int { 67 switch lb { 68 default: 69 return pickFingerprintMod(k) 70 case RoundRobinLoadBalancing: 71 return pickRoundRobin() 72 } 73 } 74 75 func pickFingerprintMod(k placement.Key) func(int) int { 76 return func(n int) int { 77 return int(k.Fingerprint % uint64(n)) 78 } 79 } 80 81 func pickRoundRobin() func(int) int { return roundRobin } 82 func roundRobin(n int) int { return rand.Intn(n) } 83 84 // needsDynamicBalancing returns true if the load balancing strategy 85 // should be chosen dynamically based on the dataset stats. 86 // x is the currently set load balancing strategy. 87 func (lb LoadBalancing) needsDynamicBalancing(x adaptive_placementpb.LoadBalancing) bool { 88 // If the configured load balancing is "dynamic", we should 89 // try to find the best strategy based on the dataset stats, 90 // except if the x is already set to round-robin, which should 91 // ensure the best distribution (from the available options). 92 return lb == DynamicLoadBalancing && x != adaptive_placementpb.LoadBalancing_LOAD_BALANCING_ROUND_ROBIN 93 } 94 95 // loadBalancingStrategy chooses the load balancing strategy. 96 // 97 // By default, we adhere to the standard fingerprint-based distribution, 98 // since it provides slightly better locality in case if the dataset has 99 // enough keys to distribute. However, oftentimes this is not the case. 100 // 101 // If at least one shard is significantly overheated, and relative standard 102 // deviation within the aggregation window is very high, which indicates 103 // that the distribution is uneven, we resort to round-robin load balancing. 104 func loadBalancingStrategy(stats *adaptive_placementpb.DatasetStats, unit uint64, target int) LoadBalancing { 105 lb := FingerprintLoadBalancing 106 if len(stats.Shards) < 2 { 107 return lb 108 } 109 if p := float64(len(stats.Shards)) / float64(target); p > 2 || p < 0.5 { 110 // It is possible that the dataset is being moved 111 // to a different node, or different shards, or is 112 // being scaled in/out, and therefore nonuniform 113 // distribution might be expected within some period 114 // of time. Moreover, there might be a sudden surge 115 // in usage; together with high dispersion, this can 116 // lead to false positives. 117 return lb 118 } 119 t := 2 * unit 120 var overheated bool 121 for _, v := range stats.Usage { 122 if v >= t { 123 overheated = true 124 break 125 } 126 } 127 if !overheated { 128 return lb 129 } 130 if float64(stats.StdDev)/float64(mean(stats.Usage)) < 0.5 { 131 return lb 132 } 133 // Thresholds (2 x unit size, shards/target ratio, 0.5 RSD) are arbitrary 134 // and can be adjusted. The current values are conservative and were chosen 135 // to use RR as a last resort. 136 return RoundRobinLoadBalancing 137 } 138 139 func stdDev(d []uint64) uint64 { 140 if len(d) == 0 { 141 return 0 142 } 143 m := mean(d) 144 var variance uint64 145 for _, v := range d { 146 dev := v - m 147 variance += dev * dev 148 } 149 variance /= uint64(len(d)) 150 return uint64(math.Sqrt(float64(variance))) 151 } 152 153 func mean(d []uint64) (m uint64) { 154 if len(d) == 0 { 155 return m 156 } 157 for _, v := range d { 158 m += v 159 } 160 return m / uint64(len(d)) 161 } 162 163 func sum(d []uint64) (s uint64) { 164 for _, v := range d { 165 s += v 166 } 167 return s 168 }