As mentioned in the first section of this chapter: HTTP is the go-to protocol these days. It’s true for all kinds of implementations, which are surely beyond the scope of the initial HTTP/1.1 use cases.
The speed of internet connections has gone up, and greater bandwidth allows for larger data transfers. At this point in time, about 80% of the internet’s bandwidth is consumed by online video.
In chapter 10 we’ll be talking about OTT video streaming, and how Varnish can be used to accelerate these streams.
It’s realistic that you’ll see output like this, where the size of the
Content-Length response header is massive:
HTTP/1.1 200 OK
Content-Length: 354648464
This is a 338 MB response. For OTT video streaming, you’ll want to chop this up into several smaller files to improve the user experience, to improve your bandwidth consumption, and to reduce the strain on your backend systems.
HTTP already has a built-in mechanism to serve partial content. This mechanism is called byte serving and allows clients to perform range requests on resources that support serving partial content.
Another use case for range request is a download manager that can pause and resume downloads. Maybe you want to download the first 300 MB right now, and continue downloading the remaining 38 MB tomorrow. That’s perfectly realistic using range requests.
A web server can advertise whether or not it supports range requests
by returning the Accept-Ranges response header.
When range request support is active on the web server, the following header can be returned:
Accept-Ranges: bytes
This means the range unit is expressed in bytes.
The value could also be none in case the server actively advertises it doesn’t support range requests:
Accept-Ranges: none
Based on either of these values, a client can decide whether or not to send a range request.
A client can send a Range request header and let the server know which
range of bytes it wishes to receive.
Here’s an example:
Range: bytes=0-701615
This example fetches a byte range starting at the beginning up to and including the 701615-th byte.
Here’s another example:
Range: bytes=701616-7141367
Whereas the previous range ended at the 701615-th byte, this example picks up from the 701616-th byte until the 7141367-th byte.
When you perform a range request, you’re not requesting the full
response body. The server acknowledges the fact that you’re receiving
partial content through the HTTP 206 Partial Content status code.
Whereas an Accept-Ranges response header is returned regardless of the
type of request, a Content-Range response header is only sent when an
actual range request occurs.
Here’s an example of a Content-Range header that is based on the
Range: bytes=701616-7141367 range request:
Content-Range: bytes 701616-7141367/354648464
The header matches the range that was requested, but it also contains the total byte size of the resource.
If we do the math, we can come to two conclusions:
7141367 - 701616 = 6439752, which corresponds to the value of the
Content-Length header we also receive from the server354648464 that is part of the Content-Range value matches the
value of the Content-Length header if you performed a regular
request on this resourceBasically, the Content-Range header confirms the range you’re
receiving, and the upper range limit you can request.
The consequences of a range request failure depend on the
implementation. But if the web server failed to deliver the requested
range, it will return an HTTP 416 Range Not Satisfiable error.
But that is typically used when your range request goes out of bounds. It is also possible that the client is performing a range request on a web server that doesn’t support this.
In that case, there various outcomes:
HTTP 406 Not acceptable error.But if you want to properly perform range requests, and avoid failures, it’s a bit of a chicken or egg situation:
Accept-Ranges: bytes header?You could send a
HEADrequest first, instead of aGETrequest, and look for anAccept-Rangesheader before performing the actual range request.
Varnish supports client-side range requests. This means that if a
client sends a Range header that Varnish can satisfy, the client will
receive the requested range, whether the origin supports it or not.
However, Varnish disables backend range requests for cached requests by default. This means that if a range request for a resource results in a cache miss, a regular HTTP request is sent to the origin. Varnish will store the full response in cache and will return the requested range to the client.
In most cases, this allows efficient collapsing of the requests, but for large objects, it can lack efficiency.
If the requested resource is a really large file, Varnish will need to ingest and cache the beginning of the file before it reaches the requested range that it will then serve.
If network throughput between Varnish and the origin is poor, the client will experience additional latency. Also, if returning this resource consumes a lot of server resources at the origin, it will also add latency.
However, if the requested range for this cache miss starts at the first byte, Varnish will leverage content streaming. This means that the client will not have to wait until the complete resource is stored in Varnish and will receive the data in chunks, as it is received by Varnish.
If you don’t want to support ranges, you can just disable the
http_range_support runtime parameter.
Although Varnish doesn’t natively support backend range requests, we can write some VCL to get the job done.
As mentioned before, VCL will be covered in detail in the next chapter. We’ll just explain the concept of the example below, without focusing too much on syntax:
sub vcl_recv {
# if there's no Range header we like, use
# "0-" which means "from the 0-th byte till the end"
if (req.http.Range ~ "bytes=") {
set req.http.x-range = "req.http.Range";
} else {
set req.http.x-range = "bytes=0-";
}
}
sub vcl_hash {
hash_data(req.http.x-range);
}
sub vcl_backend_fetch {
set bereq.http.Range = bereq.http.x-range;
}
sub vcl_backend_response {
if (beresp.status == 206) {
set beresp.ttl = 10m;
set beresp.http.x-content-range = beresp.http.Content-Range;
}
}
sub vcl_deliver {
if (resp.http.x-content-range) {
set resp.http.Content-Range = resp.http.x-content-range;
unset resp.http.x-content-range;
}
}
When Varnish receives a Range request header, it will store its value
in a custom x-range request header. This value will also be added to
the hash key to create a new cached entry per range.
Because Varnish will remove the Range header before initiating a
backend fetch, we will explicitly set a new Range header containing
the value of x-range.
When the backend responds with a Content-Range header and an HTTP
206 status code, we’ll store the value of the Content-Range header in
a custom x-range header, knowing that Varnish will strip off the
original Content-Range header.
Before returning the range to the client, we set the Content-Range
response header with the value that was captured from the origin.
At some point in time, Varnish will support native backend range requests and will probably store ranges separately in the cache. There might even be a more efficient solution than the VCL example above. But until then, you need to be aware of the limitations, potentially use the VCL example, and plan accordingly.