Neither are particularly useful unless you're a web browser. I'm sticking to HTTP/1.1 myself in all of my code.
Neither are particularly useful unless you're a web browser. I'm sticking to HTTP/1.1 myself in all of my code.
We also plan to closer evaluate HTTP/3 (h3) soon, mainly to see how the promised improvements, like no head of line blocking, turn out in practice for our use case.
So I'd disagree that h2 or h3 is useless "unless you're a web browser"; but sure if you use HTTP only to provide a simple text (e.g., JSON) based API it might not be _that_ relevant. But some benefits like faster TLS setup might even make a difference there, e.g., faster initial startup, fewer packets transmitted, IIRC even packet sniffing should be a bit harder to do with h3. If those benefits are worth it on its own, depends then probably on how easy it's to integrate in your project, iow., if your framework/standard lib/ecosystem/... already supports h3 nicely. We use the rust hyper crate for most of our HTTP* communication, and their h3 implementation is still marked as experimental; and as we're not in a rush we'll just wait until the hyper project stabilizes h3.
What HTTP 2 does is that it makes it possible to send multiple responses in parallel instead of one after each other.
Chunked transfer isn't the issue, I know that would be easily dooable with h1.1, but it would be one serialized & sync stream.
All you're doing is reducing the latency of the first request.
But yeah on package loss there's head of line blocking, which http 3 solves by using UDP and handling the reliability on the higher level.
Your thread is spending its time waiting for the other end to ack before it can send more data, delaying any other kind of work your thread has to do (such as serving other clients). The rate at which it can write doesn't magically increase by attempting to write multiple streams concurrently; quite the opposite. The rate is controlled by the other end, and what you should do if you implement a high-performance TCP service (HTTP or not) is simply disconnect clients that require you to ever block.
Now you could say, "I don't care, I write the data asynchronously". Asynchronous writes are very bad and an anti-pattern, they should typically never be used because they mean you are just queueing data indefinitely until it is eventually written, wasting massive amounts of RAM (if not taking the machine down entirely). Queues in general are serious business and require a careful design to ensure the producer doesn't produce data faster than the consumer, with adequate back-pressure algorithms. It is better to keep track of when you can write and decide on how to handle not being able to write rather than block or queue like an idiot.
Multiplexing with HTTP/2 logically makes the TCP stream which is logically a spsc queue into a pseudo-mpsc queue. Those are also way more difficult to reason about to guarantee fairness and back-pressure, they're pretty much a bottleneck by design. It's just a bad idea any way you look at it.
> TCP must send all data in order. To implement that, all data sent out gets written to a retransmit buffer (which may be dynamically sized, but only up to a maximum).
To a send buffer, at least that's what most TCP implementation name it and with modern surprisingly high reliable networks it's mostly used for that. But yes, again (3rd time now), this is head-of-line blocking which HTTP/3 would solve (and which you also classified as being just useful for web browsers, whatever that even means).
More important, yes while TCP is in-order and if the network is slow and/or drops a lot of packets you will notice that, BUT it can batch multiple HTTP requests into single TCP package and that's why an async concurrent or even parallel application can benefit from using it directly.
> Your thread is spending its time waiting for the other end to ack before it can send more data, delaying any other kind of work your thread has to do (such as serving other clients).
No it doesn't, as we are fully async on all layers with rust's zero-cost async model, e.g. using the async tokio rust crate, it can just continue to handle other requests.
Also, what would change here by using HTTP/1.1? It has the same limitations as it also uses TCP, but additionally can't batch multiple requests into single TCP packets is text based (bad for binary data) and has additional down sites - e.g., slow on start up.
> Now you could say, "I don't care, I write the data asynchronously". Asynchronous writes are very bad and an anti-pattern, they should typically never be used because they mean you are just queueing data indefinitely until it is eventually written, wasting massive amounts of RAM (if not taking the machine down entirely).
Note that 1) we're talking mostly about reads here, and 2) async writes where never that bad and especially nowadays we're not in the spinning-rust-HDDs-only era anymore, so I'd recommend getting that rather dated and never quite right view updated. How would async writes even take down a machine? The kernel just uses the scheduler (or none for NVMes as they have massive amounts of queues and queue depths and can do that themselves) to reorder them in-flight and commit them, once there's to many, your write OPs will block (i.e., unsurprisingly the kernel doesn't allow every unprivileged process to just take down a machine by triggering some async writes) now with a sync model that means the thread is blocked, with async I can just do something else.
> It's just a bad idea any way you look at it.
Nope, not in our experience; and we heavily evaluated that back when adopting our current design. But happy to be actually proved wrong, just prototype the fully async multiplexing on top of HTTP/1.1 and let's see if it's faster. You can either check against our Proxmox Backup Server project directly; it has a benchmark that among other things checks the pure HTTP streaming bandwidth, but we also have a few (a bit dusted) example programs from the very early evaluation time:
https://git.proxmox.com/?p=proxmox-backup.git;a=tree;f=examp...
the initial response headers can arrive as fast as you like, but the body can be streamed, which can have potentially infinite data, and take potentially infinite time, and it can't be interrupted without terminating the connection altogether
(before you say it: streaming response bodies are in no way abnormal, and can't be dismissed as irrelevant)
as http/1 serializes requests over connections (even with chunking or whatever else) clients cannot in general re-use connections for independent requests for this reason (and others)
if a client makes request A at t=1 and request B at t=2, and request A creates an unbounded length SSE response that lasts until t=999, request B should not need to wait until t=1000 to begin, yet this is what http/1 requires
Funnily enough most engineers don't seem to have any idea how TCP works these days.
> HTTP/2 is bad because XYZ, HTTP 3 at least fixes Z
Note giving reasons, and preferably also reasoning behind those reasons. You know, the "arguments"
Here is what clown riding that dunning-kruger peak thinks that "argument" is:
> HTTP/2 is just plain silly, HTTP/3 is a silly idea but implemented correctly.
most often, it helps when there are multiple logical connections muxed over a single physical connection
In real life, browsers open up to four to six HTTP 1.1 connections and maintain them for pipelining. You can increase that number to pretty much anything and enjoy a web full of "permission denied" pages, timeouts and IP bans. I did that for a while when I followed one of those "how to make Firefox faster" guides as a kid, cannot recommend.
That's also four to six three way handshakes, each taking several hundred milliseconds to complete with a risk of failure and retrying, most of which are pretty meaningless, with a hard limit on the amount of individual files one can request at the same time. You'll also have to deal with the traffic congestion engine kicking in multiple times when the WiFi connection degrades. There's also no real way to bypass the TCP slow start for every connection, so you'd better have an excellent connection available or you're artificially restricting your throughput when those streams suddenly start delivering data.
The inline mixing of HTTP/2 removes a whole bunch of restrictions. There are still a bunch of restrictions that only got fixed in HTTP/3 but there's no doubt that HTTP connections improved when HTTP/2 came about.
request-per-connection is a nice, simple, but ultimately naïve model for network communication
All a connection is doing is managing the state of a given byte stream, keeping track of how much was sent on one end and received on the other end. All the packets for all connections go through the same pipes and are even routed the same. The TCP stack sees an IP packet coming in, and in its IP and TCP headers it sees it's from IP:port, so it maps it to the state it is maintaining for that source.
HTTP/3 is using UDP, which is inherently connectionless, but is implementing its own logic of multiple streams/connections. It is not doing anything sensibly different.
"the state it is maintaining for that source" is the "discrete" thing I'm referring to
creating that state requires at least one network round-trip to the remote, and incurs non-negligible costs on the local machine in the application, network, system, and kernel layers
those costs define a per-connection overhead which are almost immediately the main performance bottleneck in systems that do connection-per-request
those costs amortize to zero if you multiplex requests over a single connection
Http/1 Keep-Alive was one the earliest protocol performance improvement. It meant the TCP connection could be reused for the next resource, avoiding the overhead if closing and reopening a new connection for every request to the same server.
You can see http/2 multiplexing as a natural evolution to the keep alive. Instead of doing every request sequentially, just send everything you need at once and let the server figure out in which order to process everything.
Concurrent TCP connections do have an impact on all stateful firewalls in your path, stateful load balancers and of course the server itself. A client that opens dozens of simultaneous TCP to request all the resources on a page all at once would be frowned upon, and quickly hit rate-limits and anti-abuse protections. You can call it ossification of protocols but that is the situation even google could not workaround when specifying http/2 and then quic.
Probably they are referring to head of line blocking: https://en.wikipedia.org/wiki/Head-of-line_blocking
But I suppose it's common for web people to have no idea how networking actually works.
https://web.dev/performance-http2/
With HTTP/1.x, if the client wants to make multiple
parallel requests to improve performance, then multiple TCP
connections must be used (see Using Multiple TCP
Connections ). This behavior is a direct consequence of the
HTTP/1.x delivery model, which ensures that only one
response can be delivered at a time (response queuing) per
connection. Worse, this also results in head-of-line
blocking and inefficient use of the underlying TCP
connection.
The new binary framing layer in HTTP/2 removes these
limitations, and enables full request and response
multiplexing, by allowing the client and server to break
down an HTTP message into independent frames, interleave
them, and then reassemble them on the other end.
it very often makes sense "to do that"As it says in the thing you quoted, the correct approach is to use multiple TCP connections.
The only advantage of HTTP/3 over that is that it only pays for the latency overhead of connection establishment once instead of once per stream. This is a very small optimization that is only relevant in a very narrow set of cases.
making 10 concurrent HTTP GET requests to the same server should not require you to make 10 connections to that server
muxing multiple logical connections/streams over a single physical connection is, like, networking 101 type stuff
you're out of your element donny, go home
You can switch to raw socket and do the TCP syn/ack dance on your own but this is mostly done for network testing/pen testing, as there are more overhead involved, not less.
This is mostly because the OS actually controls the send/receive buffers used by the actual network drivers. The userland program just takes care of filling these buffers and the kernel and drivers take care of emptying them (i.e. sending). TCP flow control is handled by the kernel, freeing the app to focus on its data. This abstraction actually works quite well.
There are frameworks for userland network stacks, when you're working in high performance networking and need to avoid any overhead the kernel could introduce. The drawback is you need to implement the entire stack, including the low level driver, otherwise it would be slower than what you can already do with classic sockets.
Even then, I didn’t mean you should reproduce the Berkeley socket API verbatim (ZeroMQ-style); multiple streams per connection does not sound like a particularly good fit to it (although apparently people have managed to cram SCTP in there[1]?). I only meant that with the current mainstream libraries[2,3,4], establishing a QUIC connection and transmitting bytestreams or datagrams over it seems quite a bit more involved than performing the equivalent TCP actions using sockets.
[1] https://datatracker.ietf.org/doc/html/rfc6458
[2] https://quiche.googlesource.com/quiche
there's plenty to criticize about HTTP/2 and HTTP/3 but they're definitely beneficial in many circumstances