github.com/kaisenlinux/docker@v0.0.0-20230510090727-ea55db55fac7/swarmkit/remotes/remotes.go (about)

     1  package remotes
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"math/rand"
     7  	"sort"
     8  	"sync"
     9  
    10  	"github.com/docker/swarmkit/api"
    11  )
    12  
    13  var errRemotesUnavailable = fmt.Errorf("no remote hosts provided")
    14  
    15  // DefaultObservationWeight provides a weight to use for positive observations
    16  // that will balance well under repeated observations.
    17  const DefaultObservationWeight = 10
    18  
    19  // Remotes keeps track of remote addresses by weight, informed by
    20  // observations.
    21  type Remotes interface {
    22  	// Weight returns the remotes with their current weights.
    23  	Weights() map[api.Peer]int
    24  
    25  	// Select a remote from the set of available remotes with optionally
    26  	// excluding ID or address.
    27  	Select(...string) (api.Peer, error)
    28  
    29  	// Observe records an experience with a particular remote. A positive weight
    30  	// indicates a good experience and a negative weight a bad experience.
    31  	//
    32  	// The observation will be used to calculate a moving weight, which is
    33  	// implementation dependent. This method will be called such that repeated
    34  	// observations of the same master in each session request are favored.
    35  	Observe(peer api.Peer, weight int)
    36  
    37  	// ObserveIfExists records an experience with a particular remote if when a
    38  	// remote exists.
    39  	ObserveIfExists(peer api.Peer, weight int)
    40  
    41  	// Remove the remote from the list completely.
    42  	Remove(addrs ...api.Peer)
    43  }
    44  
    45  // NewRemotes returns a Remotes instance with the provided set of addresses.
    46  // Entries provided are heavily weighted initially.
    47  func NewRemotes(peers ...api.Peer) Remotes {
    48  	mwr := &remotesWeightedRandom{
    49  		remotes: make(map[api.Peer]int),
    50  	}
    51  
    52  	for _, peer := range peers {
    53  		mwr.Observe(peer, DefaultObservationWeight)
    54  	}
    55  
    56  	return mwr
    57  }
    58  
    59  type remotesWeightedRandom struct {
    60  	remotes map[api.Peer]int
    61  	mu      sync.Mutex
    62  
    63  	// workspace to avoid reallocation. these get lazily allocated when
    64  	// selecting values.
    65  	cdf   []float64
    66  	peers []api.Peer
    67  }
    68  
    69  func (mwr *remotesWeightedRandom) Weights() map[api.Peer]int {
    70  	mwr.mu.Lock()
    71  	defer mwr.mu.Unlock()
    72  
    73  	ms := make(map[api.Peer]int, len(mwr.remotes))
    74  	for addr, weight := range mwr.remotes {
    75  		ms[addr] = weight
    76  	}
    77  
    78  	return ms
    79  }
    80  
    81  func (mwr *remotesWeightedRandom) Select(excludes ...string) (api.Peer, error) {
    82  	mwr.mu.Lock()
    83  	defer mwr.mu.Unlock()
    84  
    85  	// NOTE(stevvooe): We then use a weighted random selection algorithm
    86  	// (http://stackoverflow.com/questions/4463561/weighted-random-selection-from-array)
    87  	// to choose the master to connect to.
    88  	//
    89  	// It is possible that this is insufficient. The following may inform a
    90  	// better solution:
    91  
    92  	// https://github.com/LK4D4/sample
    93  	//
    94  	// The first link applies exponential distribution weight choice reservoir
    95  	// sampling. This may be relevant if we view the master selection as a
    96  	// distributed reservoir sampling problem.
    97  
    98  	// bias to zero-weighted remotes have same probability. otherwise, we
    99  	// always select first entry when all are zero.
   100  	const bias = 0.001
   101  
   102  	// clear out workspace
   103  	mwr.cdf = mwr.cdf[:0]
   104  	mwr.peers = mwr.peers[:0]
   105  
   106  	cum := 0.0
   107  	// calculate CDF over weights
   108  Loop:
   109  	for peer, weight := range mwr.remotes {
   110  		for _, exclude := range excludes {
   111  			if peer.NodeID == exclude || peer.Addr == exclude {
   112  				// if this peer is excluded, ignore it by continuing the loop to label Loop
   113  				continue Loop
   114  			}
   115  		}
   116  		if weight < 0 {
   117  			// treat these as zero, to keep there selection unlikely.
   118  			weight = 0
   119  		}
   120  
   121  		cum += float64(weight) + bias
   122  		mwr.cdf = append(mwr.cdf, cum)
   123  		mwr.peers = append(mwr.peers, peer)
   124  	}
   125  
   126  	if len(mwr.peers) == 0 {
   127  		return api.Peer{}, errRemotesUnavailable
   128  	}
   129  
   130  	r := mwr.cdf[len(mwr.cdf)-1] * rand.Float64()
   131  	i := sort.SearchFloat64s(mwr.cdf, r)
   132  
   133  	return mwr.peers[i], nil
   134  }
   135  
   136  func (mwr *remotesWeightedRandom) Observe(peer api.Peer, weight int) {
   137  	mwr.mu.Lock()
   138  	defer mwr.mu.Unlock()
   139  
   140  	mwr.observe(peer, float64(weight))
   141  }
   142  
   143  func (mwr *remotesWeightedRandom) ObserveIfExists(peer api.Peer, weight int) {
   144  	mwr.mu.Lock()
   145  	defer mwr.mu.Unlock()
   146  
   147  	if _, ok := mwr.remotes[peer]; !ok {
   148  		return
   149  	}
   150  
   151  	mwr.observe(peer, float64(weight))
   152  }
   153  
   154  func (mwr *remotesWeightedRandom) Remove(addrs ...api.Peer) {
   155  	mwr.mu.Lock()
   156  	defer mwr.mu.Unlock()
   157  
   158  	for _, addr := range addrs {
   159  		delete(mwr.remotes, addr)
   160  	}
   161  }
   162  
   163  const (
   164  	// remoteWeightSmoothingFactor for exponential smoothing. This adjusts how
   165  	// much of the // observation and old value we are using to calculate the new value.
   166  	// See
   167  	// https://en.wikipedia.org/wiki/Exponential_smoothing#Basic_exponential_smoothing
   168  	// for details.
   169  	remoteWeightSmoothingFactor = 0.5
   170  	remoteWeightMax             = 1 << 8
   171  )
   172  
   173  func clip(x float64) float64 {
   174  	if math.IsNaN(x) {
   175  		// treat garbage as such
   176  		// acts like a no-op for us.
   177  		return 0
   178  	}
   179  	return math.Max(math.Min(remoteWeightMax, x), -remoteWeightMax)
   180  }
   181  
   182  func (mwr *remotesWeightedRandom) observe(peer api.Peer, weight float64) {
   183  
   184  	// While we have a decent, ad-hoc approach here to weight subsequent
   185  	// observations, we may want to look into applying forward decay:
   186  	//
   187  	//  http://dimacs.rutgers.edu/~graham/pubs/papers/fwddecay.pdf
   188  	//
   189  	// We need to get better data from behavior in a cluster.
   190  
   191  	// makes the math easier to read below
   192  	var (
   193  		w0 = float64(mwr.remotes[peer])
   194  		w1 = clip(weight)
   195  	)
   196  	const α = remoteWeightSmoothingFactor
   197  
   198  	// Multiply the new value to current value, and appy smoothing against the old
   199  	// value.
   200  	wn := clip(α*w1 + (1-α)*w0)
   201  
   202  	mwr.remotes[peer] = int(math.Ceil(wn))
   203  }