github.com/tdcblockchain/tdcblockchain@v0.0.0-20191111034745-805c65ade158/les/costtracker.go (about)

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