les, les/flowcontrol: implement LES/3 (#19329)
les, les/flowcontrol: implement LES/3
This commit is contained in:
@ -18,6 +18,7 @@ package les
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@ -26,6 +27,7 @@ import (
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/les/csvlogger"
|
||||
"github.com/ethereum/go-ethereum/les/flowcontrol"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
@ -52,7 +54,7 @@ var (
|
||||
GetCodeMsg: {0, 80},
|
||||
GetProofsV2Msg: {0, 80},
|
||||
GetHelperTrieProofsMsg: {0, 20},
|
||||
SendTxV2Msg: {0, 66000},
|
||||
SendTxV2Msg: {0, 16500},
|
||||
GetTxStatusMsg: {0, 50},
|
||||
}
|
||||
// maximum outgoing message size estimates
|
||||
@ -66,17 +68,27 @@ var (
|
||||
SendTxV2Msg: {0, 100},
|
||||
GetTxStatusMsg: {0, 100},
|
||||
}
|
||||
minBufLimit = uint64(50000000 * maxCostFactor) // minimum buffer limit allowed for a client
|
||||
minCapacity = (minBufLimit-1)/bufLimitRatio + 1 // minimum capacity allowed for a client
|
||||
// request amounts that have to fit into the minimum buffer size minBufferMultiplier times
|
||||
minBufferReqAmount = map[uint64]uint64{
|
||||
GetBlockHeadersMsg: 192,
|
||||
GetBlockBodiesMsg: 1,
|
||||
GetReceiptsMsg: 1,
|
||||
GetCodeMsg: 1,
|
||||
GetProofsV2Msg: 1,
|
||||
GetHelperTrieProofsMsg: 16,
|
||||
SendTxV2Msg: 8,
|
||||
GetTxStatusMsg: 64,
|
||||
}
|
||||
minBufferMultiplier = 3
|
||||
)
|
||||
|
||||
const (
|
||||
maxCostFactor = 2 // ratio of maximum and average cost estimates
|
||||
gfInitWeight = time.Second * 10
|
||||
gfMaxWeight = time.Hour
|
||||
gfUsageThreshold = 0.5
|
||||
gfUsageTC = time.Second
|
||||
gfDbKey = "_globalCostFactor"
|
||||
gfRaiseTC = time.Second * 200
|
||||
gfDropTC = time.Second * 50
|
||||
gfDbKey = "_globalCostFactorV3"
|
||||
)
|
||||
|
||||
// costTracker is responsible for calculating costs and cost estimates on the
|
||||
@ -94,21 +106,30 @@ type costTracker struct {
|
||||
|
||||
inSizeFactor, outSizeFactor float64
|
||||
gf, utilTarget float64
|
||||
minBufLimit uint64
|
||||
|
||||
gfUpdateCh chan gfUpdate
|
||||
gfLock sync.RWMutex
|
||||
totalRechargeCh chan uint64
|
||||
|
||||
stats map[uint64][]uint64
|
||||
stats map[uint64][]uint64
|
||||
logger *csvlogger.Logger
|
||||
logRecentTime, logRecentAvg, logTotalRecharge, logRelCost *csvlogger.Channel
|
||||
}
|
||||
|
||||
// newCostTracker creates a cost tracker and loads the cost factor statistics from the database
|
||||
func newCostTracker(db ethdb.Database, config *eth.Config) *costTracker {
|
||||
// newCostTracker creates a cost tracker and loads the cost factor statistics from the database.
|
||||
// It also returns the minimum capacity that can be assigned to any peer.
|
||||
func newCostTracker(db ethdb.Database, config *eth.Config, logger *csvlogger.Logger) (*costTracker, uint64) {
|
||||
utilTarget := float64(config.LightServ) * flowcontrol.FixedPointMultiplier / 100
|
||||
ct := &costTracker{
|
||||
db: db,
|
||||
stopCh: make(chan chan struct{}),
|
||||
utilTarget: utilTarget,
|
||||
db: db,
|
||||
stopCh: make(chan chan struct{}),
|
||||
utilTarget: utilTarget,
|
||||
logger: logger,
|
||||
logRelCost: logger.NewMinMaxChannel("relativeCost", true),
|
||||
logRecentTime: logger.NewMinMaxChannel("recentTime", true),
|
||||
logRecentAvg: logger.NewMinMaxChannel("recentAvg", true),
|
||||
logTotalRecharge: logger.NewChannel("totalRecharge", 0.01),
|
||||
}
|
||||
if config.LightBandwidthIn > 0 {
|
||||
ct.inSizeFactor = utilTarget / float64(config.LightBandwidthIn)
|
||||
@ -123,7 +144,16 @@ func newCostTracker(db ethdb.Database, config *eth.Config) *costTracker {
|
||||
}
|
||||
}
|
||||
ct.gfLoop()
|
||||
return ct
|
||||
costList := ct.makeCostList(ct.globalFactor() * 1.25)
|
||||
for _, c := range costList {
|
||||
amount := minBufferReqAmount[c.MsgCode]
|
||||
cost := c.BaseCost + amount*c.ReqCost
|
||||
if cost > ct.minBufLimit {
|
||||
ct.minBufLimit = cost
|
||||
}
|
||||
}
|
||||
ct.minBufLimit *= uint64(minBufferMultiplier)
|
||||
return ct, (ct.minBufLimit-1)/bufLimitRatio + 1
|
||||
}
|
||||
|
||||
// stop stops the cost tracker and saves the cost factor statistics to the database
|
||||
@ -138,16 +168,14 @@ func (ct *costTracker) stop() {
|
||||
|
||||
// makeCostList returns upper cost estimates based on the hardcoded cost estimate
|
||||
// tables and the optionally specified incoming/outgoing bandwidth limits
|
||||
func (ct *costTracker) makeCostList() RequestCostList {
|
||||
maxCost := func(avgTime, inSize, outSize uint64) uint64 {
|
||||
globalFactor := ct.globalFactor()
|
||||
|
||||
cost := avgTime * maxCostFactor
|
||||
inSizeCost := uint64(float64(inSize) * ct.inSizeFactor * globalFactor * maxCostFactor)
|
||||
func (ct *costTracker) makeCostList(globalFactor float64) RequestCostList {
|
||||
maxCost := func(avgTimeCost, inSize, outSize uint64) uint64 {
|
||||
cost := avgTimeCost * maxCostFactor
|
||||
inSizeCost := uint64(float64(inSize) * ct.inSizeFactor * globalFactor)
|
||||
if inSizeCost > cost {
|
||||
cost = inSizeCost
|
||||
}
|
||||
outSizeCost := uint64(float64(outSize) * ct.outSizeFactor * globalFactor * maxCostFactor)
|
||||
outSizeCost := uint64(float64(outSize) * ct.outSizeFactor * globalFactor)
|
||||
if outSizeCost > cost {
|
||||
cost = outSizeCost
|
||||
}
|
||||
@ -155,17 +183,29 @@ func (ct *costTracker) makeCostList() RequestCostList {
|
||||
}
|
||||
var list RequestCostList
|
||||
for code, data := range reqAvgTimeCost {
|
||||
baseCost := maxCost(data.baseCost, reqMaxInSize[code].baseCost, reqMaxOutSize[code].baseCost)
|
||||
reqCost := maxCost(data.reqCost, reqMaxInSize[code].reqCost, reqMaxOutSize[code].reqCost)
|
||||
if ct.minBufLimit != 0 {
|
||||
// if minBufLimit is set then always enforce maximum request cost <= minBufLimit
|
||||
maxCost := baseCost + reqCost*minBufferReqAmount[code]
|
||||
if maxCost > ct.minBufLimit {
|
||||
mul := 0.999 * float64(ct.minBufLimit) / float64(maxCost)
|
||||
baseCost = uint64(float64(baseCost) * mul)
|
||||
reqCost = uint64(float64(reqCost) * mul)
|
||||
}
|
||||
}
|
||||
|
||||
list = append(list, requestCostListItem{
|
||||
MsgCode: code,
|
||||
BaseCost: maxCost(data.baseCost, reqMaxInSize[code].baseCost, reqMaxOutSize[code].baseCost),
|
||||
ReqCost: maxCost(data.reqCost, reqMaxInSize[code].reqCost, reqMaxOutSize[code].reqCost),
|
||||
BaseCost: baseCost,
|
||||
ReqCost: reqCost,
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
type gfUpdate struct {
|
||||
avgTime, servingTime float64
|
||||
avgTimeCost, servingTime float64
|
||||
}
|
||||
|
||||
// gfLoop starts an event loop which updates the global cost factor which is
|
||||
@ -178,45 +218,74 @@ type gfUpdate struct {
|
||||
// total allowed serving time per second but nominated in cost units, should
|
||||
// also be scaled with the cost factor and is also updated by this loop.
|
||||
func (ct *costTracker) gfLoop() {
|
||||
var gfUsage, gfSum, gfWeight float64
|
||||
var gfLog, recentTime, recentAvg float64
|
||||
lastUpdate := mclock.Now()
|
||||
expUpdate := lastUpdate
|
||||
|
||||
data, _ := ct.db.Get([]byte(gfDbKey))
|
||||
if len(data) == 16 {
|
||||
gfSum = math.Float64frombits(binary.BigEndian.Uint64(data[0:8]))
|
||||
gfWeight = math.Float64frombits(binary.BigEndian.Uint64(data[8:16]))
|
||||
if len(data) == 8 {
|
||||
gfLog = math.Float64frombits(binary.BigEndian.Uint64(data[:]))
|
||||
}
|
||||
if gfWeight < float64(gfInitWeight) {
|
||||
gfSum = float64(gfInitWeight)
|
||||
gfWeight = float64(gfInitWeight)
|
||||
}
|
||||
gf := gfSum / gfWeight
|
||||
gf := math.Exp(gfLog)
|
||||
ct.gf = gf
|
||||
totalRecharge := ct.utilTarget * gf
|
||||
ct.gfUpdateCh = make(chan gfUpdate, 100)
|
||||
threshold := gfUsageThreshold * float64(gfUsageTC) * ct.utilTarget / 1000000
|
||||
|
||||
go func() {
|
||||
saveCostFactor := func() {
|
||||
var data [8]byte
|
||||
binary.BigEndian.PutUint64(data[:], math.Float64bits(gfLog))
|
||||
ct.db.Put([]byte(gfDbKey), data[:])
|
||||
log.Debug("global cost factor saved", "value", gf)
|
||||
}
|
||||
saveTicker := time.NewTicker(time.Minute * 10)
|
||||
|
||||
for {
|
||||
select {
|
||||
case r := <-ct.gfUpdateCh:
|
||||
now := mclock.Now()
|
||||
max := r.servingTime * gf
|
||||
if r.avgTime > max {
|
||||
max = r.avgTime
|
||||
if ct.logRelCost != nil && r.avgTimeCost > 1e-20 {
|
||||
ct.logRelCost.Update(r.servingTime * gf / r.avgTimeCost)
|
||||
}
|
||||
if r.servingTime > 1000000000 {
|
||||
ct.logger.Event(fmt.Sprintf("Very long servingTime = %f avgTimeCost = %f costFactor = %f", r.servingTime, r.avgTimeCost, gf))
|
||||
}
|
||||
dt := float64(now - expUpdate)
|
||||
expUpdate = now
|
||||
gfUsage = gfUsage*math.Exp(-dt/float64(gfUsageTC)) + max*1000000/float64(gfUsageTC)
|
||||
exp := math.Exp(-dt / float64(gfUsageTC))
|
||||
// calculate gf correction until now, based on previous values
|
||||
var gfCorr float64
|
||||
max := recentTime
|
||||
if recentAvg > max {
|
||||
max = recentAvg
|
||||
}
|
||||
// we apply continuous correction when MAX(recentTime, recentAvg) > threshold
|
||||
if max > threshold {
|
||||
// calculate correction time between last expUpdate and now
|
||||
if max*exp >= threshold {
|
||||
gfCorr = dt
|
||||
} else {
|
||||
gfCorr = math.Log(max/threshold) * float64(gfUsageTC)
|
||||
}
|
||||
// calculate log(gf) correction with the right direction and time constant
|
||||
if recentTime > recentAvg {
|
||||
// drop gf if actual serving times are larger than average estimates
|
||||
gfCorr /= -float64(gfDropTC)
|
||||
} else {
|
||||
// raise gf if actual serving times are smaller than average estimates
|
||||
gfCorr /= float64(gfRaiseTC)
|
||||
}
|
||||
}
|
||||
// update recent cost values with current request
|
||||
recentTime = recentTime*exp + r.servingTime
|
||||
recentAvg = recentAvg*exp + r.avgTimeCost/gf
|
||||
|
||||
if gfUsage >= gfUsageThreshold*ct.utilTarget*gf {
|
||||
gfSum += r.avgTime
|
||||
gfWeight += r.servingTime
|
||||
if gfCorr != 0 {
|
||||
gfLog += gfCorr
|
||||
gf = math.Exp(gfLog)
|
||||
if time.Duration(now-lastUpdate) > time.Second {
|
||||
gf = gfSum / gfWeight
|
||||
if gfWeight >= float64(gfMaxWeight) {
|
||||
gfSum = gf * float64(gfMaxWeight)
|
||||
gfWeight = float64(gfMaxWeight)
|
||||
}
|
||||
totalRecharge = ct.utilTarget * gf
|
||||
lastUpdate = now
|
||||
ct.gfLock.Lock()
|
||||
ct.gf = gf
|
||||
@ -224,19 +293,22 @@ func (ct *costTracker) gfLoop() {
|
||||
ct.gfLock.Unlock()
|
||||
if ch != nil {
|
||||
select {
|
||||
case ct.totalRechargeCh <- uint64(ct.utilTarget * gf):
|
||||
case ct.totalRechargeCh <- uint64(totalRecharge):
|
||||
default:
|
||||
}
|
||||
}
|
||||
log.Debug("global cost factor updated", "gf", gf, "weight", time.Duration(gfWeight))
|
||||
log.Debug("global cost factor updated", "gf", gf)
|
||||
}
|
||||
}
|
||||
ct.logRecentTime.Update(recentTime)
|
||||
ct.logRecentAvg.Update(recentAvg)
|
||||
ct.logTotalRecharge.Update(totalRecharge)
|
||||
|
||||
case <-saveTicker.C:
|
||||
saveCostFactor()
|
||||
|
||||
case stopCh := <-ct.stopCh:
|
||||
var data [16]byte
|
||||
binary.BigEndian.PutUint64(data[0:8], math.Float64bits(gfSum))
|
||||
binary.BigEndian.PutUint64(data[8:16], math.Float64bits(gfWeight))
|
||||
ct.db.Put([]byte(gfDbKey), data[:])
|
||||
log.Debug("global cost factor saved", "sum", time.Duration(gfSum), "weight", time.Duration(gfWeight))
|
||||
saveCostFactor()
|
||||
close(stopCh)
|
||||
return
|
||||
}
|
||||
@ -275,15 +347,15 @@ func (ct *costTracker) subscribeTotalRecharge(ch chan uint64) uint64 {
|
||||
// average estimate statistics
|
||||
func (ct *costTracker) updateStats(code, amount, servingTime, realCost uint64) {
|
||||
avg := reqAvgTimeCost[code]
|
||||
avgTime := avg.baseCost + amount*avg.reqCost
|
||||
avgTimeCost := avg.baseCost + amount*avg.reqCost
|
||||
select {
|
||||
case ct.gfUpdateCh <- gfUpdate{float64(avgTime), float64(servingTime)}:
|
||||
case ct.gfUpdateCh <- gfUpdate{float64(avgTimeCost), float64(servingTime)}:
|
||||
default:
|
||||
}
|
||||
if makeCostStats {
|
||||
realCost <<= 4
|
||||
l := 0
|
||||
for l < 9 && realCost > avgTime {
|
||||
for l < 9 && realCost > avgTimeCost {
|
||||
l++
|
||||
realCost >>= 1
|
||||
}
|
||||
@ -339,8 +411,8 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// getCost calculates the estimated cost for a given request type and amount
|
||||
func (table requestCostTable) getCost(code, amount uint64) uint64 {
|
||||
// getMaxCost calculates the estimated cost for a given request type and amount
|
||||
func (table requestCostTable) getMaxCost(code, amount uint64) uint64 {
|
||||
costs := table[code]
|
||||
return costs.baseCost + amount*costs.reqCost
|
||||
}
|
||||
@ -360,7 +432,7 @@ func (list RequestCostList) decode(protocolLength uint64) requestCostTable {
|
||||
}
|
||||
|
||||
// testCostList returns a dummy request cost list used by tests
|
||||
func testCostList() RequestCostList {
|
||||
func testCostList(testCost uint64) RequestCostList {
|
||||
cl := make(RequestCostList, len(reqAvgTimeCost))
|
||||
var max uint64
|
||||
for code := range reqAvgTimeCost {
|
||||
@ -372,7 +444,7 @@ func testCostList() RequestCostList {
|
||||
for code := uint64(0); code <= max; code++ {
|
||||
if _, ok := reqAvgTimeCost[code]; ok {
|
||||
cl[i].MsgCode = code
|
||||
cl[i].BaseCost = 0
|
||||
cl[i].BaseCost = testCost
|
||||
cl[i].ReqCost = 0
|
||||
i++
|
||||
}
|
||||
|
Reference in New Issue
Block a user