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  }