Skip to content
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

Merged
merged 10 commits into from
Jan 28, 2021
Merged

les/utils: UDP rate limiter #21930

merged 10 commits into from
Jan 28, 2021

Conversation

zsfelfoldi
Copy link
Contributor

This PR is a separated part of the les4 code in order to make the review process easier.

// 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) {
Copy link
Contributor Author

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.

Copy link
Member

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!

address string
value float64
flatWeight, valueWeight uint64 // current selection weights in the address/value selectors
nextWeight uint // next request weight
Copy link
Member

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.

Copy link
Member

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?

Copy link
Contributor Author

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).

} else {
f = 0.001 / relWeight
}
f *= 1000000000
Copy link
Member

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

Copy link
Contributor Author

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.

close(process)
return process
}
if value > l.maxValue {
Copy link
Member

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.

Copy link
Contributor Author

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 Show resolved Hide resolved
les/utils/limiter.go Show resolved Hide resolved
sumWeight: reqWeight,
groupIndex: -1,
}
nq.flatWeight, nq.valueWeight = selectionWeights(float64(reqWeight)/float64(l.maxWeight), value)
Copy link
Member

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.

Copy link
Contributor Author

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 Show resolved Hide resolved
ag := l.addresses[nq.address]
ag.update(nq, flatWeight)
l.addressSelect.Update(ag)
if valueWeight != 0 {
Copy link
Member

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.

Copy link
Contributor Author

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).

les/utils/limiter.go Show resolved Hide resolved
les/utils/limiter.go Show resolved Hide resolved
Copy link
Member

@rjl493456442 rjl493456442 left a 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 {
Copy link
Member

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.

Copy link
Contributor Author

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.

les/utils/limiter.go Show resolved Hide resolved
les/utils/limiter.go Show resolved Hide resolved
}
}

func TestLimiter(t *testing.T) {
Copy link
Member

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)

Copy link
Contributor Author

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.

@rjl493456442
Copy link
Member

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.

Copy link
Member

@rjl493456442 rjl493456442 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zsfelfoldi zsfelfoldi changed the title les/utils: UDP rate limiter (WIP) les/utils: UDP rate limiter Jan 17, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants