github.com/theQRL/go-zond@v0.1.1/p2p/msgrate/msgrate.go (about) 1 // Copyright 2021 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 // Package msgrate allows estimating the throughput of peers for more balanced syncs. 18 package msgrate 19 20 import ( 21 "errors" 22 "math" 23 "sort" 24 "sync" 25 "time" 26 27 "github.com/theQRL/go-zond/log" 28 ) 29 30 // measurementImpact is the impact a single measurement has on a peer's final 31 // capacity value. A value closer to 0 reacts slower to sudden network changes, 32 // but it is also more stable against temporary hiccups. 0.1 worked well for 33 // most of Ethereum's existence, so might as well go with it. 34 const measurementImpact = 0.1 35 36 // capacityOverestimation is the ratio of items to over-estimate when retrieving 37 // a peer's capacity to avoid locking into a lower value due to never attempting 38 // to fetch more than some local stable value. 39 const capacityOverestimation = 1.01 40 41 // rttMinEstimate is the minimal round trip time to target requests for. Since 42 // every request entails a 2 way latency + bandwidth + serving database lookups, 43 // it should be generous enough to permit meaningful work to be done on top of 44 // the transmission costs. 45 const rttMinEstimate = 2 * time.Second 46 47 // rttMaxEstimate is the maximal round trip time to target requests for. Although 48 // the expectation is that a well connected node will never reach this, certain 49 // special connectivity ones might experience significant delays (e.g. satellite 50 // uplink with 3s RTT). This value should be low enough to forbid stalling the 51 // pipeline too long, but large enough to cover the worst of the worst links. 52 const rttMaxEstimate = 20 * time.Second 53 54 // rttPushdownFactor is a multiplier to attempt forcing quicker requests than 55 // what the message rate tracker estimates. The reason is that message rate 56 // tracking adapts queries to the RTT, but multiple RTT values can be perfectly 57 // valid, they just result in higher packet sizes. Since smaller packets almost 58 // always result in stabler download streams, this factor hones in on the lowest 59 // RTT from all the functional ones. 60 const rttPushdownFactor = 0.9 61 62 // rttMinConfidence is the minimum value the roundtrip confidence factor may drop 63 // to. Since the target timeouts are based on how confident the tracker is in the 64 // true roundtrip, it's important to not allow too huge fluctuations. 65 const rttMinConfidence = 0.1 66 67 // ttlScaling is the multiplier that converts the estimated roundtrip time to a 68 // timeout cap for network requests. The expectation is that peers' response time 69 // will fluctuate around the estimated roundtrip, but depending in their load at 70 // request time, it might be higher than anticipated. This scaling factor ensures 71 // that we allow remote connections some slack but at the same time do enforce a 72 // behavior similar to our median peers. 73 const ttlScaling = 3 74 75 // ttlLimit is the maximum timeout allowance to prevent reaching crazy numbers 76 // if some unforeseen network events happen. As much as we try to hone in on 77 // the most optimal values, it doesn't make any sense to go above a threshold, 78 // even if everything is slow and screwy. 79 const ttlLimit = time.Minute 80 81 // tuningConfidenceCap is the number of active peers above which to stop detuning 82 // the confidence number. The idea here is that once we hone in on the capacity 83 // of a meaningful number of peers, adding one more should ot have a significant 84 // impact on things, so just ron with the originals. 85 const tuningConfidenceCap = 10 86 87 // tuningImpact is the influence that a new tuning target has on the previously 88 // cached value. This number is mostly just an out-of-the-blue heuristic that 89 // prevents the estimates from jumping around. There's no particular reason for 90 // the current value. 91 const tuningImpact = 0.25 92 93 // Tracker estimates the throughput capacity of a peer with regard to each data 94 // type it can deliver. The goal is to dynamically adjust request sizes to max 95 // out network throughput without overloading either the peer or the local node. 96 // 97 // By tracking in real time the latencies and bandwidths peers exhibit for each 98 // packet type, it's possible to prevent overloading by detecting a slowdown on 99 // one type when another type is pushed too hard. 100 // 101 // Similarly, real time measurements also help avoid overloading the local net 102 // connection if our peers would otherwise be capable to deliver more, but the 103 // local link is saturated. In that case, the live measurements will force us 104 // to reduce request sizes until the throughput gets stable. 105 // 106 // Lastly, message rate measurements allows us to detect if a peer is unusually 107 // slow compared to other peers, in which case we can decide to keep it around 108 // or free up the slot so someone closer. 109 // 110 // Since throughput tracking and estimation adapts dynamically to live network 111 // conditions, it's fine to have multiple trackers locally track the same peer 112 // in different subsystem. The throughput will simply be distributed across the 113 // two trackers if both are highly active. 114 type Tracker struct { 115 // capacity is the number of items retrievable per second of a given type. 116 // It is analogous to bandwidth, but we deliberately avoided using bytes 117 // as the unit, since serving nodes also spend a lot of time loading data 118 // from disk, which is linear in the number of items, but mostly constant 119 // in their sizes. 120 // 121 // Callers of course are free to use the item counter as a byte counter if 122 // or when their protocol of choice if capped by bytes instead of items. 123 // (eg. eth.getHeaders vs snap.getAccountRange). 124 capacity map[uint64]float64 125 126 // roundtrip is the latency a peer in general responds to data requests. 127 // This number is not used inside the tracker, but is exposed to compare 128 // peers to each other and filter out slow ones. Note however, it only 129 // makes sense to compare RTTs if the caller caters request sizes for 130 // each peer to target the same RTT. There's no need to make this number 131 // the real networking RTT, we just need a number to compare peers with. 132 roundtrip time.Duration 133 134 lock sync.RWMutex 135 } 136 137 // NewTracker creates a new message rate tracker for a specific peer. An initial 138 // RTT is needed to avoid a peer getting marked as an outlier compared to others 139 // right after joining. It's suggested to use the median rtt across all peers to 140 // init a new peer tracker. 141 func NewTracker(caps map[uint64]float64, rtt time.Duration) *Tracker { 142 if caps == nil { 143 caps = make(map[uint64]float64) 144 } 145 return &Tracker{ 146 capacity: caps, 147 roundtrip: rtt, 148 } 149 } 150 151 // Capacity calculates the number of items the peer is estimated to be able to 152 // retrieve within the allotted time slot. The method will round up any division 153 // errors and will add an additional overestimation ratio on top. The reason for 154 // overshooting the capacity is because certain message types might not increase 155 // the load proportionally to the requested items, so fetching a bit more might 156 // still take the same RTT. By forcefully overshooting by a small amount, we can 157 // avoid locking into a lower-that-real capacity. 158 func (t *Tracker) Capacity(kind uint64, targetRTT time.Duration) int { 159 t.lock.RLock() 160 defer t.lock.RUnlock() 161 162 // Calculate the actual measured throughput 163 throughput := t.capacity[kind] * float64(targetRTT) / float64(time.Second) 164 165 // Return an overestimation to force the peer out of a stuck minima, adding 166 // +1 in case the item count is too low for the overestimator to dent 167 return roundCapacity(1 + capacityOverestimation*throughput) 168 } 169 170 // roundCapacity gives the integer value of a capacity. 171 // The result fits int32, and is guaranteed to be positive. 172 func roundCapacity(cap float64) int { 173 const maxInt32 = float64(1<<31 - 1) 174 return int(math.Min(maxInt32, math.Max(1, math.Ceil(cap)))) 175 } 176 177 // Update modifies the peer's capacity values for a specific data type with a new 178 // measurement. If the delivery is zero, the peer is assumed to have either timed 179 // out or to not have the requested data, resulting in a slash to 0 capacity. This 180 // avoids assigning the peer retrievals that it won't be able to honour. 181 func (t *Tracker) Update(kind uint64, elapsed time.Duration, items int) { 182 t.lock.Lock() 183 defer t.lock.Unlock() 184 185 // If nothing was delivered (timeout / unavailable data), reduce throughput 186 // to minimum 187 if items == 0 { 188 t.capacity[kind] = 0 189 return 190 } 191 // Otherwise update the throughput with a new measurement 192 if elapsed <= 0 { 193 elapsed = 1 // +1 (ns) to ensure non-zero divisor 194 } 195 measured := float64(items) / (float64(elapsed) / float64(time.Second)) 196 197 t.capacity[kind] = (1-measurementImpact)*(t.capacity[kind]) + measurementImpact*measured 198 t.roundtrip = time.Duration((1-measurementImpact)*float64(t.roundtrip) + measurementImpact*float64(elapsed)) 199 } 200 201 // Trackers is a set of message rate trackers across a number of peers with the 202 // goal of aggregating certain measurements across the entire set for outlier 203 // filtering and newly joining initialization. 204 type Trackers struct { 205 trackers map[string]*Tracker 206 207 // roundtrip is the current best guess as to what is a stable round trip time 208 // across the entire collection of connected peers. This is derived from the 209 // various trackers added, but is used as a cache to avoid recomputing on each 210 // network request. The value is updated once every RTT to avoid fluctuations 211 // caused by hiccups or peer events. 212 roundtrip time.Duration 213 214 // confidence represents the probability that the estimated roundtrip value 215 // is the real one across all our peers. The confidence value is used as an 216 // impact factor of new measurements on old estimates. As our connectivity 217 // stabilizes, this value gravitates towards 1, new measurements having 218 // almost no impact. If there's a large peer churn and few peers, then new 219 // measurements will impact it more. The confidence is increased with every 220 // packet and dropped with every new connection. 221 confidence float64 222 223 // tuned is the time instance the tracker recalculated its cached roundtrip 224 // value and confidence values. A cleaner way would be to have a heartbeat 225 // goroutine do it regularly, but that requires a lot of maintenance to just 226 // run every now and again. 227 tuned time.Time 228 229 // The fields below can be used to override certain default values. Their 230 // purpose is to allow quicker tests. Don't use them in production. 231 OverrideTTLLimit time.Duration 232 233 log log.Logger 234 lock sync.RWMutex 235 } 236 237 // NewTrackers creates an empty set of trackers to be filled with peers. 238 func NewTrackers(log log.Logger) *Trackers { 239 return &Trackers{ 240 trackers: make(map[string]*Tracker), 241 roundtrip: rttMaxEstimate, 242 confidence: 1, 243 tuned: time.Now(), 244 OverrideTTLLimit: ttlLimit, 245 log: log, 246 } 247 } 248 249 // Track inserts a new tracker into the set. 250 func (t *Trackers) Track(id string, tracker *Tracker) error { 251 t.lock.Lock() 252 defer t.lock.Unlock() 253 254 if _, ok := t.trackers[id]; ok { 255 return errors.New("already tracking") 256 } 257 t.trackers[id] = tracker 258 t.detune() 259 260 return nil 261 } 262 263 // Untrack stops tracking a previously added peer. 264 func (t *Trackers) Untrack(id string) error { 265 t.lock.Lock() 266 defer t.lock.Unlock() 267 268 if _, ok := t.trackers[id]; !ok { 269 return errors.New("not tracking") 270 } 271 delete(t.trackers, id) 272 return nil 273 } 274 275 // MedianRoundTrip returns the median RTT across all known trackers. The purpose 276 // of the median RTT is to initialize a new peer with sane statistics that it will 277 // hopefully outperform. If it seriously underperforms, there's a risk of dropping 278 // the peer, but that is ok as we're aiming for a strong median. 279 func (t *Trackers) MedianRoundTrip() time.Duration { 280 t.lock.RLock() 281 defer t.lock.RUnlock() 282 283 return t.medianRoundTrip() 284 } 285 286 // medianRoundTrip is the internal lockless version of MedianRoundTrip to be used 287 // by the QoS tuner. 288 func (t *Trackers) medianRoundTrip() time.Duration { 289 // Gather all the currently measured round trip times 290 rtts := make([]float64, 0, len(t.trackers)) 291 for _, tt := range t.trackers { 292 tt.lock.RLock() 293 rtts = append(rtts, float64(tt.roundtrip)) 294 tt.lock.RUnlock() 295 } 296 sort.Float64s(rtts) 297 298 var median time.Duration 299 switch len(rtts) { 300 case 0: 301 median = rttMaxEstimate 302 case 1: 303 median = time.Duration(rtts[0]) 304 default: 305 idx := int(math.Sqrt(float64(len(rtts)))) 306 median = time.Duration(rtts[idx]) 307 } 308 // Restrict the RTT into some QoS defaults, irrelevant of true RTT 309 if median < rttMinEstimate { 310 median = rttMinEstimate 311 } 312 if median > rttMaxEstimate { 313 median = rttMaxEstimate 314 } 315 return median 316 } 317 318 // MeanCapacities returns the capacities averaged across all the added trackers. 319 // The purpose of the mean capacities are to initialize a new peer with some sane 320 // starting values that it will hopefully outperform. If the mean overshoots, the 321 // peer will be cut back to minimal capacity and given another chance. 322 func (t *Trackers) MeanCapacities() map[uint64]float64 { 323 t.lock.RLock() 324 defer t.lock.RUnlock() 325 326 return t.meanCapacities() 327 } 328 329 // meanCapacities is the internal lockless version of MeanCapacities used for 330 // debug logging. 331 func (t *Trackers) meanCapacities() map[uint64]float64 { 332 capacities := make(map[uint64]float64, len(t.trackers)) 333 for _, tt := range t.trackers { 334 tt.lock.RLock() 335 for key, val := range tt.capacity { 336 capacities[key] += val 337 } 338 tt.lock.RUnlock() 339 } 340 for key, val := range capacities { 341 capacities[key] = val / float64(len(t.trackers)) 342 } 343 return capacities 344 } 345 346 // TargetRoundTrip returns the current target round trip time for a request to 347 // complete in.The returned RTT is slightly under the estimated RTT. The reason 348 // is that message rate estimation is a 2 dimensional problem which is solvable 349 // for any RTT. The goal is to gravitate towards smaller RTTs instead of large 350 // messages, to result in a stabler download stream. 351 func (t *Trackers) TargetRoundTrip() time.Duration { 352 // Recalculate the internal caches if it's been a while 353 t.tune() 354 355 // Caches surely recent, return target roundtrip 356 t.lock.RLock() 357 defer t.lock.RUnlock() 358 359 return time.Duration(float64(t.roundtrip) * rttPushdownFactor) 360 } 361 362 // TargetTimeout returns the timeout allowance for a single request to finish 363 // under. The timeout is proportional to the roundtrip, but also takes into 364 // consideration the tracker's confidence in said roundtrip and scales it 365 // accordingly. The final value is capped to avoid runaway requests. 366 func (t *Trackers) TargetTimeout() time.Duration { 367 // Recalculate the internal caches if it's been a while 368 t.tune() 369 370 // Caches surely recent, return target timeout 371 t.lock.RLock() 372 defer t.lock.RUnlock() 373 374 return t.targetTimeout() 375 } 376 377 // targetTimeout is the internal lockless version of TargetTimeout to be used 378 // during QoS tuning. 379 func (t *Trackers) targetTimeout() time.Duration { 380 timeout := time.Duration(ttlScaling * float64(t.roundtrip) / t.confidence) 381 if timeout > t.OverrideTTLLimit { 382 timeout = t.OverrideTTLLimit 383 } 384 return timeout 385 } 386 387 // tune gathers the individual tracker statistics and updates the estimated 388 // request round trip time. 389 func (t *Trackers) tune() { 390 // Tune may be called concurrently all over the place, but we only want to 391 // periodically update and even then only once. First check if it was updated 392 // recently and abort if so. 393 t.lock.RLock() 394 dirty := time.Since(t.tuned) > t.roundtrip 395 t.lock.RUnlock() 396 if !dirty { 397 return 398 } 399 // If an update is needed, obtain a write lock but make sure we don't update 400 // it on all concurrent threads one by one. 401 t.lock.Lock() 402 defer t.lock.Unlock() 403 404 if dirty := time.Since(t.tuned) > t.roundtrip; !dirty { 405 return // A concurrent request beat us to the tuning 406 } 407 // First thread reaching the tuning point, update the estimates and return 408 t.roundtrip = time.Duration((1-tuningImpact)*float64(t.roundtrip) + tuningImpact*float64(t.medianRoundTrip())) 409 t.confidence = t.confidence + (1-t.confidence)/2 410 411 t.tuned = time.Now() 412 t.log.Debug("Recalculated msgrate QoS values", "rtt", t.roundtrip, "confidence", t.confidence, "ttl", t.targetTimeout(), "next", t.tuned.Add(t.roundtrip)) 413 t.log.Trace("Debug dump of mean capacities", "caps", log.Lazy{Fn: t.meanCapacities}) 414 } 415 416 // detune reduces the tracker's confidence in order to make fresh measurements 417 // have a larger impact on the estimates. It is meant to be used during new peer 418 // connections so they can have a proper impact on the estimates. 419 func (t *Trackers) detune() { 420 // If we have a single peer, confidence is always 1 421 if len(t.trackers) == 1 { 422 t.confidence = 1 423 return 424 } 425 // If we have a ton of peers, don't drop the confidence since there's enough 426 // remaining to retain the same throughput 427 if len(t.trackers) >= tuningConfidenceCap { 428 return 429 } 430 // Otherwise drop the confidence factor 431 peers := float64(len(t.trackers)) 432 433 t.confidence = t.confidence * (peers - 1) / peers 434 if t.confidence < rttMinConfidence { 435 t.confidence = rttMinConfidence 436 } 437 t.log.Debug("Relaxed msgrate QoS values", "rtt", t.roundtrip, "confidence", t.confidence, "ttl", t.targetTimeout()) 438 } 439 440 // Capacity is a helper function to access a specific tracker without having to 441 // track it explicitly outside. 442 func (t *Trackers) Capacity(id string, kind uint64, targetRTT time.Duration) int { 443 t.lock.RLock() 444 defer t.lock.RUnlock() 445 446 tracker := t.trackers[id] 447 if tracker == nil { 448 return 1 // Unregister race, don't return 0, it's a dangerous number 449 } 450 return tracker.Capacity(kind, targetRTT) 451 } 452 453 // Update is a helper function to access a specific tracker without having to 454 // track it explicitly outside. 455 func (t *Trackers) Update(id string, kind uint64, elapsed time.Duration, items int) { 456 t.lock.RLock() 457 defer t.lock.RUnlock() 458 459 if tracker := t.trackers[id]; tracker != nil { 460 tracker.Update(kind, elapsed, items) 461 } 462 }