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 }