Skip to content

VIP6: What does pipe mean in Varnish5?

Dridi Boukelmoune edited this page Apr 13, 2016 · 3 revisions

Synopsis

The fact that HTTP/2 is multistreamed means we have to decide what "pipe" means going forward

Why?

In HTTP/1 all transactions are serialized and we can therefore decide, on any transaction, to start piping to a backend.

In HTTP/2 transactions happen in parallel and share not only the TCP connection, but also the HPACK compression state.

Piping was originally intended to cater for traffic Varnish did not understand, such as TN3270 with a HTTP lead-in and simular hacks. With HTTP/2 being used as transport protocol for things like websockets, similar considerations apply.

In HTTP/2 we can imagine two levels of piping, either we pipe the full connection, or we pipe stream by stream.

Piping stream by stream is hard, the streams would have to be decompressed and recompressed. We can do that for HTTP, because we know the semantics, but if another carried protocol, such as websockets, decides to implement their own shared-state compression, we'll need to know about that before we can pipe those streams.

At present we have no documented need to pipe stream-by-stream, so Gettys rule " The only thing worse than generalizing from one example is generalizing from no examples at all." controls.

Connection based piping is much simpler, but the decision criteria needs to be thought about.

In HTTP/1 we can stare at a request in vcl_recv{} and make up our mind, we can even modify it, before we send it to the backend.

In HTTP/2 we could stare at the very first request on the connection before deciding, but to maintain HPACK compression-state we cannot modify it.

If the HTTP/2 is being used for another protocol, there will be no request to stare at, and we will only have the IP#'s and possibly the identity of the protocol opening the first stream. (The same could in principle be said for HTTP/1, but we never supported a H/1 connection that didn't at the very least start out as a HTTP request.)

The need for piping in HTTP/1 is a lot smaller than it used to be, and hopefully in V5 it will never be a good idea to pipe a HTTP transaction, unless there is something truly special (read: Non-compliant) about it.

If we pipe per connection only, we can make it an option to send a PROXY header to the backend.

We have still to decide what "piping" means if the backend is a VMOD.

Error handling if piping is attempted illegal times/places needs to be thought about.

How?

1: Drop pipe entirely

After 10 years, it is not unreasonable to take the attitude that Varnish should be able to cope with any HTTP traffic without resorting to piping, and thus we could simply drop it from the feature-set.

As tempting as this might be, pipe has traditionally good escape-mechanism when things got ugly, and loosing that would be step backwards in terms of "tools not policies".

2: Pipe as in "bent pipe"

We introduce a vcl_sess{} which has access only to the connection parameters, the four IPs, any SSL/ALPN or other hints from the transport, and we can decide to pipe that to some ip+port via a backend. Varnish never inspects any of the payload bytes.

The architectural purity of this is appealing, and it does preserve the escape-mechanism as far as it can be determined from the IPs that it is necessary.

3: As much pipe as possible

We introduce req.pipe which can take the values '0' - cannot be piped, '1' can be piped and req can be modified or '2' can be piped but modifications to req will be lost.

For HTTP/1 it will always be set to "1", indicating that piping is possible.

For HTTP/2 it will be set to '2' only for the very first request, and no other requests will be allowed into vcl_recv{} from that connection, until the first request has determined if piping will happen.

If piping is attempted at an illegal time, we fail the session with appropriate noise in VSL.

We add a backend property ".proxy_header" which can be set to '1' or '2' and all connections to that backend gets a PROXY protocol header, and cannot be reused.

Discussion

dridi: After pondering this for quite a while and after a discussion on HTTP upgrades related to VIP8: No pipe in builtin.vcl in V5 I have come to the conclusion that pipe is taken from the wrong end. I also see two pipe concerns that should be treated separately:

  • once piped, a session never runs through the VCL state engine
  • piped sessions bypass stevedores

For the stevedore issue, it could be solved by having a custom storage similar to the predefined Transient, with a name that implies the lack of actual storage like Direct, None or Bypass. One of the reasons to use pipe is sometimes the delivery of really large otherwise passed objects. It is now possible to free storage as soon as it has been consumed but that doesn't solve the problem if the client is orders of magnitude slower than the backend. It would be nice to allow passed transactions to bypass stevedores.

There could even be a parameter to control the maximum size before automatically bypassing the storage.

Another reason to bypass stevedores could be Server-Sent Events, where a backend responds with an infinite stream.

The reason why piped content no longer goes through VCL states is because we use it when we switch protocols. So if we want to let websocket traffic pass through Varnish, that's the only option. But ultimately it's the backend that announces the protocol switch, so getting a transition from vcl_recv to vcl_pipe makes little sense. And since VMODs can implement their own transport, that would make them responsible for dealing with protocol upgrades.

I would consider a filesystem backend bogus if it returned 101 responses. And I would consider a VCL forging such a response stupid. In both cases that would not be Varnish's problem. If you client is using HTTP/2 and your backend advertises a protocol upgrade, a 502 response should be enough. But nevertheless the pipe transition should belong to the vcl_deliver, possibly with a built-in rule that does it for 101 responses.

Clone this wiki locally