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

Improved H3 for hypercorn. #201

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Improved H3 for hypercorn. #201

wants to merge 3 commits into from

Conversation

rthalley
Copy link

@rthalley rthalley commented Mar 3, 2024

I am a co-maintainer of aioquic, and am also using hypercorn w/ H3 in a project. I've noticed from testing related to an aioquic ticket that the way hypercorn's code is calling aioquic is suboptimal. It's creating a timer on every received packet, and this causes terrible slowdowns on large data transfers. This patch is the start of a revamp to handle timers much more efficiently, and to call aioquic in its preferred patterns. I've had good results interoperating with a trio-based server with hypercorn with Chrome and curl. I have written the asyncio port, which is just a mirror of the trio port, but I haven't tested it yet.

@rthalley
Copy link
Author

rthalley commented Mar 3, 2024

Also besides testing there are still some improvements needed for connection cleanup as the current method I'm using is inefficient. Finally we may want to add "retry mode" to the handshake, as this forces the client to prove they can roundtrip with you before you start serving them.

I'm making this PR now just to give you a heads up and show you what I'm thinking early.

Oh, and thanks for all the good stuff!

@rthalley rthalley changed the title Draft of improved H3 for hypercorn. Improved H3 for hypercorn. Mar 3, 2024
@rthalley
Copy link
Author

rthalley commented Mar 3, 2024

The asyncio support is now hand tested and works, and state management is cleaner and cleanup is now efficient. I still have to add retry mode.

@rthalley
Copy link
Author

rthalley commented Mar 4, 2024

Example performance difference:

Stock hypercorn serving a 20 MiB file to curl:

curl: (56) QUIC: recvmsg() unexpectedly returned -1 (errno=61; Connection refused)
curl --http3 https://localhost:9292/static/foo.bin -o foo  0.10s user 0.13s system 7% cpu 3.234 total

Note that it crashed after only about 8 MiB because all the spawned timers firing are happening so frequently that the loss detection code in aioquic (which isn't expecting to be called without a chance to progress) is backing off so much it exceeds the exponent representation size in a float. Aioquic should guard itself better here, but my point is we are at 3.2 seconds and haven't transferred much. Much of the time spent seems related to task scheduling.

Here's the code from this PR doing the same thing:

curl --http3 https://localhost:9292/static/foo.bin -o foo  0.11s user 0.18s system 21% cpu 1.328 total

This is a successful transfer of all 20 MiB. Granted it's still only around 1/9 the speed of transferring from nginx, but it finishes correctly and is faster than the one that fails.

@rthalley rthalley marked this pull request as ready for review March 8, 2024 16:09
@pgjones
Copy link
Owner

pgjones commented Apr 11, 2024

Many thanks for doing this, I'm short of time to review it at the moment.

@rthalley
Copy link
Author

No worries, it will be there when you are ready for it :)

@pgjones
Copy link
Owner

pgjones commented May 27, 2024

Thanks. I wanted to go in a slightly different direction and utilise a Hypercorn pattern for single tasks. I've done so in ab98383. Thoughts very welcome.

I'm now looking at the retry code - why would this be a configuration option? Is there a reason to disable this?

if self.retry is not None:
if not header.token:
if header.version is None:
return
Copy link
Owner

Choose a reason for hiding this comment

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

Why return here, is a missing version an indication of an error?

Copy link
Author

Choose a reason for hiding this comment

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

Retry is an option because it trades off increased latency for ensuring that you can roundtrip to the client and aren't just being used to amplify an attack (with the start of the TLS handshake) by someone spoofing UDP. I usually turn it on.

Re the missing version... if there is no header token, then this isn't the client retrying, and we have to ask them to retry, but to do that we need the version. If there isn't a version, they've sent us a versionless "short header" packet which is not a sensible thing to do, so we just drop the packet.

@rthalley
Copy link
Author

I'm ok with going a different direction, assuming you reuse/adapt a few other parts of this patch:

  • The session ticket support is not crucial but may be nice, as it allows TLS resumption
  • Retry is good for anti-amplification and should probably be on by default.
  • The features related to connection state and cid tracking.

@pgjones
Copy link
Owner

pgjones commented May 27, 2024

I'll add the retry part in the next release, I've ran out of time today.

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