github.com/bloxroute-labs/bor@v0.1.4/les/costtracker.go (about) 1 // Copyright 2016 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 detailct. 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 les 18 19 import ( 20 "encoding/binary" 21 "math" 22 "sync" 23 "sync/atomic" 24 "time" 25 26 "github.com/maticnetwork/bor/common/mclock" 27 "github.com/maticnetwork/bor/eth" 28 "github.com/maticnetwork/bor/ethdb" 29 "github.com/maticnetwork/bor/les/flowcontrol" 30 "github.com/maticnetwork/bor/log" 31 ) 32 33 const makeCostStats = false // make request cost statistics during operation 34 35 var ( 36 // average request cost estimates based on serving time 37 reqAvgTimeCost = requestCostTable{ 38 GetBlockHeadersMsg: {150000, 30000}, 39 GetBlockBodiesMsg: {0, 700000}, 40 GetReceiptsMsg: {0, 1000000}, 41 GetCodeMsg: {0, 450000}, 42 GetProofsV2Msg: {0, 600000}, 43 GetHelperTrieProofsMsg: {0, 1000000}, 44 SendTxV2Msg: {0, 450000}, 45 GetTxStatusMsg: {0, 250000}, 46 } 47 // maximum incoming message size estimates 48 reqMaxInSize = requestCostTable{ 49 GetBlockHeadersMsg: {40, 0}, 50 GetBlockBodiesMsg: {0, 40}, 51 GetReceiptsMsg: {0, 40}, 52 GetCodeMsg: {0, 80}, 53 GetProofsV2Msg: {0, 80}, 54 GetHelperTrieProofsMsg: {0, 20}, 55 SendTxV2Msg: {0, 16500}, 56 GetTxStatusMsg: {0, 50}, 57 } 58 // maximum outgoing message size estimates 59 reqMaxOutSize = requestCostTable{ 60 GetBlockHeadersMsg: {0, 556}, 61 GetBlockBodiesMsg: {0, 100000}, 62 GetReceiptsMsg: {0, 200000}, 63 GetCodeMsg: {0, 50000}, 64 GetProofsV2Msg: {0, 4000}, 65 GetHelperTrieProofsMsg: {0, 4000}, 66 SendTxV2Msg: {0, 100}, 67 GetTxStatusMsg: {0, 100}, 68 } 69 // request amounts that have to fit into the minimum buffer size minBufferMultiplier times 70 minBufferReqAmount = map[uint64]uint64{ 71 GetBlockHeadersMsg: 192, 72 GetBlockBodiesMsg: 1, 73 GetReceiptsMsg: 1, 74 GetCodeMsg: 1, 75 GetProofsV2Msg: 1, 76 GetHelperTrieProofsMsg: 16, 77 SendTxV2Msg: 8, 78 GetTxStatusMsg: 64, 79 } 80 minBufferMultiplier = 3 81 ) 82 83 const ( 84 maxCostFactor = 2 // ratio of maximum and average cost estimates 85 gfUsageThreshold = 0.5 86 gfUsageTC = time.Second 87 gfRaiseTC = time.Second * 200 88 gfDropTC = time.Second * 50 89 gfDbKey = "_globalCostFactorV3" 90 ) 91 92 // costTracker is responsible for calculating costs and cost estimates on the 93 // server side. It continuously updates the global cost factor which is defined 94 // as the number of cost units per nanosecond of serving time in a single thread. 95 // It is based on statistics collected during serving requests in high-load periods 96 // and practically acts as a one-dimension request price scaling factor over the 97 // pre-defined cost estimate table. 98 // 99 // The reason for dynamically maintaining the global factor on the server side is: 100 // the estimated time cost of the request is fixed(hardcoded) but the configuration 101 // of the machine running the server is really different. Therefore, the request serving 102 // time in different machine will vary greatly. And also, the request serving time 103 // in same machine may vary greatly with different request pressure. 104 // 105 // In order to more effectively limit resources, we apply the global factor to serving 106 // time to make the result as close as possible to the estimated time cost no matter 107 // the server is slow or fast. And also we scale the totalRecharge with global factor 108 // so that fast server can serve more requests than estimation and slow server can 109 // reduce request pressure. 110 // 111 // Instead of scaling the cost values, the real value of cost units is changed by 112 // applying the factor to the serving times. This is more convenient because the 113 // changes in the cost factor can be applied immediately without always notifying 114 // the clients about the changed cost tables. 115 type costTracker struct { 116 db ethdb.Database 117 stopCh chan chan struct{} 118 119 inSizeFactor float64 120 outSizeFactor float64 121 factor float64 122 utilTarget float64 123 minBufLimit uint64 124 125 gfLock sync.RWMutex 126 reqInfoCh chan reqInfo 127 totalRechargeCh chan uint64 128 129 stats map[uint64][]uint64 // Used for testing purpose. 130 } 131 132 // newCostTracker creates a cost tracker and loads the cost factor statistics from the database. 133 // It also returns the minimum capacity that can be assigned to any peer. 134 func newCostTracker(db ethdb.Database, config *eth.Config) (*costTracker, uint64) { 135 utilTarget := float64(config.LightServ) * flowcontrol.FixedPointMultiplier / 100 136 ct := &costTracker{ 137 db: db, 138 stopCh: make(chan chan struct{}), 139 reqInfoCh: make(chan reqInfo, 100), 140 utilTarget: utilTarget, 141 } 142 if config.LightIngress > 0 { 143 ct.inSizeFactor = utilTarget / float64(config.LightIngress) 144 } 145 if config.LightEgress > 0 { 146 ct.outSizeFactor = utilTarget / float64(config.LightEgress) 147 } 148 if makeCostStats { 149 ct.stats = make(map[uint64][]uint64) 150 for code := range reqAvgTimeCost { 151 ct.stats[code] = make([]uint64, 10) 152 } 153 } 154 ct.gfLoop() 155 costList := ct.makeCostList(ct.globalFactor() * 1.25) 156 for _, c := range costList { 157 amount := minBufferReqAmount[c.MsgCode] 158 cost := c.BaseCost + amount*c.ReqCost 159 if cost > ct.minBufLimit { 160 ct.minBufLimit = cost 161 } 162 } 163 ct.minBufLimit *= uint64(minBufferMultiplier) 164 return ct, (ct.minBufLimit-1)/bufLimitRatio + 1 165 } 166 167 // stop stops the cost tracker and saves the cost factor statistics to the database 168 func (ct *costTracker) stop() { 169 stopCh := make(chan struct{}) 170 ct.stopCh <- stopCh 171 <-stopCh 172 if makeCostStats { 173 ct.printStats() 174 } 175 } 176 177 // makeCostList returns upper cost estimates based on the hardcoded cost estimate 178 // tables and the optionally specified incoming/outgoing bandwidth limits 179 func (ct *costTracker) makeCostList(globalFactor float64) RequestCostList { 180 maxCost := func(avgTimeCost, inSize, outSize uint64) uint64 { 181 cost := avgTimeCost * maxCostFactor 182 inSizeCost := uint64(float64(inSize) * ct.inSizeFactor * globalFactor) 183 if inSizeCost > cost { 184 cost = inSizeCost 185 } 186 outSizeCost := uint64(float64(outSize) * ct.outSizeFactor * globalFactor) 187 if outSizeCost > cost { 188 cost = outSizeCost 189 } 190 return cost 191 } 192 var list RequestCostList 193 for code, data := range reqAvgTimeCost { 194 baseCost := maxCost(data.baseCost, reqMaxInSize[code].baseCost, reqMaxOutSize[code].baseCost) 195 reqCost := maxCost(data.reqCost, reqMaxInSize[code].reqCost, reqMaxOutSize[code].reqCost) 196 if ct.minBufLimit != 0 { 197 // if minBufLimit is set then always enforce maximum request cost <= minBufLimit 198 maxCost := baseCost + reqCost*minBufferReqAmount[code] 199 if maxCost > ct.minBufLimit { 200 mul := 0.999 * float64(ct.minBufLimit) / float64(maxCost) 201 baseCost = uint64(float64(baseCost) * mul) 202 reqCost = uint64(float64(reqCost) * mul) 203 } 204 } 205 206 list = append(list, requestCostListItem{ 207 MsgCode: code, 208 BaseCost: baseCost, 209 ReqCost: reqCost, 210 }) 211 } 212 return list 213 } 214 215 // reqInfo contains the estimated time cost and the actual request serving time 216 // which acts as a feed source to update factor maintained by costTracker. 217 type reqInfo struct { 218 // avgTimeCost is the estimated time cost corresponding to maxCostTable. 219 avgTimeCost float64 220 221 // servingTime is the CPU time corresponding to the actual processing of 222 // the request. 223 servingTime float64 224 } 225 226 // gfLoop starts an event loop which updates the global cost factor which is 227 // calculated as a weighted average of the average estimate / serving time ratio. 228 // The applied weight equals the serving time if gfUsage is over a threshold, 229 // zero otherwise. gfUsage is the recent average serving time per time unit in 230 // an exponential moving window. This ensures that statistics are collected only 231 // under high-load circumstances where the measured serving times are relevant. 232 // The total recharge parameter of the flow control system which controls the 233 // total allowed serving time per second but nominated in cost units, should 234 // also be scaled with the cost factor and is also updated by this loop. 235 func (ct *costTracker) gfLoop() { 236 var ( 237 factor, totalRecharge float64 238 gfLog, recentTime, recentAvg float64 239 240 lastUpdate, expUpdate = mclock.Now(), mclock.Now() 241 ) 242 243 // Load historical cost factor statistics from the database. 244 data, _ := ct.db.Get([]byte(gfDbKey)) 245 if len(data) == 8 { 246 gfLog = math.Float64frombits(binary.BigEndian.Uint64(data[:])) 247 } 248 ct.factor = math.Exp(gfLog) 249 factor, totalRecharge = ct.factor, ct.utilTarget*ct.factor 250 251 // In order to perform factor data statistics under the high request pressure, 252 // we only adjust factor when recent factor usage beyond the threshold. 253 threshold := gfUsageThreshold * float64(gfUsageTC) * ct.utilTarget / flowcontrol.FixedPointMultiplier 254 255 go func() { 256 saveCostFactor := func() { 257 var data [8]byte 258 binary.BigEndian.PutUint64(data[:], math.Float64bits(gfLog)) 259 ct.db.Put([]byte(gfDbKey), data[:]) 260 log.Debug("global cost factor saved", "value", factor) 261 } 262 saveTicker := time.NewTicker(time.Minute * 10) 263 264 for { 265 select { 266 case r := <-ct.reqInfoCh: 267 requestServedMeter.Mark(int64(r.servingTime)) 268 requestEstimatedMeter.Mark(int64(r.avgTimeCost / factor)) 269 requestServedTimer.Update(time.Duration(r.servingTime)) 270 relativeCostHistogram.Update(int64(r.avgTimeCost / factor / r.servingTime)) 271 272 now := mclock.Now() 273 dt := float64(now - expUpdate) 274 expUpdate = now 275 exp := math.Exp(-dt / float64(gfUsageTC)) 276 277 // calculate factor correction until now, based on previous values 278 var gfCorr float64 279 max := recentTime 280 if recentAvg > max { 281 max = recentAvg 282 } 283 // we apply continuous correction when MAX(recentTime, recentAvg) > threshold 284 if max > threshold { 285 // calculate correction time between last expUpdate and now 286 if max*exp >= threshold { 287 gfCorr = dt 288 } else { 289 gfCorr = math.Log(max/threshold) * float64(gfUsageTC) 290 } 291 // calculate log(factor) correction with the right direction and time constant 292 if recentTime > recentAvg { 293 // drop factor if actual serving times are larger than average estimates 294 gfCorr /= -float64(gfDropTC) 295 } else { 296 // raise factor if actual serving times are smaller than average estimates 297 gfCorr /= float64(gfRaiseTC) 298 } 299 } 300 // update recent cost values with current request 301 recentTime = recentTime*exp + r.servingTime 302 recentAvg = recentAvg*exp + r.avgTimeCost/factor 303 304 if gfCorr != 0 { 305 // Apply the correction to factor 306 gfLog += gfCorr 307 factor = math.Exp(gfLog) 308 // Notify outside modules the new factor and totalRecharge. 309 if time.Duration(now-lastUpdate) > time.Second { 310 totalRecharge, lastUpdate = ct.utilTarget*factor, now 311 ct.gfLock.Lock() 312 ct.factor = factor 313 ch := ct.totalRechargeCh 314 ct.gfLock.Unlock() 315 if ch != nil { 316 select { 317 case ct.totalRechargeCh <- uint64(totalRecharge): 318 default: 319 } 320 } 321 log.Debug("global cost factor updated", "factor", factor) 322 } 323 } 324 recentServedGauge.Update(int64(recentTime)) 325 recentEstimatedGauge.Update(int64(recentAvg)) 326 totalRechargeGauge.Update(int64(totalRecharge)) 327 328 case <-saveTicker.C: 329 saveCostFactor() 330 331 case stopCh := <-ct.stopCh: 332 saveCostFactor() 333 close(stopCh) 334 return 335 } 336 } 337 }() 338 } 339 340 // globalFactor returns the current value of the global cost factor 341 func (ct *costTracker) globalFactor() float64 { 342 ct.gfLock.RLock() 343 defer ct.gfLock.RUnlock() 344 345 return ct.factor 346 } 347 348 // totalRecharge returns the current total recharge parameter which is used by 349 // flowcontrol.ClientManager and is scaled by the global cost factor 350 func (ct *costTracker) totalRecharge() uint64 { 351 ct.gfLock.RLock() 352 defer ct.gfLock.RUnlock() 353 354 return uint64(ct.factor * ct.utilTarget) 355 } 356 357 // subscribeTotalRecharge returns all future updates to the total recharge value 358 // through a channel and also returns the current value 359 func (ct *costTracker) subscribeTotalRecharge(ch chan uint64) uint64 { 360 ct.gfLock.Lock() 361 defer ct.gfLock.Unlock() 362 363 ct.totalRechargeCh = ch 364 return uint64(ct.factor * ct.utilTarget) 365 } 366 367 // updateStats updates the global cost factor and (if enabled) the real cost vs. 368 // average estimate statistics 369 func (ct *costTracker) updateStats(code, amount, servingTime, realCost uint64) { 370 avg := reqAvgTimeCost[code] 371 avgTimeCost := avg.baseCost + amount*avg.reqCost 372 select { 373 case ct.reqInfoCh <- reqInfo{float64(avgTimeCost), float64(servingTime)}: 374 default: 375 } 376 if makeCostStats { 377 realCost <<= 4 378 l := 0 379 for l < 9 && realCost > avgTimeCost { 380 l++ 381 realCost >>= 1 382 } 383 atomic.AddUint64(&ct.stats[code][l], 1) 384 } 385 } 386 387 // realCost calculates the final cost of a request based on actual serving time, 388 // incoming and outgoing message size 389 // 390 // Note: message size is only taken into account if bandwidth limitation is applied 391 // and the cost based on either message size is greater than the cost based on 392 // serving time. A maximum of the three costs is applied instead of their sum 393 // because the three limited resources (serving thread time and i/o bandwidth) can 394 // also be maxed out simultaneously. 395 func (ct *costTracker) realCost(servingTime uint64, inSize, outSize uint32) uint64 { 396 cost := float64(servingTime) 397 inSizeCost := float64(inSize) * ct.inSizeFactor 398 if inSizeCost > cost { 399 cost = inSizeCost 400 } 401 outSizeCost := float64(outSize) * ct.outSizeFactor 402 if outSizeCost > cost { 403 cost = outSizeCost 404 } 405 return uint64(cost * ct.globalFactor()) 406 } 407 408 // printStats prints the distribution of real request cost relative to the average estimates 409 func (ct *costTracker) printStats() { 410 if ct.stats == nil { 411 return 412 } 413 for code, arr := range ct.stats { 414 log.Info("Request cost statistics", "code", code, "1/16", arr[0], "1/8", arr[1], "1/4", arr[2], "1/2", arr[3], "1", arr[4], "2", arr[5], "4", arr[6], "8", arr[7], "16", arr[8], ">16", arr[9]) 415 } 416 } 417 418 type ( 419 // requestCostTable assigns a cost estimate function to each request type 420 // which is a linear function of the requested amount 421 // (cost = baseCost + reqCost * amount) 422 requestCostTable map[uint64]*requestCosts 423 requestCosts struct { 424 baseCost, reqCost uint64 425 } 426 427 // RequestCostList is a list representation of request costs which is used for 428 // database storage and communication through the network 429 RequestCostList []requestCostListItem 430 requestCostListItem struct { 431 MsgCode, BaseCost, ReqCost uint64 432 } 433 ) 434 435 // getMaxCost calculates the estimated cost for a given request type and amount 436 func (table requestCostTable) getMaxCost(code, amount uint64) uint64 { 437 costs := table[code] 438 return costs.baseCost + amount*costs.reqCost 439 } 440 441 // decode converts a cost list to a cost table 442 func (list RequestCostList) decode(protocolLength uint64) requestCostTable { 443 table := make(requestCostTable) 444 for _, e := range list { 445 if e.MsgCode < protocolLength { 446 table[e.MsgCode] = &requestCosts{ 447 baseCost: e.BaseCost, 448 reqCost: e.ReqCost, 449 } 450 } 451 } 452 return table 453 } 454 455 // testCostList returns a dummy request cost list used by tests 456 func testCostList(testCost uint64) RequestCostList { 457 cl := make(RequestCostList, len(reqAvgTimeCost)) 458 var max uint64 459 for code := range reqAvgTimeCost { 460 if code > max { 461 max = code 462 } 463 } 464 i := 0 465 for code := uint64(0); code <= max; code++ { 466 if _, ok := reqAvgTimeCost[code]; ok { 467 cl[i].MsgCode = code 468 cl[i].BaseCost = testCost 469 cl[i].ReqCost = 0 470 i++ 471 } 472 } 473 return cl 474 }