Tag: developers corner

Learn about what LiteSpeed is working on currently, from the point of view of our developers. Explore exciting cutting-edge technologies right along with us. Get a peek behind-the-scenes at some of the decision-making processes that go into creating good software. Our mission is to accelerate the internet, and with this series, you get a glimpse into how we do it.

  • Fast Huffman Encoder

    Fast Huffman Encoder

    Fast Huffman Encoder

    LiteSpeed’s HPACK Huffman Encoder has been optimized: it now runs twice as fast as before. In this article, we present the Fast Huffman Encoder, share a few optimization tricks and pitfalls, and benchmark several Huffman encoder implementations.

    Introduction

    Huffman encoder is used both in HTTP/2 and HTTP/3 stacks (HPACK and QPACK compression, respectively). Speeding up this component thus leads to a double win.

    In our previous blog post, we discussed how we sped up our Huffman decoder. The main idea was that we can process the input two, rather than one, bytes at a time. The same approach can be used to speed up the Huffman encoder.

    Unlike the decoder, the encoder implementations by LiteSpeed, nginx, h2o, and nghttp2 do not share a common ancestor. For that reason, we will benchmark all of them.

    Starting Line

    Before optimization, the main encoder loop in our implementation looks like this:

        while (src != src_end)
        {
            cur_enc_code = encode_table[*src++];
            if (bits_used + cur_enc_code.bits < sizeof(bits) * 8)
            {
                bits <<= cur_enc_code.bits;
                bits |= cur_enc_code.code;
                bits_used += cur_enc_code.bits;
                continue;
            }
            else if (p_dst + sizeof(bits) <= dst_end)
            {
                bits <<= sizeof(bits) * 8 - bits_used;
                bits_used = cur_enc_code.bits - (sizeof(bits) * 8 - bits_used);
                bits |= cur_enc_code.code >> bits_used;
    #if UINTPTR_MAX == 18446744073709551615ull
                *p_dst++ = bits >> 56;
                *p_dst++ = bits >> 48;
                *p_dst++ = bits >> 40;
                *p_dst++ = bits >> 32;
    #endif
                *p_dst++ = bits >> 24;
                *p_dst++ = bits >> 16;
                *p_dst++ = bits >> 8;
                *p_dst++ = bits;
                bits = cur_enc_code.code;   /* OK not to clear high bits */
            }
            else
                return -1;
        }
    

    Where an element of encode_table is as follows:

    struct encode_el
    {
        uint32_t code;
        int  	bits;
    };
    

    Each byte of input is used as an index into encode_table. The associated encoded bit string is added to the accumulator buffer bits, which is an 8-byte or 4-byte integer, depending on the platform. When bits is full, its bytes are written out and the accumulator is reset.

    We want to reduce the number of branches. A useful metric is “number of conditionals per byte of input.” In this loop, we check whether

    1. we’ve run out of input (for every input byte);
    2. the accumulator buffer is full (for every input byte); and
    3. we’ve run out of output (every 8 output bytes).

    Estimating average compression ratio of 0.75, this translates to 2.1 branches per byte of input. Can we do better?

    Big Table

    Since there is no obvious way to reduce the number of conditionals in the processing loop, the next thing to do is to reduce the number of input units: that is, process two bytes at a time instead of one.

    struct henc {
        unsigned lens;
        uint32_t code;
    } hencs[] = {
        [0x0000] = { 26, 0x3FF1FF8 },
        [0x0001] = { 64, 0 },
        /* --- 8< -- snip --- 8< --- */
        [0x6161] = { 10, 0x63 },
        /* --- 8< -- snip --- 8< --- */
        [0xFFFF] = { 64, 0 },
    };
    

    This table has 64K entries. Each element contains the bit string code that represents the Huffman encoding of the two input bytes and its length, lens. Because code is 32 bits wide, some two-byte encodings cannot be represented. They are marked by lens having value 64. In this case, the encoder has to fall back to processing input byte by byte.

    New Loop

    The new loop is broken up into three parts below to place explanation next to the code. You can view the loop in its entirety on GitHub.

    Read

        while (src + sizeof(bits) * 8 / 5 + sizeof(idx) < src_end
                                        && p_dst + sizeof(bits) <= dst_end)
        {
            memcpy(&idx, src, 2);
            henc = &hencs[idx];
            src += 2;
            while (bits_used + henc->lens < sizeof(bits) * 8)
            {
                bits <<= henc->lens;
                bits |= henc->code;
                bits_used += henc->lens;
                memcpy(&idx, src, 2);
                henc = &hencs[idx];
                src += 2;
            }
    

    To avoid source and destination checks inside the loop, they are performed once. The body of the loop is executed if there are enough source bytes to fill the accumulator with shortest Huffman codes and read two more bytes (that translates to 64 / 5 + 2 = 14 bytes on 64-bit platforms) and enough space in the destination buffer to fit all bytes in the accumulator.

    With that out of the way two bytes are read into idx and this value is used to point to the corresponding entry in the hencs table. memcpy is used to guarantee aligned access.

    The loop condition of the inner while loop checks two things:

    1. whether the accumulator will be filled by the current output bit string; and
    2. output bit string validity.

    The reason for making 64 the invalid value in the hencs table is to be able to combine the two checks above into a single expression. If 0 were the invalid value indicator, the condition would have to be more complicated:

    henc->lens && bits_used + henc->lens < sizeof(bits) * 8
    

    This optimization makes a noticeable difference by reducing the number of instructions in the inner loop.

    Write

            if (henc->lens < 64)
            {
                bits <<= sizeof(bits) * 8 - bits_used;
                bits_used = henc->lens - (sizeof(bits) * 8 - bits_used);
                bits |= henc->code >> bits_used;
    #if UINTPTR_MAX == 18446744073709551615ull
                *p_dst++ = bits >> 56;
                *p_dst++ = bits >> 48;
                *p_dst++ = bits >> 40;
                *p_dst++ = bits >> 32;
    #endif
                *p_dst++ = bits >> 24;
                *p_dst++ = bits >> 16;
                *p_dst++ = bits >> 8;
                *p_dst++ = bits;
                bits = henc->code;   /* OK not to clear high bits */
            }
    

    If the current henc entry is valid, the accumulated bytes are written to destination buffer. First, the accumulator bits is filled to the max by adding first part of the current output bit string henc->code.

    Because the destination check has already been performed and we know the number of bytes to write, it is done in a series of shift, assignment, and increment statements.

    henc->code is simply copied to the bits accumulator as is not necessary to clear any bits. The unused bits from henc->code will be processed based on the value of bits_used.

    Note that it costs only one conditional to output a bits-ful of bytes.

    Fallback

            else
            {
                src -= 2;
                break;
            }
        }
    

    The fallback occurs when the current two-byte input sequence translates to an output bit string that is too long. The input pointer is backed up two bytes and the new loop is exited, with control proceeding naturally to the original byte-by-byte loop.

    Count Branches

    The loop checks:

    1. source and destination bounds once per every 8 output bytes;
    2. accumulator fill every two input bytes; and
    3. code validity every 8 output bytes.

    Again, assuming compression ratio to be 0.75, this translates to 1 / (8 / 0.75) + 1 / 2 + 1 / (8 / 0.75) = 0.69 branches per input byte. This is three times fewer branches than in the original loop.

    Benchmarks

    We compare the optimized Huffman encoder (litespeed) to the original (litespeed-orig), as well as to Huffman decoders from nginx, h2o, and nghttp2 projects. I extracted the relevant pieces of code from each project and placed them into separate C files in our Huffman encoder benchmark GitHub repo.

    We will test using four different inputs:

    1. litespeed.txt: This file contains the string “LiteSpeed”. It is used to test small inputs.
    2. x-fb-debug.txt: This is a medium-size string taken from a real x-fb-debug header from the Facebook response QIF file. This is used to test medium-size input.
    3. idle.huff: This file contains the first few paragraphs from The Idle Thoughts of an Idle Fellow by Jerome K. Jerome. It is used to test large input.
    4. x-fb-backslash.huff: This is the same as (2), except with three backslashes inserted in the middle of the string. The backslash is encoded using a 19-bit code, and so the Fast Huffman Encoder will have to fall back to using the original byte-by-bytes loop.

    The table below was generated by the helper program bench.pl. In addition to running the driver and calculating median values, it first calibrates each input file so that the fastest of the encoders runs for at least five seconds. Each encoder runs an input file five times and the median run time is taken. These are the numbers for each encoder in the table below. They are in seconds.

    Results

    Input
    Number of iterations
    litespeed
    litespeed-orig
    nginx
    h2o
    nghttp2
    litespeed.txt 327680000 8.67 8.80 9.61 10.19 20.59
    x-fb-debug.txt 40960000 5.25 11.23 11.13 18.83 30.31
    x-fb-backslash.txt 40960000 7.22 12.65 11.20 19.80 32.21
    idle.txt 2560000 9.32 15.89 17.60 29.50 52.25

    As expected, there is no speedup for litespeed.txt, as that string is shorter than 14 bytes. For x-fb-debug.txt, x-fb-backslash.txt, and idle.txt, litespeed is 2.14, 1.75, and 1.70 times, respectively, faster than litespeed-orig. litespeed Huffman encoder beats nginx, h2o, and nghttp2 in all tests. This is how much faster:

    input
    litespeed
    nginx
    h2o
    nghttp2
    litespeed.txt 1.00 1.11 1.18 2.37
    x-fb-debug.txt 1.00 2.12 3.59 5.77
    x-fb-backslash.txt 1.00 1.55 2.74 4.46
    idle.txt 1.00 1.89 3.17 5.61

    litespeed is about twice as fast (2.12 and 1.89) as nginx on normal input when the string is not tiny (and when it is tiny, its still faster).

    h2o is an also-ran, while the nghttp2 encoder is the slowest of the bunch.

    Conclusion

    The Fast Huffman Encoder uses the same insight as the Fast Huffman Decoder: the subset of commonly used characters in HTTP headers is much smaller than the set of possible characters. For inputs of 12 characters or longer, the encoder virtually always uses the fast path, and the fallback is cheap. At the cost of 512KB of a read-only lookup table, we've doubled the Huffman encoding speed.

    Portability

    This is a bonus section for the dedicated. Welcome!

    Table Compilation: Endianness

    The initial description of the big table was a bit of a white lie. The table actually looks like this:

    #if __BYTE_ORDER == __LITTLE_ENDIAN
    #define I(i,j) ((j<<8)|i)
    #else
    #define I(i,j) ((i<<8)|j)
    #endif
    #if UINTPTR_MAX == 18446744073709551615ull
    #define X32 32
    #else
    #define X32 64
    #endif
    struct henc {
        unsigned lens;
        uint32_t code;
    } hencs[] = {
        [I(0,0)] = {26,0x3FF1FF8},
        [I(1,0)] = {64,0},
        /* --- 8< --- snip --- 8< --- */
        [I(97,97)] = {10,0x63},
        /* --- 8< --- snip --- 8< --- */
        [I(203,97)] = {X32,0xFFFFFBC3},
        /* --- 8< --- snip --- 8< --- */
        [I(255,255)] = {64,0},
    };
    #undef X32
    #undef I
    

    The I() macro is used to generate different indexes on little-endian and big-endian platforms.

    Avoid Undefined Behavior

    The X32 macro effectively disables 32-bit output sequences on 32-bit platforms. This is because of the left shift used when outputting encoded buffer in the big loop. The C standard says the following about bitwise shift operators:

    If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behavior is undefined.

    This means that the result of the following statement may be undefined if we shift by 32:

    bits <<= sizeof(bits) * 8 - bits_used;
    

    To avoid bits_used from being 0 here, the longest code must be smaller than 32 on a 32-bit platform. We guarantee this with the inner while loop.

    Aligned Access

    We read encoder input as a sequence of two-byte integers by copying memory to a local variable:

    memcpy(&idx, src, 2);
    henc = &hencs[idx];
    

    On Intel x86 and x64 architecture, data does not have to be aligned, so one could (or could one?) get away with writing code like this:

    henc = &hencs[ * (uint16_t *) src];
    

    But then this would not work on platforms with stricter alignment requirements, such as SPARC.

    Fortunately, the compiler is smart enough to compile both of these code snippets to exactly the same -- fast -- code.

  • Fast Huffman Decoder

    Fast Huffman Decoder

    Fast Huffman Decoder

    Summary

    The LiteSpeed HPACK Huffman decoder is twice as fast as that used by nginx. This is one of the reasons LiteSpeed Web Server outperforms nginx in a variety of HTTP/2 benchmarks. This article describes how our Huffman decoder works and then tests its performance against that of nginx Huffman decoder. The benchmarked code is available in a public GitHub repository, making the results easy to verify.

    Introduction

    The latest major release of LiteSpeed Web Server includes several new features and optimizations. Our web server outperforms nginx by a factor of 2 to 12 in several readily reproducible benchmarks.

    How do we do it? The truth is, when we are not adding new features, we are optimizing existing code. We 1) examine each module and submodule to find one we can speed up; 2) speed it up; and 3) go back to step 1. Sometimes we wake up because a way to elide a branch in the middle of a tight loop has appeared to us in a dream. Then we turn on the terminal and put down the nocturnal epiphany in code.

    Why do we do it? HTTP/2 is on its way to become the dominant web protocol. HTTP/2 performance is crucial for any modern web server. And, of course, we want to be faster than everyone else.

    This article describes one of the optimizations that made ours the fastest HTTP/2 implementation in the world.

    History

    Well-led software projects are not afraid to borrow good ideas, for the hubris of the not-invented-here syndrome often leads to suboptimal implementations. Such is the story of the HPACK Huffman decoder in today’s major web servers. Apache, LiteSpeed, and nginx have all, until recently, used the same Huffman decoder implementation.

    HPACK is a specialized compression mechanism for HTTP headers, designed specifically for HTTP/2. Header compression is one of HTTP/2’s “killer features:” it reduces bandwidth necessary to transfer headers and thus improves throughput. Huffman coding is used to compress headers that are not in one of the HPACK tables.

    In January of 2014, over a year before the HPACK RFC was published, Tatsuhiro Tsujikawa rewrote Huffman decoder in nghttp2’s implementation of HPACK. (nghttp2 is used by Apache to provide HTTP/2 functionality.) The new code used the method described eleven years earlier by Renato Pajarola in his paper Fast Prefix Code Processing. It is significantly faster than prior methods. The idea is to process compressed data several bits — instead of one bit — at a time. Tatsuhiro chose 4 bits as input length. 4 is a good number. Conveniently, it is exactly half the length of a byte. A shorter sequence would not have sped up the code as much, while a longer sequence would grow the transition table.

    Because HPACK Huffman codes do not change, the transition table is generated once — at compile time. The static table is then used by a simple, robust, and fast decoder function. This was a good idea and a good piece of code, so LiteSpeed, nginx, and others copied (with slight alterations) the code, and moved on.

    Fast or Slow?

    Our own version of the Huffman decoder looks like this (see litespeed.c):

    int
    lshpack_dec_huff_decode_full (const unsigned char *src, int src_len,
                                                unsigned char *dst, int dst_len)
    {
        const unsigned char *p_src = src;
        const unsigned char *const src_end = src + src_len;
        unsigned char *p_dst = dst;
        unsigned char *dst_end = dst + dst_len;
        struct decode_status status = { 0, 1 };
    
        while (p_src != src_end)
        {
            if (p_dst == dst_end)
                return -2;
            if ((p_dst = hdec_huff_dec4bits(*p_src >> 4, p_dst, &status))
                    == NULL)
                return -1;
            if (p_dst == dst_end)
                return -2;
            if ((p_dst = hdec_huff_dec4bits(*p_src & 0xf, p_dst, &status))
                    == NULL)
                return -1;
            ++p_src;
        }
    
        if (!status.eos)
            return -1;
    
        return p_dst - dst;
    }
    

    Even though 4 bits at a time is faster than one bit at a time, the code is still pretty expensive. One way to look at this is to count the number of branches for each byte of input. Inside the while loop above, there are four if statements. This is in addition to conditionals in hdec_huff_dec4bits function (there are two) and in addition to the while loop conditional that checks for end of input. That is seven branches for each byte of input. Can we do better?

    Common Case

    HPACK defines 257 Huffman codes: one for each of the 256 possible bytes and one for the special end-of-stream (EOS) symbol. The codes range from 5 to 30 bits in length: the more likely the value, the shorter its code. What is important to realize is that two thirds of the Huffman codes are virtually never used in HTTP headers. Most of the time, only shorter codes are used. For example, no codes longer than 13 bits are used in the QIF corpus. In this restricted domain, there are more possibilities for optimization.

    Big Table!

    The idea is to generate a large table that can be indexed by the longest of the common Huffman codes. For example, a table with 64K entries can be indexed by a 16-bit value, representing a sequence of 16 input bits. In the common case, a 16-bit sequence is wide enough to contain Huffman codes for one or more output symbols (the average number is 2.14 output symbols per 16 bits).

    Encountering a code longer than 16 bits would require falling back to the original, full Huffman decoder that processes the input 4 bits at a time.

    Fast Huffman Decoder Details

    (To keep the blog post manageable, only the most important snippets of code are included here. For your convenience, the table and the Fast Huffman Decoder function are available in their entirety here: litespeed.c, litespeed-table.h.)

    The Table

    As mentioned above, the table contains 64K entries, for the full range of values that fits into a 16-bit integer, from 0 to 65,535. Each entry contains three things:

    1. Sum of lengths of Huffman codes consumed from the input stream, in bits. This value has a valid range from 5, which is the length of the shortest HPACK Huffman code, to 16, which is the number of bits available for input. Remember, the input bits here are in the index value.
    2. Number of output bytes. The smallest value is 1, while the largest is 3. 3 is how many 5-bit codes can fit into 16 bits.
    3. Output bytes themselves.
    static const struct hdec {
        uint8_t lens;
        uint8_t out[3];
    } hdecs[] = {
        /* 0 */ {(15<<2)|3,{48,48,48}},
        /* 1 */ {(15<<2)|3,{48,48,48}},
        /* 2 */ {(15<<2)|3,{48,48,49}},
        /* 3 */ {(15<<2)|3,{48,48,49}},
        /* --- 8< --- many rows skipped --- 8< --- */
        /* 65532 */ {(15<<2)|1,{123,0,0}},
        /* 65533 */ {(15<<2)|1,{123,0,0}},
        /* 65534 */ {0,{0,0,0,}},
        /* 65535 */ {0,{0,0,0,}},
    };
    

    To keep each element to 4 bytes, Huffman codes lengths and number of output bytes are combined into a single struct member lens. The 3-byte array out holds the output bytes themselves. For example, element 2 above consumes 15 bits of input and outputs 3 bytes: 0, 0, 1. Element 65533 consumes 15 bits of input and outputs 1 byte: {.

    The last two elements of the table have lens equal zero. This indicates that there is no matching Huffman code that fits into 16 bits. The HPACK Huffman code table shows that, indeed, all codes longer than 16 bits begin with either 11111111 11111110 (65534) or 11111111 11111111 (65535) bit sequence. When this code is encountered, the Fast Huffman Decoder has to use the fallback mechanism.

    Main Loop

    The main loop can be described in this high-level pseudocode:

    while (have input)
    {
        fill buf;
        drain buf;
    }
    

    Several input bytes are copied into an intermediate representation. This batch of input bits is then processed: an element in hdecs table is looked up and output is written, until the number of available bits (avail_bits) falls under 16. At this point, buf is refilled.

    Input Batching

    The input bytes are copied into an integer buffer buf, which is the size of a pointer (8 bytes on 64-bit platforms). avail_bits holds the number of input bits available in buf. avail_bits must be at least 16 to be able to use the large table.

    if (src + sizeof(buf) <= src_end)
        /* --- 8< --- omitted for brevity, refer to litespeed.c --- 8< --- */
        ;
    else if (src < src_end)
        do
        {
            buf <<= 8;
            buf |= (uintptr_t) *src++;
            avail_bits += 8;
        }
        while (src < src_end && avail_bits <= sizeof(buf) * 8 - 8);
    else
        break;  /* Normal case terminating condition: out of input */
    

    Filling buf is optimized. If the number of remaining input bytes is at least the size of buf, we can use a switch statement. Otherwise, bytes are added to buf using a while loop.

    Batch Processing

    The batch of input bits in buf is used to look up elements in hdecs. If the output buffer has enough room to fit the maximum number of bytes that could possibly be encoded in buf (64 / 5 = 12 bytes on 64-bit platform), the processing loop can skip destination check.

    if (dst_end - dst >= (ptrdiff_t) (8 * sizeof(buf) / SHORTEST_CODE)
                                                    	&& avail_bits >= 16)
    {
        /* Fast path: don't check destination bounds */
        do
        {
            idx = buf >> (avail_bits - 16);
            hdec = hdecs[idx];
        	dst[0] = hdec.out[0];
        	dst[1] = hdec.out[1];
        	dst[2] = hdec.out[2];
        	dst += hdec.lens & 3;
        	avail_bits -= hdec.lens >> 2;
        }
        while (avail_bits >= 16 && hdec.lens);
        if (avail_bits < 16)
            continue;
        goto slow_path;
    }
    

    The do/while loop above is the main reason the Fast Huffman Decoder is fast. The only conditional is the terminating condition, which checks both the number of available bits and hdec validity at the same time. The average number of consumed bits in hdecs table is 12.76. This translates to 0.63 branches per input byte. Far cry from seven! Of course, there are branch penalties elsewhere — filling the batch, error checking — but this loop’s speed makes up for them in spades.

    The next 16-bit input sequence to use is in the most significant bits of buf. Since idx is uint16_t, there is no need to mask out the low 16 bits of buf: assignment to uint16_t takes care of that.

    All three bytes of the hdec.out array are written to the destination buffer. This saves a conditional at a cost of an unnecessary write. 15% of hdecs entries output 3 bytes, while 84% output 2 bytes. On average, then, 5 times out of 6 an extra byte is written to dst. This is cheaper than a branch.

    As described above, invalid entries have lens equal to zero. In this case, the values of dst and avail_bits are not changed inside the loop. The if statement after the loop is used to differentiate whether we ran out of bits (the common case) or found an invalid code. The latter forces us to fall back to the full Huffman decoder.

    A slower while loop is utilized when there is not much room left in the destination buffer.

    Fallback

    When a long (or invalid) Huffman code is encountered, we have to use the full (original) Huffman decoder to process the rest of the string.

    /* Find previous byte boundary and finish decoding thence. */
    while ((avail_bits & 7) && dst > orig_dst)
        avail_bits += encode_table[ *--dst ].bits;
    src -= avail_bits >> 3;
    r = lshpack_dec_huff_decode_full(src, src_end - src, dst, dst_end - dst);
    if (r >= 0)
        return dst - orig_dst + r;
    else
        return r;
    

    We cannot start processing from the current input position if the Huffman code is not on the byte boundary. We need to rewind the input pointer toward the beginning, looking for the byte boundary. To do that, we use the bytes we already output and the Huffman encoder table. We back up until avail_bits is a multiple of 8. In the worst case, we will stop at the beginning of the input, but that usually does not happen for an input of any appreciable length.

    Benchmarks

    We will compare the Fast Huffman Decoder with Huffman decoder from nginx. To do that, I extracted Huffman decoder functions from ls-hpack and nginx and wrote a small driver program that runs the decoder. See our Huffman decoder GitHub repo. The specific versions of the decoder are here (LiteSpeed) and here (nginx). The latter has been adapted from nginx 1.16.1. You should be able easily to clone this repository and replicate our results.

    The driver is a simple program that reads Huffman-encoder string from a file and decodes it a number of times. The less time a decoder takes to decode the input, the faster the decoder.

    For example:

    sh$ time -p ./comp-dec idle.huff 1000000 litespeed
    real 8.35
    user 8.33
    sys 0.00
    sh$ time -p ./comp-dec idle.huff 1000000 nginx
    real 16.20
    user 16.16
    sys 0.00
    

    It took Fast Huffman Decoder 8.35 seconds to decode Huffman-encoded string in idle.huff one million times, while the nginx Huffman decoder took 16.20 seconds.

    We will test using four different inputs:

    1. litespeed.huff. This is a Huffman-encoded string “LiteSpeed”. It is used to test small inputs.
    2. x-fb-debug.huff. This is a medium-size string taken from a real x-fb-debug header from the Facebook response QIF file. This is used to test medium-size input.
    3. idle.huff. This file contains the first few paragraphs from The Idle Thoughts of an Idle Fellow by Jerome K. Jerome. It is used to test large input.
    4. x-fb-backslash.huff. This is the same as (2), except with a backslash inserted in the middle of the string. The backslash is encoded using a 19-bit code, and so the Fast Huffman Decoder will have to backtrack and fall back to the full decoder. This is used to test how well (or poorly) the Fast Huffman Decoder performs in this case.

    We’ll run each input file five times for each decoder, LiteSpeed and nginx. Then we will compare median average run times. (See the complete numbers at the bottom of this article.)

    Results

    Input nginx LiteSpeed Speed-up
    litespeed.huff 5.05s 2.57s 1.96x
    x-fb-debug.huff 5.33s 2.75s 1.94x
    idle.huff 16.09s 8.22s 1.96x
    x-fb-backslash.huff 4.86s 4.21s 1.15x

    Presented with normal input, the LiteSpeed Fast Huffman Decoder almost doubles the speed of the nginx Huffman decoder. Even when the Fast Huffman Decoder has to backtrack due to uncommon input, it still performs significantly better than the alternative.

    Conclusion

    The LiteSpeed Fast HPACK Huffman Decoder leverages its knowledge of the expected input to optimize for the common case. The fast code path uses two to three times fewer conditionals than the original decoder, making it twice as fast as the original Huffman decoder for all but the most unusual inputs. This is one of the many optimizations we performed — and continue to perform — to make ours the fastest web server, period.

    We also use the Fast Huffman Decoder in our QPACK library, speeding up our HTTP/3 stack. We intend to maintain performance leadership with next-generation HTTP version as well.

    Post Scriptum

    While writing this article, I found two more optimizations that would make the Fast Huffman Decoder even faster. Stay tuned!

    Raw Numbers

    litespeed.huff

    Needed to set number of iterations to 100,000,000 to get into the seconds.

    nginx 5.05 4.94 4.97 5.07 5.07
    LiteSpeed 2.93 2.74 2.78 2.74 2.75

    x-fb-debug.huff

    Number of iterations is set to 10,000,000

    nginx 5.27 5.37 5.31 5.34 5.33
    LiteSpeed 2.75 2.72 2.77 2.74 2.85

    idle.huff

    Number of iterations set to 1,000,000

    nginx 16.09 16.06 16.13 16.09 16.05
    LiteSpeed 8.17 8.25 8.21 8.22 8.29

    x-fb-backslash.huff

    Number of iterations set to 10,000,000

    nginx 4.82 4.86 4.80 4.89 4.86
    LiteSpeed 4.21 4.23 4.21 4.22 4.20

    It is curious that this input differs only by one character from x-fb-debug.huff, yet nginx decoder processes it markedly faster. I haven’t been able — using a limited amount of time — to figure out why.

  • Developer’s Corner: QUIC Kista Interim Day 2

    Developer’s Corner: QUIC Kista Interim Day 2

    Developer's Corner: QUIC Kista Interim Day 2

    Explicit Congestion Notification

    The second day of the Kista Interim Meeting began with the Explicit Congestion Control (ECN) presentation by Magnus Westerlund. The proposal to add ECN to QUIC has been around since at least January of 2017. A year and a half is a long time in the context of an IETF Working Group! In January of this year, Ingemar Johansson presented the proposal to the WG at the Melbourne Interim but it was still deemed to be lacking. This time around, the result was different. Over the last several months, Magnus and Ingemar (colleagues at Ericsson, our gracious host) prepared a comprehensive presentation along with a GitHub pull request that incorporates ECN into the QUIC protocol.

    The Working Group adopted the proposal. Of course, some parts will have to be changed. This is inevitable in a large proposal such as this one. For example, there was a marked pushback against adding a second frame type to carry acknowledgements (named ACK_ECN in the proposal). It looks like the existing ACK frame will be augmented to support additional ECN information. At one point in the discussion, Brian Trammell suggested that we add SMACK (small ACK) and SHACK (short ACK) frames to the protocol. But he was only joking. I think.

    QUIC @Facebook

    Subodh Iyengar shared experiences implementing and deploying IETF QUIC at Facebook. It is rather impressive that Facebook is running IETF QUIC now, while the protocol is still shifting under our feet. Since HTTP/QUIC is not yet ready, the folks at Facebook use QUIC as the transport for HTTP/1.1. The setup carries 100 billion HTTP requests per day.

    QUIC Kista Interim Day 2: Facebook Load Balancer
    Facebook Load Balancer Architecture, taken from Subodh’s slides

    I already knew about Facebook’s pioneering ways. During the Seattle Interim in 2017, Alan Frindell told us that Facebook has been running experiments (in production, of course) with the different HPACK successors. Having worked a bit with the Facebook code, I know that they are not afraid to use the hot-off-the-presses C++14 features.

    This sort of “lessons learned” disclosure is a very valuable contribution both to the working group and to the implementers of the nascent protocol. The Working Group can see what shortcomings there are in the protocol, so that we can address them early. The implementers avoid committing the same errors, saving valuable time.

    Well done, Facebook.

    QUIC Load Balancers

    Martin Duke presented his QUIC Load Balancers draft. The draft proposes a way for QUIC load balancers to update each other’s state to allow them operating correctly even if there are connection migrations. The load balancers would talk to each other using (naturally) QUIC-LB protocol.

    Implicit Stream Open is In

    In my first Kista Interim Summary post, I stated that the requirement to open streams in order is without merit. While I still oppose the implicit open, I have come to see the other side’s perspective. Yes, the implicit open carries more information with each STREAM frame, making it simpler to keep track of some things. I still oppose it, for the drawbacks are more significant.

    This time around, this issue came to a head and we hummed. Outhummed I was. The consensus is to have implicit open. Well, what’s done is done and life goes on. This issue is comparatively small, and it is not like we were assigning stream IDs out of order anyway.

    PNE Negotiation

    Packet number encryption (PNE) negotiation, championed by Praveen Balasubramanian, was rejected by the Working Group. Praveen advocated the data center use case, where PNE is unnecessary and only adds processing overhead. The Working Group’s consensus was that PNE is an intrinsic part of QUIC v1. Not having it means a different protocol, which cannot be called QUIC v1.

    The Bad

    The audio. The audio was bad. The audio was bad as usual, which is also bad. I do not expect a United Nations level of audio setup, but a little extra effort would pay off a lot.

    While the chairs’ microphones were good, the two omnidirectional microphones used to pick up comments from the two or so dozen of the other participants failed to serve adequately. Many people are not used to the idea of projecting their voice. Indeed: when you are speaking to people who are fifteen feet away from you, raising your voice is unnecessary. The end result is that I at times had trouble following the discussion.

    Next Interim: NYC!

    The current plan is to hold the next Interim Meeting in New York City. How convenient: the QUIC brain trust just a train ride away from my house!

    “Live from New York, it is the IETF QUIC!”

  • Developer’s Corner: QUIC Kista Interim Day 1

    Developer’s Corner: QUIC Kista Interim Day 1

    Developer's Corner: QUIC Kista Interim Day 1

    June 6, 2018

    The IETF QUIC Working Group has come together for another Interim meeting. This time, the meeting sponsor is Ericsson: they are providing a conference room and networking at their office in Kista, Sweden. I am participating remotely, which means that I woke up at 3 o’clock this morning to be ready for the 9:30 am meeting held across the ocean.

    Every item on the agenda is pressing, as the Working Group is trying to make the November deadline. The way QUIC transport layer interacts with the TLS stack is changing. The HTTP/QUIC draft is still quite raw (if it were a steak, you’d send it back). We are trying to reach consensus on as many issues as possible and nail down more stuff than we will add this time around, thereby making progress.

    Editors’ Update

    Martin Thomson’s report was optimistic. Since our last meeting at the IETF 101, the editors closed many issues in GitHub and marked others v2. The latter means that these issues are postponed until the second version of IETF QUIC, whenever that turns out to be.

    Stream 0 Redesign

    Eric “EKR” Recorla’s DTLS presentation at the IETF 101 created a lot of discussion. EKR showed that the current QUIC drafts have a fundamental flaw: the transport part of QUIC depends on the crypto layer and the crypto layer depends on the QUIC transport. This makes several things quite painful. Eric’s own DTLS proposal, while not accepted by the Working Group at the time, resulted in the Chairs (Mark Nottingham and Lars Eggert) empaneling a special Stream 0 Design Team. This team, led by EKR and Ian Swett, worked out a novel, consensus approach to address the stream 0 mess.

    Bye, Stream 0!

    Remember the “CREAMs and CRACKs?” The new design uses dedicated frame types (instead of a special QUIC stream) to carry handshake messages. This allows for significant layering simplification, going from this:

    QUIC Kista Interim Day 1

     

    to this:

    QUIC Kista Interim Day 1

    (Both diagrams are borrowed from the design document linked above.)

    Besides the new CRYPTO_HS frame type (I guess they did not have the chutzpah to call the new type CREAM), we see that the TLS record layer is missing. This is the novel part of the design. Since QUIC frames are themselves a type of a record — in the sense that they have boundaries — the TLS record structure can be skipped and TLS messages are carried directly in the new QUIC frames.

    The risk of this approach is dependency on TLS libraries exposing a new API to be able to access TLS messages outside of records. The good news is that OpenSSL and BoringSSL have agreed to accommodate us, while a few smaller implementations have already added such support.

    Packet Number Spaces

    Another interesting part of the Design Team’s proposal is that each encryption level (there are three) gets its own packet numbers. This is something new and was met with scepticism by some.

    The proposal — or rather, its direction — was adopted with strong support, modulo the packet number discussion, which is to be revisited.

    HTTP

    The state of HTTP over QUIC Internet Draft was presented by Mike Bishop, the draft’s author. Mike covered three topics: changes to HTTP frame types, self-describing streams, and stream priority trees.

    Frame Type Changes

    Out of all HTTP frame types in HTTP over QUIC (HQ), only the PRIORITY frame uses flags. Thus, the flags field is removed from the generic HTTP frame.

    Self-Describing Streams

    HQ uses three special unidirectional streams:

    • Control stream;
    • QPACK encoder stream; and
    • QPACK decoder stream.

    Multiply this by two (client and server) and you get six streams that an HQ implementation deals with in a special manner. Mike’s proposal is to have unidirectional streams in HQ have a leading type byte. Based on its value, the application will know what stream type it is: a control stream, a decoder stream, and so on. This would make it, in the presenter’s own words, simpler. This is to satisfy the principle that

    Application layer shouldn’t need to “grab” a particular stream by ID.

    I don’t know whence comes this principle. It sounds rather meritless to me. I had to speak up. My argument against this proposal went as follows.

    The HQ layer installs different stream handlers based on stream type. The control stream does one thing, the encoder stream another, while the regular HTTP message streams are handled in yet another way. A special stream ID — 2, 3, 6, 7, 10, or 11 — allows to initialize a stream with the correct handler as soon as the first stream frame arrives. On the other hand, having to examine the first byte to initialize the stream forces one to handle the case when the first STREAM frame for a particular stream does not have the first byte. In other words, a stream is instantiated, but has to be finalized later. This is certainly not simpler.

    Martin Thomson replied that it made his code simpler. I don’t know about Martin’s code, but it would not make our code simpler. A bit later in the discussion, I offered the following analogy:

    Think of a networked computer. Port 80 is HTTP; port 22 is SSH; and so on. The kernel does not examine the first byte of an incoming TCP connection to see what service it wants. Instead, the application is allowed to use port numbers. TCP connections have ports; QUIC streams have IDs. We can use them.

    This reminded me of a related discussion we had on the mailing list. There, I also argued against hiding stream IDs. I will quote the last communication I authored on that thread at some length:

    The API problem Marten describes seems like a non-issue: There are
    two sets of APIs:
    - User code / HQ; and
    - HQ / QUIC.
    
    Stream IDs do not have to be exposed in the former: The user does not
    care about them, as these things are just your regular HTTP messages.
    In the HQ / QUIC API, on the other hand, expose away! Streams have
    IDs, the QUIC transport draft talks about them, the IDs have meanings --
    it only makes sense that the layer that uses the QUIC objects directly
    can use Stream IDs. Why not?!
    

    Why do some people high on the IETF QUIC totem pole advocate for not using stream IDs in HQ without giving a good technical reason? I can only guess. (A related issue — in the sense that it is similarly groundless — is the desire to mandate that streams be opened in sequence. This discussion is being kept alive and tomorrow will be revisited again. Watch this space.)

    The “no-grab” principle above is fine and dandy, but QUIC streams do not have intrinsic types. Using the first payload byte as a substitute is a kludge.

    Roberto Peon pointed out that control streams without fixed IDs would make debugging difficult. Certainly, the proposal’s momentum was slowed, but the consensus was to “continue discussing” it. I suppose it means I have to remain vigilant!

    Stream Priorities

    HQ has inherited the HTTP/2 stream priority model… and associated problems. The short version is that streams can depend on one another and just because a stream is finished does not mean that it can be removed from the dependency tree. In some scenarios, this could mean that the dependency tree keeps on growing. (Few HTTP/2 implementations do stream dependencies properly. See Are HTTP/2 Servers Ready Yet? Salient quote: “the priority mechanism has not been well designed and deployed.”)

    A few ideas were tossed around:

    • Is this really our problem? Since HQ uses HTTP/2 semantics, shouldn’t we keep priorities as-is?
    • Should we drop support for priorities altogether?
    • Should priorities only affect idle streams?

    Ultimately, the Working Group resolved to come back to this issue later.

    QPACK

    Alan Frindell, the editor of the QPACK draft, described what the current version of QPACK looks like. Other than being broken by design, QPACK is almost complete. So much so that the last slide of Alan’s presentation talked about an interop.

    I objected to using framing on the QPACK encoder stream, as it renders some optimizations impossible. For example, why can the encoder not write to the packet directly? If one has to know the length, it means that the encoder instructions must first be buffered. The reason for framing, Alan explained, is to aid the decoder. Some HPACK decoders do not deal well with incomplete instructions; they do not know how to resume.

    I think that this is not applicable in this case. The table update stream processed by a QPACK library only affects a single entity, the dynamic table, and can save the state easily. It is the processing of partial HEADERS block that HPACK decoders have historically had trouble with. These blocks are framed and carried on other, regular, streams. (I will likely present this argument to the QPACK editor and authors.)

    While Alan proceeded to demonstrate a simple QPACK session walk-through, Martin Thomson filed a bug titled “Encoder stream can deadlock.” And so it can! Due to framing. Thanks, Martin.

    ***

    The second part of the Interim debates is tomorrow. Stay tuned!

  • Developer’s Corner: OpenLiteSpeed’s PHP Module

    Developer’s Corner: OpenLiteSpeed’s PHP Module

    Developer's Corner: OpenLiteSpeed's PHP Module

    We’ve been working on a PHP module.

    PHP, a server-side scripting language, was designed for web development but it does double duty as a general-purpose programming language. This makes it popular among internet developers who embed it into HTML code, or use it in web publishing frameworks and content management systems. PHP code is interpreted by either a web server module or a Common Gateway Interface (CGI) executable.

    As a CGI executable, it’s familiar to most LiteSpeed users as lsphp, lsphp5 or lsphp7 (if there’s a specific version dependency). For Apache there are corresponding programs: mod_php and php-cgi.

    Drawback of CGI Executables

    LiteSpeed does a lot of work to make lsphp fast. Once loaded, it will be reused for the same connection for as many applications as want to use it. This allows the PHP opcache to be reused and the speed to thus increase.

    Sounds good, but not ideal. Whenever an older connection times out and the process terminates or a new connection is started, a separate instance of lsphp is used and the opcache starts from scratch. In the future, there will be a CRIU (CheckPoint restore In Userspace) version of lsphp which will reload the opcache enabled code (and is CloudLinux aware), but that’s for a future article.

    The Module Approach

    Each time a separate instance is started, it causes performance lag. The module approach addresses this issue by loading in one instance for the entire server at server startup time. There are lots of reasons this is great:

    • The amount of memory and CPU necessary to run each PHP process is reduced.
    • The startup time to run a PHP process is reduced.
    • The opcache is available to all, which should make the applications progressively faster as they get compiled into opcache.

    But module usage also comes with some issues that are not necessarily easy to address:

    • Because both Apache and LiteSpeed* are multi-threaded applications, PHP has to be able to operate multi-threaded. This has only been true since PHP 7.2 has been released (November 2017). There were PHP implementations that claimed to be thread-safe, but they were not and have been deprecated for thread use by the PHP team.
    • Building mod_lsphp is quite a chore. It requires access to both OpenLiteSpeed module headers and a full PHP installation. The result of the compile is the entire PHP stack as a shared library. There are wiki instructions on how to do the build, but it can be tricky.
    • The existing SAPI module is highly optimized, both by the LiteSpeed developers and the PHP developers. Mod_lsphp may actually not be faster than lsphp so you may want to benchmark it. If lsphp is faster, obviously you should stay with it.
    • An errant module will crash the server. A PHP application can not. Mod_lsphp has been extensively tested, but it is possible. We recommend running mod_lsphp in isolation before putting it into full production.

    The Solution for You

    So why go through all of this trouble? The module may perform better on your server. And when the server is under a heavy load, it may perform even better. If your web site is PHP heavy and perhaps PHP bound, it’s really worth your while to go through whatever you can think of to improve its performance.

    Perhaps do a bit of homework before starting the process:

    • Make sure that your applications are compatible with PHP 7.2 or higher. This is a requirement.
    • You can use system tools (like the top program, for example) to see which processes are the most expensive in CPU utilization during your load peaks. In particular look for lsphp high on the list.
    • Verify that you’re not just simply low on memory. The free program can be helpful here. If a memory upgrade or an increase in swap space will address your problems you should start there.

    If you find that PHP is your problem, then mod_lsphp may be the solution for you.

    We should mention, this is a pioneering effort. We are still building this multi-threaded framework (more on that in a future article), and mod_lsphp is currently in beta. We encourage you to try it and share your feedback and any contributions you may have. Help us to make this module the best it can be!

    * The latest version (1.5) of OpenLiteSpeed is now multi-threaded and exports a detailed and robust set of APIs, all of which are thread-aware. Note that this is threading internal to the web server – it’s not PHP threading. That is still unavailable for web applications and is likely to remain that way.

  • Developer’s Corner: January Interim Highlights

    Developer’s Corner: January Interim Highlights

    Developer's Corner: IETF QUIC Working Group January Interim Meeting

    Nothing is more permanent than a temporary solution.

    – Folk wisdom

    The Melbourne Interim Meeting has come and gone. I participated remotely, which means that my workday started at 5 pm and wrapped up at 1 am. The whopping 16-hour difference turned out not to be so inconvenient after all.

    The three-day schedule was packed tight with topics (see Agenda; Minutes) as the Working Group strives to converge quickly in order to hit the November 2018 deadline for QUIC v1. In this article I will touch on some of these topics.

    Header Compression

    The Working Group rejected the conservative approach to blocking outright and selected QCRAM (with a “tweak”) as the native QUIC header compression protocol.

    Out of the three working proposals put forth by the Header Compression Design Team, QMIN is the most conservative: the protocol solves the head-of-line blocking (HoLB) problem at the cost of compression efficiency. The Working Group looked at the compression performance comparison graph (ideal network conditions: zero packet loss) and decided that compression performance trumps all other considerations:

    HPACK/GQUIC, QMIN, QPACK, and QCRAM-03 Compared
    The flat purple line is hpack0: that is HPACK with the dynamic table size of zero. This is the baseline. The other four lines are HPACK (as it would perform in GQUIC), QMIN, QPACK, and QCRAM-03. One can observe the initial penalty incurred by QMIN.

    Of the two proposals that seek to minimize HoLB rather than avoid it, QCRAM is simpler than QPACK, as the latter uses multiple transient control streams to transmit updates. The vanilla QCRAM, however, risks a deadlock.

    On the spot, the Working Group came up with the fix: “just use a single control stream,” they said. With that, Buck Krasic and Mike Bishop went off to write up the QCRAM-04 specification. The use of a single control stream makes QCRAM unable to address a whole class of HoLB scenarios. In other words, the deadlock fix makes QCRAM ineffective.

    Before the adoption vote was taken on the last day of the meeting, I pointed this out to the Working Group and advocated for selecting a more robust HoLB avoidance mechanism. Despite my objections, the motion to adopt QCRAM passed with overwhelming support. The main line of reasoning behind picking a worse solution was that we can always go back and make improvements or replace it with something better. I am skeptical.

    On the bright side, having settled on the header compression protocol, the Working Group has eliminated a large roadblock on the way to hit its November 2018 deadline for QUIC v1. Furthermore, QCRAM has a knob to use blocking avoidance instead of minimization.

    In the avoidance mode, QCRAM is structurally similar to QMIN — non-modifying header blocks in individual HTTP message streams and a single control stream to carry updates with ACKs — and thus should have similar performance characteristics.

    Encrypted Packet Numbers

    Martin Thomson presented his idea of greasing the protocol. Greasing means designing the QUIC packet format (the stuff that is actually transmitted over the network) in such fashion so as to allow future changes to the protocol. What greasing tries to prevent is ossification: inability to deploy new protocol or protocol extensions due to the unchangeable nature of infrastructure components that have come to rely on a particular feature of the current protocols. (As a matter of fact, deployability was the number one original design goal of QUIC.)

    Among other things, Martin’s original proposal obfuscates almost everything in the QUIC packet header, including packet numbers. This is quite radical. The proposal engendered lively debate at the Interim Meeting. The subsequent, more modest proposal only aims to encrypt packet numbers. There is an interesting (and long!) email thread where several aspects of this proposal are discussed.

    Variable-Length Connection IDs

    In our code, the QUIC connection ID is represented as uint64_t. Easy-peasy! You’d think that eighteen quintillion is enough for everybody, right? It turns out, however, that there are uses for both longer and shorter connection identifiers. The longer IDs can be useful to load balancers: the load balancing information (e.g., which backend server to forward the connection to) can be embedded into the connection ID itself. Eight bytes is simply not enough for this. On the other hand eight-byte IDs may be an overkill when used, for example, in a P2P setup.

    A new Design Team to hash this out has been formed. The team is to present its findings, potentially with a proposal, at the next meeting.

    Explicit Congestion Notification (ECN)

    Explicit Congestion Notification (ECN) is a way for the network (read: routers) to notify the endpoints about incipient congestion. (See RFC 3168 for details. It is impressive how much complexity can fit into just four bits.) ECN is meant to improve network performance of any transport protocol on top of IP, not just TCP, and thus is of interest to QUIC. The benefits of ECN were deemed significant enough that a dedicated Design Team has been empaneled. Headed by Ingemar Johansson, it is expected to produce a proposal by IETF 101 in London. One can track their progress by looking at the working document.

    Summary

    The deadline for QUIC v1, which has already been moved once, is only nine months away. For the Working Group not to miss it, many large pieces have to fall into place, and quickly. Each new conference of the Working Group will be more and more consequential. Here is to our success!

  • Developer’s Corner: QCRAM-04 Does Not Solve HoLB

    Developer’s Corner: QCRAM-04 Does Not Solve HoLB

    Developer's Corner: QCRAM-04 Does Not Solve HoLB

    At the Melbourne Interim meeting, the IETF QUIC Working Group decided to use a version of the QCRAM header compression protocol with the following modification: use a dedicated control stream to carry all dynamic table updates. The result is QCRAM-04. This modification is one way to prevent deadlock hazard (the subject of last week’s blog post) present in the previous QCRAM proposals. While it certainly addresses the deadlock, the single control stream introduces ordering to otherwise independent operations, making it unable to minimize HoLB for a class of applications.

    The Original HoLB Problem

    The original HoLB problem that we are trying to solve is as follows. HPACK/GQUIC delivers all HTTP message headers as header blocks on a separate control stream. HTTP message payload (bodies) are sent on different streams, one message per stream. The application (HTTP) cannot process message bodies without the headers; it must have the headers before it starts reading from the message stream.

    The Original Head-of-Line Blocking Problem
    Figure 1: HPACK/GQUIC: Control stream carrying two header blocks and two streams carrying message bodies.

    If a packet carrying part of the Header Block A is lost, Header Block B and, therefore, the whole of Message B cannot be processed, even if all of the packets carrying Header Block B and Message B Body have arrived. Message B is stalled until the lost packet is retransmitted successfully and the control stream is unblocked. This is the HoLB problem.

    Proposed Solutions

    The solutions to the HoLB problem fall into two categories:

    1. Avoidance: Disallow vulnerable references. This approach is robust and simple to implement, but carries a compression performance penalty.
    2. Minimization: Allow vulnerable references, but minimize their impact. This approach looks to achieve maximum compression at the cost of protocol and implementation complexity.

    QCRAM and QPACK belong to the second category. Until QCRAM-04, both carried table updates on multiple streams, for the following reason:

    Coping with False Dependencies

    (Refer to Figure 1 again). If Header Block B uses a dynamic entry inserted by Header Block A, then B cannot be processed until A has been processed; this is a true dependency. If, however, Header Block B does not depend on any insertions performed by Header Block A, placing them in order on a single control stream introduces a false dependency. The design choice of placing dynamic table updates on separate streams is driven by desire to eliminate false dependencies and, thereby, to minimize HoLB.

    How QCRAM-04 is Different From HPACK/GQUIC

    QCRAM-04 moves parts of header blocks from the control stream to the individual message streams. This means that if a particular message’s headers do not cause an insertion into the dynamic table, this message does not have to write to the control stream. This eliminates some false dependencies and is an improvement over HPACK/GQUIC.

    How QCRAM-04 is Different From HPACK/GQUIC
    Figure 2: QCRAM-04: control stream carrying table modifications upon which Header Block A depends and message streams for messages A and B. Message B can be processed if parts of message A are lost.

    False Dependencies Remain

    If messages A and B cause independent updates to the dynamic table, they are written to the control stream. This fact makes message B depend on message A: a false dependency.

    False Dependencies Remain
    Figure 3: QCRAM-04: control stream carrying independent table modifications which, nevertheless, make Message B depend on Message A: a false dependency.

    Note that Figure 3 and Figure 1 are quite similar.

    Three Application Classes

    To analyze how QCRAM-04 improves over HPACK/GQUIC, we should consider three application classes (basically, usage patterns):

    1. Dependent header blocks;
    2. Independent header blocks without table modification; and
    3. Independent header blocks with table modification.

    I. Dependent Header Blocks

    If Header Block B uses table entries added by Header Block A, there is not much one can do about that: A must be processed before B, no matter how the header blocks are transmitted. QCRAM-04 and HPACK/GQUIC do not introduce HoLB: this is the property of the application itself.

    II. Independent header blocks without table modification

    Header blocks that do not modify the dynamic table do not require the use of the control stream in QCRAM-04 (see Figure 2). This is a clear improvement over HPACK/GQUIC, which introduces a false dependency and risks HoLB by delivering all header blocks over the same stream.

    III. Independent header blocks with table modification

    If Header Blocks A and B insert different entries into the dynamic table and Header Block B does not use any entries inserted by A, both QCRAM-04 and HPACK/GQUIC introduce a false dependency by delivering unrelated table updates associated with A and B on the same stream (see Figure 3).

    Quantitatively, QCRAM-04 is a little better than HPACK/GQUIC here. This is because it sends less data on the control stream, which means that the chances of packet loss are smaller. However, the improvement is small: the control stream carries table updates, which is most of the header block data.

    Summary

    QCRAM-04 achieves HoLB minimization in one class of applications, but not in the other, where it introduces a false dependency. This means that the answer to the question “Will my application suffer from protocol-induced HoLB were I to switch to HTTP/QUIC?” is “It depends.” That is not good enough. In the HoLB minimization approach, independent dynamic table updates must be sent on different streams.

    —
    Text by: Dmitri Tikhonov
    Graphics by: Mark Zou

  • Developer’s Corner: HoL Blocking and the Deadlock Hazard

    Developer’s Corner: HoL Blocking and the Deadlock Hazard

    Developer's Corner: Head of Line Blocking and the Deadlock Hazard

    SMITH: Doctor, it hurts when I do this.
    DALE: Don’t do that.

    – Smith and Dale

    This is the third article in the QUIC Header Compression Design Team series.

    Introduction

    To achieve the best header compression performance, it is tempting to have the compression protocol allow potential head-of-line blocking (HoLB). The idea is that HoLB will not occur most of the time, while the compression gain is significant. The first article of this series exposes priority inversion caused by HoLB and challenges the assertion that HoLB is rare. The second problem is that HoLB is a potential attack vector, wherein the decoder is tricked into using a lot of memory. One way to prevent such attack is to not read from the stream until the decoder can process the stream immediately. This approach creates a deadlock hazard. This problem is examined in this article.

    Note on Terminology

    If a header block B depends on an entry inserted by header block A, we say that B depends on A. Together, they are dependent header blocks.

    Problem Statement

    Writing dependent header blocks to stream out of order introduces the risk of a deadlock. There are two types of a deadlock.

    Single-stream Deadlock

    When dependent header blocks are written out of order to the same stream, the first header block cannot be parsed because it depends on the block that follows it.

    Multiple-stream Deadlock

    This type of deadlock happens when the out-of-order header block prevents the receiver from reading stream data that follows the header block. What makes it different from the regular HoLB case is that the sender cannot put the resolving header block onto the stream due to the limits imposed by the QUIC connection flow control.

    Scenario 1: Resend After a Reset

    One way a multi-stream deadlock can occur is when a header block must be resent after a stream reset. By the time the sender figures out that it needs to resend a header block, it may have filled all existing streams to the brim — which the receiver cannot drain — and is not allowed to stream more data.

    Possible Mitigation: Don’t Fill Flow Control Window

    One potential solution is always to leave some room in the connection flow control window. That is, leave at least the number of bytes required to resend all unacknowledged header blocks.

    There are downsides to this approach:

    1. It is not clear that the connection flow control window information is available at HTTP/QUIC level.
    2. The code complexity of the HTTP/QUIC layer increases — the user code can no longer write directly to the stream. These calls must be intercepted at HTTP/QUIC layer to ensure that the threshold is not exceeded.
    3. Most of the time, these are just wasted bytes that could otherwise be used. In other words, throughput suffers.
    4. Not filling the connection window may prevent the peer from sending WINDOW_UPDATE, resulting in a stall.

    Scenario 2: Out-of-order Write

    In general, a multi-stream deadlock can occur any time dependent header blocks are written to different streams — consuming flow control credits — out of order. If header block B depends on header block A, but is written to stream first, it may happen that header block A can never be written: If the receiver does not process stream B because of its application protocol rules (in our case, the receiver is avoiding memory exhaustion attacks), it is possible that the connection flow control prevents the sender from sending more data.

    Possible Mitigation: Don’t Do That

    “Well, don’t do that, then!” – might one say. This is a sound point of view: since the problem is of the application’s own making, it is up to the application to solve it. Nevertheless, the deadlock risk bears being pointed out, as it affects how a QUIC/HTTP implementation may be designed. For example, if the stream interface offered by a QUIC implementation performs internal buffering, can the application make sure that data on one stream gets sent before dependent data on another stream?

    Possible Mitigation: Make the Transport Do It

    My initial report to the Design Team prompted Martin Thomson to share the finding on the common IETF QUIC WG Mailing list. The resulting thread is large and makes for an interesting read. It contains several ideas on how such deadlock could be prevented by the transport, either by making the current priority implications stricter; or by introducing extra transport rules, exceptions, or overrides of various sorts.

    Summary

    It is natural to write dependent header blocks to streams in order. Not doing so may result in a deadlock. In particular, resetting streams forces the encoder to write header blocks out of order. Hence, the encoder must address the possibility of a deadlock in some fashion.

    P. S.

    The “don’t do that” versus “let the transport solve it” discussion is likely to resume at the upcoming Interim Meeting (Jan 23 – 25) in Melbourne, Australia. Watch this space for updates.

  • Developer’s Corner: Head-of-Line Blocking Allows Attacks

    Developer’s Corner: Head-of-Line Blocking Allows Attacks

    Developer's Corner: Head-of-Line Blocking Allows Attacks

    Protocol designers underestimate
    how badly people will implement
    their designs.

    – Adam Langley, Cryptographic Agility.

    Abstract

    Compression schemes that allow head-of-line blocking are targets for memory exhaustion attacks against the decoder. Detecting such an attack is not trivial. Without guidance, programmers are likely to produce vulnerable implementations. HoL blocking must either be prohibited at the protocol level or the attack mitigation mechanism must be specified explicitly in the compression specification. One way to avoid an attack is presented.

    Introduction

    In the previous article, we discussed how head-of-line blocking can cause priority inversion and vice versa. In the second part of the series, we will examine potential attacks made possible by head-of-line blocking.

    Problem Statement

    Head-of-line blocking causes the decoder to queue header blocks that cannot be decoded due to a missing dynamic entry. Under normal circumstances, this occurs because of packet loss or reordering. An adversary, however, may create this scenario artificially in order to get the decoder to queue data, thereby creating a denial-of-service attack.

    Here, a simple recommendation that the decoder use a timer is insufficient. The decoder must decide how much data it is willing to buffer and for how long. In other words, it must be able to differentiate between an attack and a regular packet loss/reordering situation. An incorrect determination may mean allowing a server to be DoSed; err the other way, and a valid connection is aborted.

    Sample Attack

    The attacker’s strategy is simple: send header blocks that contain a reference to a non-existent dynamic entry:

    Head-of-Line Blocking Allows Attacks: Sample Attack

    The decoder cannot resolve a reference and has to wait — buffering data. More and more header block data is sent. To maximize the effect of the attack, the adversary may use more than one — potentially all — available streams.

    Mitigation Strategies

    Strategy 1: Simple Timer

    The decoder may associate a simple timer with each stalled header block. A simple timer has to be quite conservative in order to eliminate false positives. During that time, the attacker writes more data to each of the header blocks in a round-robin fashion, perhaps even resolving the blocking entry to reset the decoder timer while introducing a new unresolved entry at the same time. The amount of data queued at the decoder grows and grows and the server is eventually hosed.

    Strategy 2: Adaptive Timer

    A conservative timer means that the attacker has plenty of time to write more blocking data. An adaptive timer aims to reduce that window by using recent network conditions for determining a smaller timeout value. Besides RTT, it may need information such as the current CWND as well as callbacks fired when packet loss or timeout is detected. (This is digging pretty deep into the transport.) Even then, the adaptive timer must take into account that data could be lost multiple times as well as the possibility that the peer retransmits lost packets after it sends new data.

    This approach lessens the impact (although without further analysis I cannot state to what degree), but does not solve the problem. In addition, the possibility of a false positive grows.

    Strategy 3: Application-Aided Attack Detection

    The decoder does not know whether the headers it receives make sense to the application or whether they are just fluff the attacker uses to blow up its blocked queue. The application knows about the headers it uses and thus could be called upon to evaluate the headers’ fluff level, sort of like an email SPAM filter.

    This idea is not workable:

    1. While we (the IETF QUIC Working Group) do control the direction the transport layer takes, requiring all applications to provide extra logic to fight attacks is a non-starter.
    2. The attacker may simply target an application with special header blocks that would pass the checks.

    Strategy 4: Change Application Interface

    It is possible to change the decoder API to require the application to receive out-of-order headers, allowing the decoder to queue just the unresolved references. This is not good enough, either:

    1. The problem is just kicked upstairs — now the application must queue the data and decide when to abort the connection.
    2. If an indexed-name/literal header representation is used, with the index being unresolved, the decoder still has to queue the whole header field.

    Strategy 5: Unread Data From Stream

    One can imagine requiring the stream interface to be able to revert a read; alternatively, a peeking functionality may be provided. Either will allow the decoder to leverage the underlying stream’s queuing functionality instead of doing its own: when an unresolved entry is encountered, push back some data (or the whole header block). The advantage of using the stream for this is that the amount of data in the stream is limited by that stream’s maximum as well as the connection’s overall maximum.

    The first downside of this approach is that unreading may not always be possible, as the application must take off data from streams so that the connection can proceed.

    When it is possible, this strategy effectively exchanges a memory exhaustion attack for processor exhaustion attack. One can imagine an attacker now switching to feeding the decoder resolved entries and new data with unresolved entries, forcing it to read, decode, and push back the data.

    In addition, it is unclear whether the QUIC standard will require the transport layer to provide such functionality. (Consider, for instance, the implications of deferring sending a WINDOW_UPDATE frame while waiting for the application to “commit” to a read.)

    No Way to Detect An Attack

    None of the mitigation strategies above help the decoder to detect an attack in time. An odd (for some value of “odd”) sequence of incoming streams, header blocks, and dynamic table updates may be a perfectly valid use case. An attack detection mechanism built into the decoder must be sure that it is not going to break applications. Thus, one implementation may do it well and another may do it poorly. Chances are good that it is the protocol that will take the blame. One could add: for a good reason.

    What This Means

    Section 7: Security Considerations of RFC 7541 (HPACK) is five pages long — a quarter of the whole text! (Sans preamble, table of contents, and appendices). Obviously, the security of the header compression mechanism has been addressed thoroughly in the past.

    It is apparent that the compression mechanism that we (the Design Team) eventually present to the Working Group must either preclude this type of attack at the protocol level or offer a robust attack detection and mitigation that can pass a security review.

    There are two ways to address this:

      1. Not allow head-of-line blocking in the final proposal.
      2. Agree that the final compression proposal
        • limit HoL blocking in a quantifiable fashion and/or
        • include a robust attack detection mechanism.

    Limiting HoL blocking in a quantifiable fashion means that the compression mechanism guarantees that the amount of data that has to be queued at the decoder does not exceed some threshold.

    A robust attack-detection mechanism implies that it is possible to implement using standard programming techniques. It would be unreasonable to require that every decoder implement a sophisticated attack detection logic. (This would also be against the QUIC WG mandate that the decoder be simple.) In addition, the detection code must be relatively inexpensive (memory, CPU) to operate as part of the decoder.

    Potential Solution: Defer Reading

    One potential solution to this is to prefix header block data with information that would tell the decoder whether the header block could be processed in one shot given current decoder state. For example:

    Head-of-Line Blocking Allows Attacks: Potential Solution

    If the Update #X has not yet arrived, the decoder does not read any more from this stream. This leaves the headers in the transport. (This takes Strategy 5 one step further.) Such approach limits the amount of memory required by the decoder.

    The downside to this approach is that not draining the streams promptly may prevent the transport from operating efficiently.

    Summary

    Allowing HoL blocking and requiring the decoder to queue data and wait until it can be decoded is a denial-of-service attack vector. The simplest way to prevent such attack is to not allow HoL blocking at all. Deferring reading from affected streams is likely feasible but requires further analysis. GitHub issue #1011 was opened to track this.

  • Developer’s Corner: HoL Blocking and Priority Inversion

    Developer’s Corner: HoL Blocking and Priority Inversion

    Developer's Corner: HoL Blocking and Priority Inversion

    Background

    Producing a native header compression mechanism for QUIC is one of the goals of the IETF QUIC Working Group. The approach used in the Google QUIC (gQUIC) suffers from head-of-line blocking (HoLB) and is thus suboptimal. I had been interested in header compression for some time and, in November of 2017, I added my own proposal (QMIN) to the other two proposals already under consideration. The Working Group had empaneled the Compression Design Team and tasked it with coming up with a single proposal. Our team has six members:

    • Alan Frindell, the author of proxygen;
    • Charles “Buck” Krasic, the author of the QCRAM compression proposal;
    • Dmitri Tikhonov, the author of the QMIN compression proposal.
    • Martin Thomson, the co-editor of the QUIC Internet Draft;
    • Mike Bishop, the author of the QPACK compression proposal; and
    • Roberto Peon, the co-author of HPACK (RFC 7541).

    There are many design decisions that go into producing a compression mechanism. The resulting proposal (Internet Draft) will hopefully become a standard (RFC) one day. The rationale behind many aspects of a standard usually does not get included into RFCs: this is not what the RFCs are for. In this article — which may become first in a series of several — I will describe an interesting problem that could impact the choices the Compression Design Team makes.

    Introduction

    QUIC streams have priorities which the application layer can set. HTTP/2 and, consequently, HTTP/QUIC use priorities to improve UX. Usually, priorities are assigned by content type: HTML has higher priority than CSS, which in turn has higher priority than images. In general, an application using the protocol may assign any priorities to suit its needs.

    Problem Statement

    In gQUIC, the header blocks are delivered on a separate, high-priority (higher than all other priorities on the connection save the HANDSHAKE stream), unbounded HEADERS stream. The HTTP payload-bearing streams’ priorities are used to select which HTTP message to process. Lost or late packets on the HEADERS stream may cause other streams to wait. (This is the head-of-line blocking problem that the Design Team is trying to solve.) The HEADERS stream itself does not depend on other streams and it is always processed first.

    In the current IETF HTTP/QUIC draft, on the other hand, the header blocks are part of the request/response stream itself. In the case where the header compression protocol mandates that the decoder be able to wait on unresolved entries (this is the decoder HoL blocking), the compression protocol and the QUIC protocol can actively hinder each other in the following manner:

    1. HoL blocking can cause priority inversion; and
    2. Priority inversion can cause HoL blocking.

    HoL Blocking Causes Priority Inversion

    This situation can occur when a server sends two responses (let’s call them A and B) where the header block of a higher-priority response (for instance, A) references entries first inserted in the header block of the lower-priority response.

    Example:

    HoL Blocking Causes Priority Inversion

    Here, the server does the correct thing — since it got the B request first and the A request has not yet arrived, it encodes and sends the response, even though a higher-priority request is coming.

    The client, too, does everything correctly: after receiving the responses, it processes the higher-priority A response first.

    Stream Priorities Cause HoL Blocking

    Priority-induced HoL blocking can happen whenever there is packet loss. If a high-priority response A depends — via its header block compression — on a low-priority response B which gets lost, the server will choose to send the higher-priority data on stream A before retransmitting the lost B response. The client will have to buffer the whole A response or fill the A stream buffer before it receives the required B response and can proceed.

    The example is a variation of the above when the B response gets lost:

    Stream Priority causes HoL Blocking

    The server’s choice to send higher-priority stream data is in accordance with the QUIC recommendation. It does everything correctly.

    This scenario is worse than the first, because the client cannot use the high-priority response (say, render an HTML page or compile JavaScript) for as long as it takes to fill the stream buffer.

    Attention: This is worse than the original gQUIC HoL blocking problem, for the gQUIC server would choose to send the missing header block first.

    More Potential Troubles

    Above, only static priorities are considered. QUIC allows stream priorities to be adjusted dynamically. An implementation that changes stream priorities based on some performance metrics — for example, as an indirect result of priority inversion — may only make the problem worse.

    Summary

    Adding stream dependencies in the form of header block dependencies in the decoder introduces a conflict with the QUIC stream priorities whenever there is packet loss or reordering. This conflict can cause HoL blocking, resulting in a markedly degraded user experience. Since both packet loss and packet reordering happen constantly (the former is how congestion control works), the problem will be ubiquitous.

    These issues can be solved by designing the protocol so that either

    • Response or request headers can be processed immediately; or
    • Operations that modify the table state are sent on a different, special-purpose stream (or streams).

    Text by: Dmitri Tikhonov
    Graphics by: Mark Zou