HPACK: the silent killer (feature) of HTTP/2
blog.cloudflare.com
blog.cloudflare.com
I would add that it also consumes more resources to compress these headers (I know huffman is fast; but it's not as fast as plaintext), but you can probably cache a good portion of it, and then just encode the per-connection values at the end. Although I'd be surprised if Cloudflare has nginx doing that.
Right now, my domain's HTTP header is 192 bytes:
Content-Type: text/html; charset=utf-8
Content-Length: 3884
Connection: keep-alive
Content-Security-Policy: upgrade-insecure-requests
X-Content-Type-Options: nosniff
X-Frame-Options: deny
And more than half of it is just optional security enhancements.So now I'm going to compress that header so that it's ... what, 100 bytes? Even on my site that's incredibly lean and lacks images, it's around 20-30 KiB per page of content. Modern web pages now average being larger than the original DOOM video game (2301 KiB.)
So we're doing all of this to chase roughly a 0.05% reduction in bandwidth per page? We're worrying about the spot on the carpet while ignoring the elephant in the room.
Header compression is cutting our overall bandwidth for HTTP/2.
While it does not look like much, it is still more than increasing the compression level for data would give in many cases.
So, better to do this than try to further compress pages.
But it's still small. Instead of adding a large chunk of complexity to HTTP/2, how much of that 1.4% do you suppose could be gleaned by removing unnecessary headers like Date, Server, X-Powered-By, cf-ray (on Cloudflare), etc?
I understand that for Cloudflare hosting/proxying others' sites, their options are limited and they have to forward their customers' headers along. But I still think adding all of this is a bad idea. Not that it matters of course, it's already done. The IETF basically rubber stamped SPDY.
Access-Control-Allow-Origin is in the static table, and so is Strict-Transport-Security. The rest go into the dynamic table [1], which would be beneficial if they don't change.
Most security headers are hardcodeable, while C-S-P is designed to be a function of the selected resource representation, not some site-wide global. But it's often used in such a way.
`content-security-policy` on inbox.google.com is 1.4K and my `cookie` sent to inbox is around 900B. :-(
Sometimes, for different features, those defaults are permissive, and sometimes they're restrictive, provided the browser supports that header -- and most of them were designed to be backward-compatible so that their truth tables are complicated.
But realistically, consider C-S-P as metadata about the resource representation about to be delivered in the body, instead of a property of the HTTP response itself. In fact, the naming of the header 'Content-[...]' is consistent with the semantics of RFC 7231 [2] which defines a number of different 'Content-[...]' headers, like 'Content-Type' and 'Content-Language'. You can mentally move C-S-P from the HTTP headers into a meta tag (you can do this actually [3]).
[1] https://hn.algolia.com/?query=niftich%20"damn%20header"&type... [2] https://tools.ietf.org/html/rfc7231#section-3.1 [3] https://w3c.github.io/webappsec-csp/#meta-element
In addition, hpack also allows the client to compress their request headers, which decreases your incoming traffic. And in that case, the majority of requests will have very little data in them; ideally, only the path will change between requests.
A 304 should be able to fit into a single frame, unless you're doing things very wrong. You don't even need headers in that case.
> In addition, hpack also allows the client to compress their request headers, which decreases your incoming traffic.
Yeah, that was the DoS concern I had. Lots of web server software checks the incoming header sizes don't exceed a certain length. But with huffman expansion, you can't quite make an INFLATE bomb, but if the implementation doesn't have a backout function during the decompression, you could consume a huge amount of memory by abusing this.
> ideally, only the path will change between requests.
Can you put the path at the bottom of an HTTP/2 request? Otherwise it would be tricky with lots of bit-shifting to reuse existing compressed headers but insert new paths onto the top of each one.
Relatively large request overhead of HTTP/1 caused headaches with icon fonts, sprite maps, and gigantic compiled JS bundles. The aim of HTTP/2 was to make requests so cheap that we could go back to having individual icon files and unbundled ES6 modules.
To me, I see the problems that make HTTP/1 slow, and so I say, "okay let's address those issues." So I'll inline my includes, use spriting if I have a bunch of icons to display, cache things where possible, perform keep alive where I can, drop the use of giant JS libraries, don't use webfonts, use subdomains to avoid cookie pollution, etc. Doesn't take long.
HTTP/2 seems more like, "how can we let people keep their bad practices the same yet try and speed their stuff up a bit?"
And that's fine and I'm okay with the latter existing to make web sites easier and faster. But I don't like the presumption that HTTP/2 is inherently superior to HTTP/1, which is what 99% of these HTTP/2 explanatory articles promote. Complexity adds a dark cost. I think there's a lot of value in someone being able to write a client that can interact with my site in 200 lines of C code with no library dependencies. I feel like we could have put more effort into our libraries and web packages to make HTTP/1 operate much better instead, and kept the wonderful simplicity of HTTP/1.
We already needed multiple requests on the same connection because not even the top sites are one request to load (they are very, very few of these). So all of these "bad practice" improvements are essentially "free".
Also I see no reason to try to enforce habits that were formed to work around an insufficient protocol when we are designing that protocols replacement.
True, it's an interesting trade-off. If you inline your CSS and Javascript like I do, then you can't afford to have 100KiB of CSS and 2.4MiB of Javascript code. But you can at least merge all CSS to one file, all JS to one file, and use a combination of max-age and etags to only pay the cost once.
When I look at site waterfalls today ... cnn.com for example is making 20 separate Javascript file requests against its own domain. They don't have to develop their entire JS backend in a single file, but when deploying to production, they should be merging them.
There's also the corollary that trying to put everything on the same connection (both HTTP/2 stream multiplexing and HTTP/1 file merging) tends to have disastrous consequences when you start experiencing 2% packet loss or higher. Great video with charts on that here: https://www.youtube.com/watch?v=0yzJAKknE_k
> We already needed multiple requests on the same connection because not even the top sites are one request to load (they are very, very few of these)
Oh yeah, it's definitely rare. cnn.com hits me with 110 connections to load their homepage.
My site (byuu.org) does it in a single connection, though. Although I'll admit I haven't won any design awards yet for it ;)
The thing is, I wanted to optimize load times, so I learned the best practices and implemented them. If other sites are concerned about HTTP/1 responsiveness, they could easily do the same. I'm actually paying a small bit due to being HTTPS-only with strong 256-bit crypto and compression necessarily disabled. Everything always seems to be a trade-off, eh?
> Also I see no reason to try to enforce habits that were formed to work around an insufficient protocol when we are designing that protocols replacement.
Well again, to me, things should be as simple as possible. People hate C++ because it has ten billion features and you need a large team of engineers to implement even a basic compiler for it. With C, a single developer can do that.
I'm not saying we should've been stuck with HTTP/1 forever, nor with C forever. But certainly we could have started with HTTP/1.2 before turning the HTTP protocol into something that needs a team of engineers to implement in a product from scratch. Server-Push would have been a very simple header to add, for instance.
Yes, the complexity is hidden to the average web developer that installs nginx, flips a switch for HTTP/2, and calls it a day. But as you and I know, that complexity leads to bugs, vulnerabilities, inconsistencies between implementations, lack of choices in software (monocultures), etc. All very bad things. Further, we're stuck with this for at least 20-40 years now. Just like we still have HTTP/1.0 servers. Every new HTTP revision is one more version your software will need to support. How fun do you think it'll be to write an alternative to cURL once we're at HTTP/7? (assuming new revisions pick up the pace as they've suggested it would.)
Bundling is not a best practice because anything about it is inherently better. It's only better because http is bad at delivering multiple responses efficiently.
I think HTTP/2 is a quite reasonable compromise compared to the ideal, which would be scrapping http and starting over. The biggest change is to multiplex requests over a single TCP socket, something that's been done by many other protocols over the years. The added complexity is needed because the alternative, running modified http over UDP, would break so much of our infrastructure that it's implausible.
https://news.ycombinator.com/item?id=13022596
I had to abandon [client certificates] because of HTTP/2. If one site on the web server uses TLS client auth, and then you go to another site on the same server you receive HTTP 421 Misdirected Request, because of connection reuse. And almost none browser can deal with them correctly (or could not few months ago) - I'm looking at you Chrome, mobile Opera etc... -samsk 2016-Nov-28
Response #1:
date:Wed, 07 Sep 2016 21:41:23 GMT
expires:Wed, 07 Sep 2016 21:41:53 GMT
Response #2:
date:Wed, 07 Sep 2016 21:41:23 GMT
expires:Thu, 07 Sep 2017 21:41:23 GMT
> Although the two expires headers are almost identical, they can only be Huffman compressed, because they can't be matched in full.It seems like it'd be be an easy win to round expiry times to the nearest minute/hour/day. At least for requests such as these where that value is one year in the future.
The fact that a web page loads 40+ domains? Yeah, that's a problem.
The fact that a web page prioritizes serving me an ad so everything blocks until it gets that ad? Yeah, that's a problem.
The fact that a web page downloads 25 Megabytes of data? Yeah, that's a problem.
HTTP headers getting compressed? Not even on my radar.
This benefits a very small number of companies who manage to serve very small, highly optimized payloads and nobody else.
And everybody else pays the cost in terms of complexity, security holes, and implementation bugs.
Yes, exactly!! SPDY, QUIC, et al are designed by Google for the Googles and Facebooks of the world. It is not prioritizing what most sites need most. Hell, most sites don't need this period. Optimizing even the most generous 1.4% that Cloudflare claims isn't going to have any meaning to sites ranked below 100,000 in Alexa (eg 99% of them.) They're not going to run out of bandwidth, and their traffic means they're only using ~3% of a $5/mo VPS.
You want software and website monocultures? This is how you get it. Unbelievable complexity creep. All the fun we've had with OpenSSL vulnerabilities? Get ready for them in the new QUIC-HTTP/2 servers.
TLS alone already basically makes it impossible to write your own web server anymore, but at least that's a necessary evil due to ISP and state actor spying abuses. If you're okay with a world where everyone has the choice between nginx or Apache, I guess that's fine. I'm not, though.
An endpoint might choose to close a connection without sending a GOAWAY for misbehaving peers.
But it does not define how a misbehaving peer could look a like. Or WINDOW_UPDATE or RST_STREAM frames can be received in this state for a short period after a DATA or HEADERS
Actually it's really really hard to get such a time right. Since it's extremly depending on the client.
Also the http/2 state machine is extremly complex vs. the statefulness of a http/1 client (no state machine at all).
Also the spec gives a lot of weight to the server/client where they can treat some errors either as a stream only error or as a full error (i.e. goaway, reconnect/disconnect). Not sure if it is good at all.Push Promises are probably not even useful to the most servers/users. And also have a flaky specification. It's okai to send them between data frames, but it makes no sense to do it.
WebSockets don't work over http/2 and even with a extension they would disconnect every 31-bit/2 streams (or less).
I think that http/2 is a good thing, but at the moment I would rather count on http/2.1 that will get everything correct and made stuff significantly easier.
P.S.: Hpack is probably the easiest to implement/get right
However I agree there are a few rough edges in the HTTP/2 spec. The worst one for me are race conditions around SETTINGS negotiations. If one implementations wants to use a low window size or HPACK buffer size it must still cope with the fact that the remote might send more data before it knows about it - which it can only answer with a stream reset. This in turn might cause many clients to assume that the server is not working or shutting down and to create a new connection and try the same again. The only sensible way for SETTINGS in my opinion is to use the default settings or higher ones - which are unfortunatly so high that it makes HTTP/2 a poor fit for embedded or IoT applications, where it could otherwise shine due to the less demanding binary protocol.
The other thing that I don't like too much are the headers in special header frames without flow control. Getting overall flow control correct (and avoiding that attackers can exploit this) is hard. And from a layering perspective it would have been much clearer if there would have been a lower layer which allows to create flow-controlled multiplexed byte streams and a higher layer which defines on how to transport headers and body data inside these streams. Now we have one big HTTP/2 layer which covers everything. But I guess that was done to get HPACK working. HPACK itself is IMHO ok.
https://www.youtube.com/watch?v=0yzJAKknE_k
Overall, the performance gain is not as much (if any) compared to the hype we hear.
There's a few challenges here…
WebPageTest uses DummyNet to simulate packet loss and in a HTTP/1.x scenario the not all TCP connections in the test will experience packet loss, whereas all the HTTP/2 ones will. Now in the real world if there's packet loss on route how likey is it that TCP connections experience packet loss?
Our H2 implementations are still young, this Cloudflare work is one example, until they made this patch nginx didn't support HPACK and there are other issues in other browsers and servers too.
As the implementations mature and we understand how to make the most of H2 then performance gains will appear - there's already evidence from people like the FT that there are gains to be made now.
Real world experience beats synthetic tests. Shipping counts for a lot.
In the time that some other competitors have been talking about HTTP/2 we've had it out for a year and have been experimenting and improving (server push, HPACK, ...). Clearly, HTTP/2 is not the one-size-fits-all-answer-to-everything, but it amuses me to see competitors not shipping, yet talking a good game. See this sort of thing with HTTP/2, IPv6, ...
"Look at how much faster HTTP/2 is loading this page that is nothing but an array of 64 x 64 icons that are each 16x16 pixels!" -- well of course that's going to be slow with HTTP/1 if each icon is unique and not cached. But who does that? If that pathological case is yours, then sprite the icons and now it's one request, done.
Also the HTTP authors haven't been too worried about header size in the past so I doubt this will change just because we have a new protocol.
:authority:blog.cloudflare.com
:method:GET
:path:/assets/images/cloudflare-sprite-small.png
:scheme:https
Isn't that going to confuse every header parser.No parser is ever going to parse these strings.
HTTP2 supporting libraries see the well-known binary values and libraries without support for HTTP2 will fall-back to HTTP1 anyways where the human-readable form of the request-line is, well, just the request line itself.
And no - this doesn't confuse a header parser since they are not parsed with an HTTP/1 header parser but with the HTTP/2 header tranmission mechanism (HPACK - as shown in the article). Before handing the request to your traditional web app the HTTP/2 server would remove those values from the set of received header fields and instead put them into the request struct.
I don't worry about compression effort too much - deflate has been used for a long time.
Depending on the protocol (or version) traffic may or may not be served via nginx.
Is there any interest in publishing this whole fork? I know you already publish separate modules to github, beyond that is there crossover with nginx plus functionality they don't want to accept?
Open sourcing stuff has a real cost. We do as much as we can.
Could you elaborate, within the limits that you're allowed, on the patches that got rejected? I'm curious as to what they were about and why they didn't make it in.
We tend to publish rejected stuff as patches: https://github.com/cloudflare/sslconfig/tree/master/patches
I'm curious about the Dynamic TLS records patch. Could you or someone else that understands what that's doing elaborate on what's going on?
https://blog.cloudflare.com/hpack-the-silent-killer-feature-...