libp2p / specs

Technical specifications for the libp2p networking stack
https://libp2p.io
1.56k stars 273 forks source link

define a new stream multiplexer #377

Open marten-seemann opened 2 years ago

marten-seemann commented 2 years ago

yamux is bad, and mplex is even worse. We need a better stream muxer for TCP connections.

Specifically, we need a muxer that has:

Ideally, all these limits can be set dynamically during the lifetime of a connection (e.g. allowing with small limits, and increasing them later).

The QUIC stream multiplexer fulfills all these properties, and we know that it works well. We could adapt it to work over TCP.

I've started writing up how this could look like: https://hackmd.io/JXgEHrA5SGy7ioKCp-OnGQ. This document will need a lot more love before we can implement something. We will also need to coordinate the implementation work across languages: This effort only makes sense if we have implementations in Go, Rust and JavaScript. Once we have those, we can start phasing out old stream multiplexers, for example by reducing their limits (so performance decreases, but we don't break connectivity to legacy nodes).

Stebalien commented 2 years ago

Note: Yamux has backpressure for new streams (unacked), it just doesn't have a total "stream" limit. But yeah, the latter is likely more flexible than the former.

Other desirable (but not critical) properties:

Possible paths:

mxinden commented 2 years ago

Efficient. IIRC, yamux headers aren't quite as compact as they could be.

Yamux headers are 12 bytes. Say a frame is on average 64 KiB of goodput (totally made up), the overhead would be 12 ÷ (64×1024) = 0.000183105. I would argue that this overhead is negligible.

Add the necessary backpressure to yamux as-is. Honestly, this isn't a huge change and is probably the fastest way forward.

:+1:

Stebalien commented 2 years ago

Yamux headers are 12 bytes. Say a frame is on average 64 KiB of goodput (totally made up), the overhead would be 12 ÷ (64×1024) = 0.000183105. I would argue that this overhead is negligible.

Well.... we have some pretty small writes (20-60 bytes). We have some (disabled) logic for write coalescing, but that merged writes from multiple streams.

But yeah, it probably doesn't matter too much given that, if we're bandwidth constrained, the majority of the bandwidth will be taken up by large writes.

marten-seemann commented 2 years ago

Note: Yamux has backpressure for new streams (unacked), it just doesn't have a total "stream" limit.

I'd phrase it this way: Yamux has the concept of unacked streams, but no way to reasonably act on it (I don't consider the stream muxer resetting a stream to be reasonable).

Say a frame is on average 64 KiB of goodput (totally made up), the overhead would be 12 ÷ (64×1024) = 0.000183105

That frame size is way too large. If you're committing to sending 64 KB without the option to send any other frame (be it a control frame or data for another stream), you're going to have a very unresponsive stream muxer.

We can bring down the frame size of a STREAM frame to 2 bytes in the best case (1 type byte + varint stream ID).

Stebalien commented 2 years ago

I'd phrase it this way: Yamux has the concept of unacked streams, but no way to reasonably act on it (I don't consider the stream muxer resetting a stream to be reasonable).

Well, as long as both sides agree on the same number of unacked streams, you can implement backpressure that way. But yeah, a negotiated value would be better.

If you're committing to sending 64 KB without the option to send any other frame (be it a control frame or data for another stream), you're going to have a very unresponsive stream muxer.

Really? That's the current "max message size". We can make it smaller, but I'm worried that we'll lose performance when, e.g., jumbo-packets are available. Although I guess that's rather unusual.