-
Notifications
You must be signed in to change notification settings - Fork 20.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
les/utils: UDP rate limiter #21930
les/utils: UDP rate limiter #21930
Conversation
les/utils/limiter.go
Outdated
// selectionWeights calculates the selection weights of a node for both the address and | ||
// the value selector. The selection weight depends on the next request weight or the | ||
// summed weights of recently dropped requests. relWeight is reqWeight/maxWeight. | ||
func selectionWeights(relWeight, value float64) (flatWeight, valueWeight uint64) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About selection weights: I realized that the terminology is a bit misleading because two things are called "weight". I'll think about better names to clean up the confusion. The weight of a request basically means a fixed cost. Note that we don't use CPU time as a cost metric any more, it can be a constant value depending on the request type (or a sum of fixed costs if requests are grouped in a batch).
The other "weight" is the selection weight which is is used by WeightedRandomSelect and it represents the selection probability. The two are actually inversely related, if there are two requests queued and one of them is considered 10 times heavier then it is selected with 10 times less probability. The flatWeight
is 100000000/(requestWeight/sumRequestWeights)
. This is good because if there are many "light" and "heavy" requests then all will be served eventually but the heavy requests won't starve the light ones by slowing down the processing queue. If you send a heavy request in a crowded situation, you should be prepared that probably 10 light requests will be served before yours gets selected.
The valueWeight
is also proportional to value
which is an arbitrary number assigned to the sending node (maybe the name is also not the most intuitive here). It is used to ensure that once a node has been proven useful it can get a good chance even if there is a DDoS attack and there requests coming from many addresses. You can think of these as "trusted" nodes. Let's imagine a situation where request weights are all the same, there are 10000 requesting nodes and 5 of them are trusted. The address selector selects any of the requesting nodes with 1/10000 chance while the value selector selects one of the trusted nodes with 1/5 chance (assuming that they have the same "value", the same level of trust). The two selectors are both used with 1/2 chance so the good nodes still get 1/10 chance while the attackers get 1/20000. This is good because a DDoS attack can at least not starve the proven useful connections.
In reality the "trustedness" of a node is not necessarily a true/false thing so instead a 0..1 "value" is used and a node can get into the value selector with a proportional chance. It also makes sense to take the individual request weights into account similarly to flatWeight
. Therefore valueWeight
is calculated as 100000000*nodeValue/(requestWeight/sumRequestWeights)
. Note that flatWeight
and valueWeight
are never compared to each other because they are used in different selectors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! Now I understand much better!
les/utils/limiter.go
Outdated
address string | ||
value float64 | ||
flatWeight, valueWeight uint64 // current selection weights in the address/value selectors | ||
nextWeight uint // next request weight |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please distinguish the request weight
and selection weight
. It's really easy to confuse people. For example, the nextWeight
, sumWeight
here refer to the request weight while all other weight refers to the selection weight.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps: flatSelection
, and valueSelection
would be better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling the selection weights "selection" also does not really describe what those numbers actually are (sounds more like it has something to do with the actual selected item).
Since we've already had the "weighted random selector" for a long time, I decided to keep the term "weight" for selection weights and renamed request weight to request cost (since we already use the term "cost" for LES requests).
les/utils/limiter.go
Outdated
} else { | ||
f = 0.001 / relWeight | ||
} | ||
f *= 1000000000 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The number is fancy to me. Can we use some meaningful constants? So that the whole conversion logic is more clear
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, now it's a named constant so it's hopefully easier to see what it does.
les/utils/limiter.go
Outdated
close(process) | ||
return process | ||
} | ||
if value > l.maxValue { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the request weight, it's only capped with the max weight and normalize at the selectionWeights
stage.
Can we do the same thing for the value?
Even more, we can cap both value
and requestWeight
with the maximum number, and do the normalization in the selectionWeights
function internally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
les/utils/limiter.go
Outdated
sumWeight: reqWeight, | ||
groupIndex: -1, | ||
} | ||
nq.flatWeight, nq.valueWeight = selectionWeights(float64(reqWeight)/float64(l.maxWeight), value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to do the request weight normalization and value normalization in the function internally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
les/utils/limiter.go
Outdated
ag := l.addresses[nq.address] | ||
ag.update(nq, flatWeight) | ||
l.addressSelect.Update(ag) | ||
if valueWeight != 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here have a corner case, the original valueWeight is not zero, but after that, the weight is 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, this was a bug, I removed the if
clause because it is unnecessary (doing a no-op Update
is not expensive).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing is failed, please fix it
if value > l.maxValue { | ||
l.maxValue = value | ||
} | ||
if value > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the negative value be supported?
if so here https://github.com/ethereum/go-ethereum/pull/21930/files#diff-05f453344a1370c706aca53fb8864362a541daeca84a5c82aecf9344c7af20acR165
we can't just convert it to the uint64.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, negative values are not supported, they wouldn't make any sense. The check is there to avoid a 0/0 division. Checking either value
or maxValue
for > 0
or != 0
would work fine here.
} | ||
} | ||
|
||
func TestLimiter(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TestLimiter: limiter_test.go:181: Request ratio (0.751915) does not match expected value (0.500000)
TestLimiter: limiter_test.go:181: Request ratio (0.121453) does not match expected value (0.250000)
TestLimiter: limiter_test.go:181: Request ratio (0.126632) does not match expected value (0.250000)
TestLimiter: limiter_test.go:181: Request ratio (0.895618) does not match expected value (0.500000)
TestLimiter: limiter_test.go:181: Request ratio (0.052434) does not match expected value (0.250000)
TestLimiter: limiter_test.go:181: Request ratio (0.051948) does not match expected value (0.250000)
TestLimiter: limiter_test.go:181: Request ratio (0.750025) does not match expected value (0.500000)
TestLimiter: limiter_test.go:181: Request ratio (0.126047) does not match expected value (0.250000)
TestLimiter: limiter_test.go:181: Request ratio (0.123928) does not match expected value (0.250000)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rjl493456442 I improved the probabilistic testing method a bit because sometimes some values were slightly out of range. Now the test is super stable and quick.
I couldn't reproduce the results seen above though. Your test errors are not just randomly slightly off, they are explicitly wrong. I ran the test several hundred times locally and I haven't seen anything like this. Are you sure you didn't have any code modifications locally? What parameters did you use with go test
? I tried cpu=8
but didn't make any difference, my local test results are consistently good now.
I don't quite understand the unit tests, but now it passes. I would approve it, the code LGTM and it's used not yet. So I think it's safe to merge it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix https://github.com/ethereum/go-ethereum/pull/21930/files#r542156190
otherwise, LGTM
This PR is a separated part of the les4 code in order to make the review process easier.