discovery: unify rate.Limiter across all gossip peers - #9607
Conversation
|
Important Review skippedAuto reviews are limited to specific labels. 🏷️ Labels to auto review (1)
Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
| defer returnSema() | ||
|
|
||
| for _, msg := range newUpdatestoSend { | ||
| if err := g.maybeAddMsgDelay(); err != nil { |
There was a problem hiding this comment.
Looking at the diff again, I think ti'll just move this call into sendToPeerSync.
|
Concept ACK. This makes sense -- the rate limit is there to throttle outgoing bandwidth, so it should apply globally rather than per-peer and per-incoming-request. |
4d7b24d to
7662eaa
Compare
|
Pushed up an updated variant where we'll use |
7662eaa to
ceb4ea0
Compare
a383ea9 to
9eaec1f
Compare
yyforyongyu
left a comment
There was a problem hiding this comment.
Looks like the changes got rebased out😂
9eaec1f to
05a6b68
Compare
ceb4ea0 to
dae6da6
Compare
LOL, not sure what happened there... FIxed! |
yyforyongyu
left a comment
There was a problem hiding this comment.
Looking good - I also ran the benchmark tests and it doesn't seem to impact the performance which is nice,
On master,
=== RUN TestLightningNetworkDaemon/tranche00/117-of-275/btcd/async_payments_benchmark
lnd_payment_test.go:757: Benchmark info: Elapsed time: 40.501505292s
lnd_payment_test.go:758: Benchmark info: TPS: 11.925482683119037
=== RUN TestLightningNetworkDaemon/tranche00/225-of-275/btcd/remote_signer-funding_async_payments
lnd_payment_test.go:757: Benchmark info: Elapsed time: 42.424433709s
lnd_payment_test.go:758: Benchmark info: TPS: 11.384948666917278
=== RUN TestLightningNetworkDaemon/tranche00/226-of-275/btcd/remote_signer-funding_async_payments_taproot
lnd_payment_test.go:757: Benchmark info: Elapsed time: 42.670583291s
lnd_payment_test.go:758: Benchmark info: TPS: 11.319273437302027
This branch,
=== RUN TestLightningNetworkDaemon/tranche00/117-of-274/btcd/async_payments_benchmark
lnd_payment_test.go:757: Benchmark info: Elapsed time: 43.210928459s
lnd_payment_test.go:758: Benchmark info: TPS: 11.177727885627055
=== RUN TestLightningNetworkDaemon/tranche00/224-of-274/btcd/remote_signer-funding_async_payments
lnd_payment_test.go:757: Benchmark info: Elapsed time: 43.428684625s
lnd_payment_test.go:758: Benchmark info: TPS: 11.121681537689446
=== RUN TestLightningNetworkDaemon/tranche00/225-of-274/btcd/remote_signer-funding_async_payments_taproot
lnd_payment_test.go:757: Benchmark info: Elapsed time: 46.923330958s
lnd_payment_test.go:758: Benchmark info: TPS: 10.293386896005364
Think once the CI is fixed we are good to go.
|
|
||
| // TestMaybeAddMsgDelay tests that the SyncManager.maybeAddMsgDelay method | ||
| // correctly computes delays based on message size. | ||
| func TestMaybeAddMsgDelay(t *testing.T) { |
| // DefaultMaxUndelayedQueryReplies specifies how many gossip queries we | ||
| // will respond to immediately before starting to delay responses. | ||
| DefaultMaxUndelayedQueryReplies = 10 | ||
| DefaultMaxUndelayedQueryReplies = 50 |
There was a problem hiding this comment.
Done.
Part of me wonders if we should keep a base line msg/s second limit in addition to the bandwidth limit.
There was a problem hiding this comment.
I like the current approach as it deals with network resources directly - what would be a use case for limiting by num of msgs?
There was a problem hiding this comment.
Just thinking about a degenerate case where someone is having us send super small messages, so while we're under the total limit, we're doing more work overall.
There was a problem hiding this comment.
I think this only matters for messages we send out not ones that they send us, so the size of our sent messages is less under their control (still under their control somewhat since they are querying).
There was a problem hiding this comment.
I think it'd be wise to also limit the number of messages so that they don't potentially sit in memory in the case of routing dumps, but this should be addressed by the existing syncer semaphore.
acfda36 to
2bb575f
Compare
That's expected, since this should only affect gossip syncers. We don't rate limit our own commit sig or HTLC messages. What do we think about the set of defaults? Is 100 KB/s reasonable for most nodes? |
1aa2f65 to
c4f23b5
Compare
|
Fixed a bug (was returning early from the send loop). Also modified the structure to allow us to cancel a rate limit reservation if a peer exits. This way we release those tokens/bytes back to the bucket. |
c4f23b5 to
6171441
Compare
yyforyongyu
left a comment
There was a problem hiding this comment.
Close! Some typos and my main comment is whether we should add a simple validation for the user configs, to make sure we won't be blocked on the delay forever.
Also modified the structure to allow us to cancel a rate limit reservation if a peer exits. This way we release those tokens/bytes back to the bucket.
If the peer reconnects I think a new rate limiter will be created with a bucket full of tokens.
| func TestDeriveRateLimitReservation(t *testing.T) { | ||
| // Define standard test parameters for rate limiting. | ||
| bytesPerSec := uint64(1000) // 1000 bytes/second | ||
| bytesBurst := uint64(100) // 100 bytes burst capacity |
There was a problem hiding this comment.
Looks like the burst is smaller than the rate?
There was a problem hiding this comment.
I think this is ok for the test, as we're just making assertions based on the delay value, rather than having the test sleep. In the future I think something like synctest would allow us to actually sleep here to assert messages are eventually sent.
| // DefaultMaxUndelayedQueryReplies specifies how many gossip queries we | ||
| // will respond to immediately before starting to delay responses. | ||
| DefaultMaxUndelayedQueryReplies = 10 | ||
| DefaultMaxUndelayedQueryReplies = 50 |
There was a problem hiding this comment.
I like the current approach as it deals with network resources directly - what would be a use case for limiting by num of msgs?
So there's a global bucket. The change I made handles the case where a peer sleeps to send 65 KB, but then disconnects before they actually send (or disconnects mid sleep). We'll now release that reservation back into the available pool. |
e6bb1b0 to
1166f2c
Compare
There was a problem hiding this comment.
Concept ACK. Reviewed the code and I think this is a good change. I think this needs to be tested under real load during RC to figure out better values. My only concern is that depending on the values, it might be possible that an attacker can get honest peers rate-limited by asking for a graph dump since the limiter is shared. So I think good values here will go a long way. Alternatively, maybe we could note down all the scenarios where peers will request routing dumps and then evaluate if all of those cases are necessary?
FWIW, we still have that global semaphore to limit the number of outstanding gossip filter requests. Moving to a per peer limiter is possible, then then an attacker can just spin up multiple peers to get around it. The original motivation for this change was to move to a global rate limiting value, as in the wild for certain degenerate cases, all the peers were below the rate limit, but in aggregate (hundreds of peers) bandwidth utilization was very high. |
ziggie1984
left a comment
There was a problem hiding this comment.
LGTM, really easy to review change 👌
| bytesPerSecond = DefaultMsgBytesPerSecond | ||
| } | ||
|
|
||
| bytesBurst := cfg.AllotedMsgBytesBurst |
There was a problem hiding this comment.
Should we do some boundary checks for these constants ?
There was a problem hiding this comment.
I added a check in the config validation. We need this value to be above 65 KB.
| peer lnpeer.Peer, nodeID route.Vertex, msgs ...lnwire.Message) error { | ||
|
|
||
| for _, msg := range msgs { | ||
| if err := m.maybeRateLimitMsg(ctx, nodeID, msg); err != nil { |
There was a problem hiding this comment.
I wonder if we should skip rate-limiting when sync=true, imo we should not really rate-limit if we require a peer synchronization.
There was a problem hiding this comment.
IIUC sync=true here just means we should block until the message has been fully sent out.
| } | ||
|
|
||
| // TestSizeableMessage is a test implementation of lnwire.SizeableMessage. | ||
| type TestSizeableMessage struct { |
There was a problem hiding this comment.
why did you not use a mock ?
There was a problem hiding this comment.
Just needed some simpler behavior so I didn't reach for it 🤷
| }) | ||
|
|
||
| // Test that message size affects delay. | ||
| t.Run("larger messages have longer delays", func(t *testing.T) { |
There was a problem hiding this comment.
Nit: seems like we are testing the rateLimiter here, I think we might not really need this testcase ?
There was a problem hiding this comment.
In a sense that's true, but at least we're able to bind the current implementation to a basic property based assumption.
A larger test would need to actually sleep to make sure we're able to eventually send messages. Once we can use synctest, we can add something like that.
| ; The number of confirmations required before processing channel announcements. | ||
| ; gossip.announcement-conf=6 | ||
|
|
||
| ; The allotted bandwdith rate expressed in bytes/second that will be alloacted |
There was a problem hiding this comment.
Nit: alloacted => allocated, bandwdith => bandwidth
In this commit, we revamp the old message based rate limiting. First, we move to meter by bytes/s instead of messages/s. The old logic had an error in that it limited groups of message replies, instead of each message. With this new approach, we'll use the newly added SerializedSize method to implement fine grained bandwidth metering. We need to pick two values, the burst rate, and the msg bytes rate. The burst rate is the max amt that can be sent in a given period of time. We need to set this above 65 KB, or the max msg limit, otherwise no messages can be sent. The bucket starts with this many tokens (bytes). As those are depleted, the amount of tokens is refilled at the msg bytes rate. As conservative values, we've chosen 200 KB as the burst rate, and 100 KB/s as the limit.
We go with the defaults of if no values are set.
1166f2c to
8c3c53f
Compare
In this commit, we revamp the old message based rate limiting. First, we
move to meter by bytes/s instead of messages/s. The old logic had an
error in that it limited groups of message replies, instead of each
message. With this new approach, we'll use the newly added
SerializedSize method to implement fine grained bandwidth metering.
We need to pick two values, the burst rate, and the msg bytes rate. The
burst rate is the max amt that can be sent in a given period of time. We
need to set this above 65 KB, or the max msg limit, otherwise no
messages can be sent. The bucket starts with this many tokens (bytes).
As those are depleted, the amount of tokens is refilled at the msg
bytes rate.
As conservative values, we've chosen 200 KB as the burst rate, and 100
KB/s as the limit.
Depends on #9623