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  }