Skip to content
This repository has been archived by the owner on Feb 23, 2022. It is now read-only.

rfc: add P2P stream proposal #227

Closed
wants to merge 2 commits into from
Closed

Conversation

erikgrinaker
Copy link
Contributor

Rendered

Proposes two changes to the MConnection protocol to better accomodate multi-stream protocols such as QUIC.

Copy link
Contributor

@tac0turtle tac0turtle left a comment

Choose a reason for hiding this comment

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

Thank you for the well written RFC!! I am in favor of this change.

Copy link
Contributor

@cmwaters cmwaters left a comment

Choose a reason for hiding this comment

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

This looks like sound reasoning to me.

I don't know the space too well. Are there any single streamed transports we would want to consider adopting. It seems relatively incongruent with the current format of reactors to not have a transport that more natively supports multiple streams (without HOL blocking and so forth)

Copy link
Contributor

@alexanderbez alexanderbez left a comment

Choose a reason for hiding this comment

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

Great explanation! I'm in favor of the main/ideal proposal. I think adopting QUIC/stream-oriented transports is preferable for many reasons.

}
```

P2P reactor messages should instead be framed by the message sender, using length-prefixed encoding (the de-facto standard for Protobuf). This allows using e.g. a standard `protoio.Reader` and `protoio.Writer` to automate framing. An additional benefit is that the length of a message is known prior to receiving it, such that an appropriately sized buffer can be initialized immediately.
Copy link
Contributor

Choose a reason for hiding this comment

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

This allows using e.g. a standard protoio.Reader and protoio.Writer to automate framing.

This is a very important detail. Might want to add a sentence or two on how this happens.

@xla
Copy link
Contributor

xla commented Dec 2, 2020

Following the developments of ADR-062 and working on the new p2p stack in tendermint-rs, I'm surprised to see this proposal. While stream based transports are on the horizon, it seems odd that it would necessitate retrofitting as it indicates a leak of abstraction at some point in the stack. Unless this is in preparation for the Router to be generic over the transport plugged. Yet if that is the desired outcome, a breaking protocol change for that internal convenience seems induldgent. If it is the declared outcome indeed, a less intrusive approach is to wrap the existing MConn with some thing that quacks and walks like a QUIC.

And there is a lot of mention of quic and its streams, these are byte streams which is not a sensible interface to expose up to reactors, which only depend on PeerID (origin) and message (payload). So the Router should really guard against the transport details. I refer to the Router here as it is the entity in the ADR-062 diagrams. Which also might be the angle (the layer where the Router sits) where innovation has impact as it would determine how ready the codebase is to be migrated onto something like QUIC.

It is mentioned that streams can be set up in an ad-hoc way. Is that something that has been decided/settled on? It seems like a protoocl violation if a peer is trying to initiate communication for a capability not advertised. It feels worthy to flag that this is not just a decision that should be made based on technical merit.

Lastly is it anticipated to solve any of the shortcomings with package transmission/delivery of the current implementation? Which would seem like an attempt to reimplement something like gRPC.

@erikgrinaker
Copy link
Contributor Author

erikgrinaker commented Dec 2, 2020

While stream based transports are on the horizon, it seems odd that it would necessitate retrofitting as it indicates a leak of abstraction at some point in the stack. Unless this is in preparation for the Router to be generic over the transport plugged.

The router, or rather the peer connection, will be "generic" over the transport - the router can communicate with different peers using different transports. We therefore need a transport abstraction. The proposal in ADR-062 is for that abstraction to be byte streams (plural), which is the de-facto low-level networking abstraction across most languages (although normally singular). The main proposal here is to change MConnection from a message-oriented multi-stream transport to a byte-oriented multi-stream transport, which is the same model that QUIC uses.

Yet if that is the desired outcome, a breaking protocol change for that internal convenience seems induldgent. If it is the declared outcome indeed, a less intrusive approach is to wrap the existing MConn with some thing that quacks and walks like a QUIC.

Yes, that is option 2: writing a shim around MConnection that takes reassembled messages and passes them through the P2P transport API as byte streams using length-prefix framing. This has increased resource and complexity costs, so I think this is best suited as a temporary measure as long as we are fairly confident that we want to migrate to QUIC, but it is a viable option.

these are byte streams which is not a sensible interface to expose up to reactors, which only depend on PeerID (origin) and message (payload). So the Router should really guard against the transport details.

Indeed. Reactors will exchange Protobuf messages across Go channels, using peer ID addressing. Only the router cares about low-level network transports, and it is its job to serialize messages, frame them, and pass them over the appropriate byte stream.

It is mentioned that streams can be set up in an ad-hoc way. Is that something that has been decided/settled on?

No, we could do all stream setup during the initial transport handshake, and use QUIC-specific stream handshakes. But explicitly opening and closing channels allows them (and thus reactors) to be dynamic, and I think this is an improvement over the current state where we announce that we have some capability (e.g. consensus) but then just ignore received messages when necessary (e.g. during fast sync). This is somewhat orthogonal though, and we can defer this discussion until later without any real impact on the P2P refactor.

Lastly is it anticipated to solve any of the shortcomings with package transmission/delivery of the current implementation? Which would seem like an attempt to reimplement something like gRPC.

I'm not sure if I understand the question. Internal message scheduling and prioritization will be revamped as part of the router, e.g. to provide fair queueing of messages received from different peers and prioritize certain reactors over others. Apart from this, the MConnection transport characteristics will largely remain unchanged. Could you elaborate on the gRPC comparison?

@erikgrinaker
Copy link
Contributor Author

Are there any single streamed transports we would want to consider adopting. It seems relatively incongruent with the current format of reactors to not have a transport that more natively supports multiple streams (without HOL blocking and so forth).

We do sort of use a single-streamed transport currently, in the sense that TCP is single-streamed and MConnection multiplexes multiple logical streams onto a single TCP stream. UNIX sockets are another example. Most transport protocols, and thus must networking APIs, have traditionally been single-streamed, which is why it's slightly painful to introduce something like QUIC that breaks this convention by using multiple streams.

@tessr
Copy link
Contributor

tessr commented Dec 3, 2020

While stream based transports are on the horizon, it seems odd that it would necessitate retrofitting as it indicates a leak of abstraction at some point in the stack.

I think we're basically dealing with an abstraction mismatch here, between the abstractions we already have and the abstractions we want to have. The mconnection protocol necessitates that multiplexing happen within the "application" abstraction, while something like QUIC expects multiplexing to happen within the "transport" abstraction.

There had been a lot of enthusiasm for a "transport-agnostic" P2P layer, since it seemed like that meant that we could move all the internals around while leaving the protocol in place. This also meant that we could postpone any decisions about what the transport layers should actually be. But I think we've realized that the preexisting mconnection protocol is not compatible with the concept of "transport," at least if transports behave like QUIC, since both QUIC and mconnection expect to handle multiplexing.

It sounds like, as an alternative to this RFC, mconnection and QUIC could be shoehorned together. But this would be at the cost of a lot of complexity (and also likely a performance hit), and that would be a little bit of a shame since the whole point of this P2P refactor is to reduce complexity.

All of that said, if we don't expect to adopt QUIC (or another multi-stream transport), this whole thing is moot. Perhaps we should begin by trying to build confidence around that decision.

@erikgrinaker
Copy link
Contributor Author

erikgrinaker commented Dec 4, 2020

To unblock the P2P refactor I'm going to implement a message-oriented transport API as a temporary solution, and we can revisit once we get alignment on a long-term plan.

@ebuchman
Copy link
Contributor

ebuchman commented Dec 6, 2020

My understanding was that we would add support for QUIC as an alternative to MConnection, stabilize it over a release or two, and then remove the MConnection. So I'm not sure it makes sense to be updating the MConnection if we don't have to, unless for some reason we're trying to keep it around.

As for how to support both in the meantime, the reactors currently use peer.Send(chID, msgBytes) - my impression was that peer would be capable of mapping that API onto either an underlying MConnection (as is) or a new QUIC connection (with streams for channel ids). In the former case, nothing would change. In the latter case, it would select the appropriate QUIC stream, add a length prefix to the msgBytes, and send it off. The QUIC streams would be set up in the peer during the initial connection handshakes through some new stream handshake thing.

Is that what Option 2 is proposing? I didn't get that from Yes, that is option 2: writing a shim around MConnection that takes reassembled messages and passes them through the P2P transport API as byte streams using length-prefix framing.

Maybe the problem is that this works if all we're trying to do is add QUIC, but if we want to more generally refactor for transport agnosticism and change the API exposed to the reactors, we'd need to do more work on or around the MConnection, and thus we'd be otherwise blocked on further refactor until MConnection was fully removed?

Is that all basically right?

@erikgrinaker
Copy link
Contributor Author

erikgrinaker commented Dec 7, 2020

Yeah, that's basically right. Taking full advantage of QUIC requires a somewhat different processing/scheduling model in the P2P stack (basically one goroutine per stream rather than one goroutine per peer), and the most natural way to do this is to expose streams as a first-class transport concept. If we were to implement that, it would make sense to use similar models for QUIC and MConnection streams (i.e. byte streams instead of message streams), such that we could unify higher-level protocol concerns such as framing and handshakes across all transports. There's also the question of whether we implement this now, or defer until we build the QUIC transport.

Is that what Option 2 is proposing?

No, that is sort of the opposite. Option 2 instead proposes to implement the full stream-based transport API with per-stream goroutines, and shoehorn MConnection into this by basically rewriting the transport protocol on-the-fly (which is about as unsatisfying as it sounds).

I'm going to do basically what you outline for now: implement a higher-level SendMessage(chID, msg) and ReceiveMessage() (chID, msg) transport API that fits with the current MConnection semantics and protocol, and also allows us to experiment with a QUIC transport using a different framing/handshake protocol (although inefficiently since there will be a single goroutine consuming all streams per peer). Then we can defer refactoring of the transport bits until we start working on QUIC.

mergify bot pushed a commit to tendermint/tendermint that referenced this pull request Dec 15, 2020
This implements a new `Transport` interface and related types for the P2P refactor in #5670. Previously, `conn.MConnection` was very tightly coupled to the `Peer` implementation -- in order to allow alternative non-multiplexed transports (e.g. QUIC), MConnection has now been moved below the `Transport` interface, as `MConnTransport`, and decoupled from the peer. Since the `p2p` package is not covered by our Go API stability, this is not considered a breaking change, and not listed in the changelog.

The initial approach was to implement the new interface in its final form (which also involved possible protocol changes, see tendermint/spec#227). However, it turned out that this would require a large amount of changes to existing P2P code because of the previous tight coupling between `Peer` and `MConnection` and the reliance on subtleties in the MConnection behavior. Instead, I have broadened the `Transport` interface to expose much of the existing MConnection interface, preserved much of the existing MConnection logic and behavior in the transport implementation, and tried to make as few changes to the rest of the P2P stack as possible. We will instead reduce this interface gradually as we refactor other parts of the P2P stack.

The low-level transport code and protocol (e.g. MConnection, SecretConnection and so on) has not been significantly changed, and refactoring this is not a priority until we come up with a plan for QUIC adoption, as we may end up discarding the MConnection code entirely.

There are no tests of the new `MConnTransport`, as this code is likely to evolve as we proceed with the P2P refactor, but tests should be added before a final release. The E2E tests are sufficient for basic validation in the meanwhile.
@github-actions
Copy link

github-actions bot commented Jan 7, 2021

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@github-actions github-actions bot added the Stale label Jan 7, 2021
@erikgrinaker
Copy link
Contributor Author

Closing this, will be superseded once QUIC work begins.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
S:proposal Status: Proposal Stale
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants