Subject: HTTP/2 feedback/questions

From: "Valentin V. Bartenev" <[email-masked]>
Date: Fri, 24 Jul 2015 20:35:44 +0300

Hello,

I've been working on HTTP/2 implementation in NGINX for the past few months.
As a server-side developer I'd like to share some feedback about the protocol
as well as take a chance to ask some question regarding the specification.

Here they are:

 1. The whole prioritization mechanism looks over-engineered.  The essential
    thing it was designed to do is to determine what request need to be served
    first.

    At the server side we have a few requests and a connection pipe, and all
    we need is the order in which we should send response data.

    Instead we must maintain a dependency tree and support a bunch of
    complicated operations over it, and the tree doesn't have any explicit
    limitation.

    Seems like the KISS principle was forgotten.

 2. A quote from the RFC:

   "an endpoint SHOULD retain stream prioritization state for a period after
    streams become closed.  The longer state is retained, the lower the chance
    that streams are assigned incorrect or default priority values."

    How long this period should be?

    At first, a race condition was introduced, and then such a weak solution
    was given.  It's another signal that the prioritization mechanism isn't in
    a good shape.  Btw, there is no such problem in SPDY.

 3. Another quotes:

   "a relative weight, a number that is used to determine the relative
    proportion of available resources that are assigned to streams dependent
    on the same stream."

   "Streams with the same parent SHOULD be allocated resources proportionally
    based on their weight."

   "Thus, if stream B depends on stream A with weight 4, stream C depends on
    stream A with weight 12, and no progress can be made on stream A, stream B
    ideally receives one-third of the resources allocated to stream C."

    ...there are also other mentions about "resource allocation" and
    "available resources".

    What does it mean to allocate resources proportionally?  What kind of
    resources?

    The solution I've eventually come to is dividing the number of bytes
    already sent in a response by these weights to determine the order of
    following frames with the same dependency.
    
 4. The compression ratio of Huffman encoding in HPACK varies between 1.6
   (in the best case) and ~0.26..6 (in the worst case), and the spec doesn't
    have requirement to avoid Huffman encoding when the ratio is 1 or lower.

    Also there's no number of uncompressed bytes stored, so to determine
    the real length of field we need to decompress it first.  And we cannot
    even have a good optimistic guess, because the ratio varies so much.
    
    This creates a bunch of problems.  The most annoying of them is when
    we actually reject a request due to some limitation and must use
    as lower resources as possible, but we still need to decompress the
    HPACK header block because of stateful nature of compression.

 5. What's the purpose of "Dynamic Table Size Update"?  Why does this thing
    exist?  In what cases it should be used?

 6. Why the header that doesn't fit into dynamic table can be transfered
    with indexing?  What's the purpose of eviction of all fields of dynamic
    table?  

Thanks.

  wbr, Valentin V. Bartenev

---

From: Amos Jeffries <[email-masked]>
Date: Sat, 25 Jul 2015 07:40:41 +1200

> as well as take a chance to ask some question regarding the specification.

Welcome to the circus. Its all fun and games...

>     thing it was designed to do is to determine what request need to be served

Designed to cope with clients demanding that their content be served in
specific orders. In a spec largely driven by browser developers.

If you think about it carefully no one client is going to prioritize
another clients traffic over its own. So all clients will be demanding
top priority, their order, etc.

For servers prioritization is OPTIONAL. Dont mistake it for flow control
which is required, although they are related. Worst case you ignore the
demands and simply accept the sane-looking dependencies. Send whats
available as it becomes available. Then the things depending on those
come into focus as a set of more things to do. Weights make a bit more
sense at that point.

Dont whatever you do block sending something on due to priority while
the connectinon sits idle. Its preference priority, not a requirement.
Send whats ready to send.

Some of us server people will be prioritizing FIFO. Some a simple
dependency ordering. Some a weighting. Some might follow the spec.

... we got clowns and acrobats ...

>     Seems like the KISS principle was forgotten.

Would you believe this *is* the simplest version the meets criteria from
both proponents of dependency and of weighting? no?

A lot of implementations are simply ignoring this section of the spec.
It is optional after all. Or implemented and tested only the parts that
are identical to SPDY (having had the already).

>     How long this period should be?

I was assured infinity was not quite long enough.

The original proposal was for immediate removals from a simple
dependency link from request N+1 to N. So one when data wanted to be
sent for some stream N+1 code could dereference its depndency and see
that N state was closed/missing and N+1 could go ahead. Or dereference
from N+1 and see N had data to push first in preference over the N+1 (or
not yet and N+1 could trickle some out). That N+1 or N could disappear
early was seamless, and no complex operations.

But adding weights and in particular weight redistribution means
everything pointing at N has to stay around so N weight could be
reassigned to them.

I suppose N could be removed after its weight has been reassigned. But
then it might have a prior dependency of its own that reassigns weight
to it later as well. Which would then need passing on, etc.

Keep in mind that no sender from which data is received is obliged to
obey the priority anyway. So streams all over the "tree" may finish
early or hang.

"Yuck" might be the word you are looking for.

>     a good shape.  Btw, there is no such problem in SPDY.

Nod. SPDY also does not account for the levels of muliplexing that
middleware services have to deal with in HTTP/2. Sadly the text is just
mind-boggling. So it does not easily meet the needs that were put
forward as its case for being different from SPDY in the first place.

... swinging on trapeze  ...

>     What does it mean to allocate resources proportionally?  What kind of

The valuable resource of bandwith on the single TCP or TLS connection
being used by all these streams.

>     following frames with the same dependency.

Weights are weird. A weighting 20% vs 30% more than B makes little
difference when its a send/no-send choice being decided. As is common
for a server.

What both mean though is that if A and B are otherwise equal in priority
and dependencies, then A is more important than B so send it first. Then
divide A weight up and seen whats new highest weight fr sending next.
Might be B might be some other stream.

>     even have a good optimistic guess, because the ratio varies so much.

Expect infinity 'cause you have to add CONTINUATION frames decoded
payload if it turns out that was not even the end of the compressed bits.

>     HPACK header block because of stateful nature of compression.

Welcome to the circus ... oh I said that already.

Reality is that the decompressor has to set a limit. The SETTINGS value
negotiates/adverties what that is. It can then limit its memory
allocation size. Any compressed headers exceeding that limit get an HTTP
headers-too-big error response. Same as big HTTP/1 messages do today.

The buffer already allocated for such messages can then be used as a
scratch area to complete the decode. But no worries about
loosing/overwriting decompressed data. Its all garbage at that point.

>     exist?  In what cases it should be used?

To update the size of the dynamic table. The often discussed use case is
when needing to completely change all or most of the contents of the
table. Sending an update to shrink its size to 0 then expanding again,
and starting to fill with new values.
 Lets both ends do a massive memory release and reset instead of
incremental replacing each header stored already.

The other use is for systems with limited resources who dont want to
allocate the default table size, to send a smaller (or larger maybe)
size for use on the rest of the connection. Messages recieved have to be
discarded until both ends sync up.

>     with indexing?  What's the purpose of eviction of all fields of dynamic

I recall KISS being involved there. Its simpler and faster overall to
add, evict, remove than to special-case check on every header.

HTH
Amos

---

From: RUELLAN Herve <[email-masked]>
Date: Fri, 31 Jul 2015 17:13:52 +0000

Hi Valentin,

See my responses for HPack below.

Hervé
________________________________________
From: Valentin V. Bartenev <[email-masked]>
Sent: Friday, July 24, 2015 19:35
To: [email-masked]
Subject: HTTP/2 feedback/questions

Hello,

Here they are:

 2. A quote from the RFC:

 3. Another quotes:

The format of HPACK was chosen for optimizing the byte size. So we didn't add the uncompressed size to the code. It's true that it prevents some optimizations.

One of the purpose is for the receiver to reclaim some memory allocated for the dynamic table. The use case was a server under a heavy load.

As Amos pointed, it slightly easier from an implementation point of view.

Thanks.