-
Notifications
You must be signed in to change notification settings - Fork 343
Varnish Support
This wiki page is for documenting the steps for adding Varnish Cache support to t3c
.
This section discusses mapping ATS configuration files to VCL (Varnish Configuration Language).
This mapping is only in the context of Traffic Control so some of the functionality of ATS configuration files might not be addressed.
From ipallowdotyaml.go:
- No conditions are set on outbound connections.
- Only methods with conditions are:
PUSH
,DELETE
andPURGE
. - localhost is trusted.
One way to meet the previous specification:
- Use
acl
for each method containing only allowed ips for that method. - At the start of
vcl_recv
check for each method with the matchingacl
.
Example:
acl purgers {
...
"127.0.0.1";
}
sub vcl_recv {
if (req.method == "PURGE") {
if (!client.ip ~ purgers) {
return (synth(405, "Not allowed."));
}
return (purge);
}
}
From cachedotconfig.go:
-
never-cache
is the only specified action so the config is only used to disable caching for some origins
There are multiple ways to achieve that in VCL using VMODs, one of them:
Using selector
VMOD to make a set of the specified origins and matching the set with backend response.
Example:
import selector;
backend ds1 {
.host = "my-origin.com";
.port = "8000";
}
backend ds2 {
.host = "another-origin.com";
.port = "80";
}
sub vcl_init {
new no_cache = selector.set();
no_cache.add("ds1");
no_cache.add("ds2");
...
}
sub vcl_backend_response {
if (no_cache.match(beresp.backend.name)) {
beresp.uncacheable = true;
}
}
From sslmulticertdotconfig.go:
- only
ssl_cert_name
andssl_key_name
are used. - no
dest_ip
is specified so ATS uses SNI if possible or choose default cert as mentioned in certificate selection.
Varnish doesn't support TLS natively so a solution to that is to use Hitch. It's commonly used with Varnish to terminate TLS/SSL connections and forward them to Varnish.
To use multiple certs with Hitch we update the configuration in /etc/hitch/hitch.conf
:
frontend = {
host = "10.10.10.131"
port = "443"
}
backend = "[127.0.0.1]:8443"
pem-file = {
cert = "/config/ds.cer"
private-key = "/config/ds.key"
}
pem-file = {
cert = "/config/ds2.cer"
private-key = "/config/ds2.key"
}
write-proxy-v2 = on
Hitch will try to use SNI if possible and will default to last cert specified as mentioned in vhosts
Varnish will need to be configured to listen on the backend IP with proxy enabled to get which IP and port client is connected to as mentioned in Hitch docs.
varnishd -a :80 -a :8443,PROXY
From snidotyaml.go:
- only
http2
andvalid_tls_versions_in
are used with each server FQDN
Hitch does not support specifying HTTP/2 or TLS version based on domain. It is only defined on all IPs used.
Example:
frontend {
...
}
alpn-protos = "h2, http/1.1"
tls-protos = TLSv1.2 TLSv1.3
So, It is either supported for all domains on the cache server or not.
varnishd
will need to be configured with -p feature=+http2
option to enable HTTP/2 support.
For this configuration file there are three parts formats, filters and log files. All of them can be done using varnishncsa
with some differences.
ATS supports aggregating logs for a period of time which Varnish does not support. But according to loggingdotyaml.go it is not used so only mapping from log fields from ATS to Varnish and using -F
flag with varnishncsa
.
Example:
formats:
- name: examplefmt
format: '%<chi> , %<pssc>'
Using varnishncsa
:
varnishncsa -F "%h , %s"
There are some differences between how ATS filter logs and how filtering works with Varnish:
- It allows specifying multiple operands as comma separated values (e.g.
1,2,3
,POST,GET
) which isn't supported in Varnish But it can be simplified to multiple expressions withor
in case of accept andand not
in case of reject. -
wipe_field_value
has no direct mapping to Varnish. - It supports specifying IP range to match against which Varnish also doesn't support.
- It supports comparing strings case-insensitive for specific filters, which Varnish doesn't support directly but it can do all matching case-insensitive by using
-C
flag. For specific filter there is no direct way but it can be specified with(?i)
regex matching.
Example:
filters:
- name: examplefilter
action: accept
condition: cqhm MATCH POST,PUT
Using varnishncsa
:
varnishncsa -q 'ReqMethod eq POST or ReqMethod eq PUT'
The way varnishncsa
works is in three modes backend mode, client mode or both. Fields related to client connections only show up in client mode and fields related to backend connections only show up in Backend mode (using both modes just outputs two log lines for each mode). So log lines can't have fields for both client and backend at the same time (it will show -
for the field that it can't output). ATS doesn't work like that so it can have fields with details throughout the session with no problem.
So, for example to get the response code returned to user from server and to server from origin it can specified in ATS as follows:
formats:
- name: responses
format: '%<pssc> , %<sssc>'
Trying that with varnishncsa
:
$ varnishncsa -bc -F "%{VSL:RespStatus}x %{VSL:BerespStatus}x"
- 200
200 -
It returns two log lines containing the response from either the origin or the cache server but not both. There is no current solution to that so either specify the needed fields and run in backend and client modes with %{Varnish:side}x
to distinguish between which log line belongs to what mode or for each log file in ATS it gets mapped to two log files one logging backend fields and the other logging client fields.
This shouldn't be a problem with filtering because VSL queries by default filter on all steps of the transaction as mentioned here.
For mapping log fields from ATS to Varnish they can be grouped into three categories:
- Fields with direct mapping like request method and status codes.
- Fields in ATS that either returns different format from its Varnish equivalent or doesn't have an equivalent field but could be synthesized form VCL code using
std.log("key:value")
and using%{VCL_Log:key}
to get thevalue
. For example fields related to SSL, some of them can be generated usingvmod_proxy
withstd.log()
. - Fields that can't be mapped to Varnish like proxy UUID which is the UUID for the running ATS process.
Differences between ATS and Varnish logging:
-
varnishncsa
doesn't support writing binary logs so only ascii logs can be used.varnishlog
supports binary logs but cannot be formatted. so, if used,varnichncsa
will need to consume its logs to output ascii logs with proper formatting. -
varnishncsa
doesn't support log rotation sologrotate
can be used. - multiple
varnishncsa
daemons will be running for each log file as it only supports writing to one file.
For example logging.yaml
file:
logging:
filters:
- name: examplefilter
action: accept
condition: cqhm MATCH POST,PUT
formats:
- name: examplefmt
format: '%<chi> , %<pssc>'
logs:
- filename: example.log
format: examplefmt
filters:
- examplefilter
rolling_enabled: log.roll.size
rolling_size_mb: 80
varnishncsa
command would be:
varnishncsa -q 'ReqMethod eq POST or ReqMethod eq PUT' \
-F "%h , %s" \
-w /var/log/varnish/example.log -a -D
logrotate
config would be:
/var/log/varnish/example.log {
rotate 7
compress
size 80M
postrotate
systemctl -q is-active varnishncsa.service || exit 0
systemctl reload varnishncsa.service
endscript
}
Note that paths used and logrotate
configuration are specified in records.config
.
Also logrotate
doesn't rotate in a less than daily period so rolling_interval_sec
might be discarded.
From volumedotconfig.go:
- volume size is irrelevant.
- it is used to separate different storage types with different volume IDs.
So, instead of mapping volume IDs 1,2 and 3 to Drive, RAM Drive and SSD Drive a prefix could be added to each storage used in varnishd
options.
Example:
varnishd -s d0=file,/dev/vda -s d1=file,/dev/vdb -s r0=malloc -s s0=file,/dev/vdc -s s1=file,/dev/vdd
Prefix d for Drive, r for RAM and s for SSD.
From hostingdotconfig.go:
- RAM is used to store content form origins if RAM Drive is used.
- any other content is stored on Drive or RAM if Drive is not used.
VMOD selector
can be used as mentioned in cache.config with std
, var
and random
:
import var;
import std;
import random;
import selector;
...
sub vcl_backend_response {
if (origins.match(beresp.backend.name)) {
var.set_int("r", std.integer(std.round(std.random(0, 2)), 0));
if (var.get_int("r") == 0) {
beresp.storage = storage.r0;
}
else if (var.get_int("r") == 1) {
beresp.storage = storage.r1;
}
else {
beresp.storage = storage.r2;
}
}
else {
# same for non origin content with Drive storage
}
}
From storagedotconfig.go:
- RAM Drives uses path to drive which Varnish doesn't support with malloc. So, either use malloc with the same size as the RAM Drive or treat it as a file storage.
Example storage.config
:
/dev/vda volume=1
/dev/vdb volume=1
/dev/vdc volume=2 <- RAM Drive
to match it with varnishd
:
varnishd -s d1=file,/dev/vda -s d2=file,/dev/vdb -s r0=file,/dev/vdc
Notes:
- to apply storage changes to Varnish instance, systemctl will be reloaded to register new changes in varnish service unit file with
systemctl daemon-reload
andvarnishd
will be restartedsystemctl restart varnish.service
. - another option to handle Varnish storage is VMOD slash which offers more configuration.
plugin.config
is mainly responsible for loading plugins to ATS at runtime. For the list of stable plugins a lot of them are either supported natively in VCL like Header Rewrite
or Regex Remap
and some are available as VMODs like AWS S3 Authentication
as awsrest
(see VMODs for more info) while some are not implemented.
For plugins used in Traffic Control the equivalent VMOD will be imported and configured the same or by using VCL to have the same effect.
This configuration file is mainly about cache hierarchy and how requests are sent to parent cache servers and origins. The list of Parent cache servers are exhausted first before request is sent to the origin.
The algorithm used for selecting parents is defined by round_robin
and the relation between primary parents and secondary parents is controlled by secondary_mode
in case of consistent hash algorithm.
For defining and using parent servers VMOD director
is used.
round_robin
field can be one of five algorithms:
-
true
andstrict
which are replaced withround_robin
director. -
false
which could be replaced withrandom
director. -
consistent_hash
which is replaced withshard
director. -
latched
which is replaced withfallback
director.
Since the request is sent to origin if no parent is healthy, all directors will be used with the origin backend in a fallback
director.
backend be1 {
.host = "parent1.example.com";
.port = "80";
}
backend be2 {
.host = "parent2.example.com";
.port = "80";
}
backend be3 {
.host = "origin.example.com";
.port = "80";
}
sub vcl_init {
new dir = directors.round_robin();
dir.add_backend(be1);
dir.add_backend(be2);
new fdir = directors.fallback();
fdir.add_backend(dir.backend());
fdir.add_backend(be3);
}
For secondary_mode
there are only two options used in ATC:
-
0
which chooses a server from the parent list then exhaust the secondary parent list if the parent is down then revert to the parent list if no secondary is available. There is no straightforward way to do that in VCL but with parameter manipulation of theshard
director it might be achievable.
sub vcl_init {
new primary = directors.shard();
primary.add_backend(be1);
primary.add_backend(be2);
new secondary = directors.shard();
secondary.add_backend(be3);
new fdir = directors.fallback();
fdir.add_backend(secondary.backend(resolve=LAZY));
fdir.add_backend(primary.backend(resolve=LAZY));
fdir.add_backend(be4);
}
sub vcl_recv {
// healthy=IGNORE to get the primary backend even if unhealthy to decide whether to skip parent director or not.
set req.backend_hint = primary.backend(healthy=IGNORE, resolve=NOW);
if(!std.healthy(primary.backend(healthy=IGNORE, resolve=NOW))) {
// use fallback director to exhaust secondary list then primary then go to origin.
set req.backend_hint = fdir.backend();
}
}
-
1
which exhausts parents list then secondary parents list then goes to the origin.
It could be done like this:
sub vcl_init {
...
new fdir = directors.fallback();
fdir.add_backend(primary.backend(resolve=LAZY));
fdir.add_backend(secondary.backend(resolve=LAZY));
fdir.add_backend(be4);
}
sub vcl_recv {
set req.backend_hint = fdir.backend();
}
resolve=LAZY
is used so shard_param
could be used to change backend parameters after backend is assigned to the request.
parent_retry
field is related to using different backend based on response code from chosen parent. unavailable_server_retry
marks the backend as unhealthy which is not achievable from VCL, while simple_retry
is for retrying a different backend after a certain response is returned which there is no documentation on changing backends after request is sent. So, all fields related to parent_retry
might not be used.
qstring
is used for including the query string with the URL as the hash key. It can be removed by using VMOD querystring
or by just using regsuball
to remove it from the URL using regular expression.
go_direct
will result in not including the parent directors in a fallback
director but only include the origins. And parent_is_proxy
will determine if go_direct
is used.
Example line in parent.config
:
dest_domain=origin.example.com port=80 parent="parent1.example.com:80|1.0;parent2.example.com:80" round_robin=strict
Similar VCL:
import directors;
probe default {
.url = "/";
.timeout = 1s;
.interval = 4s;
.window = 5;
.threshold = 3;
}
backend be1 {
.host = "parent1.example.com";
.port = "80";
.probe = default;
}
backend be2 {
.host = "parent2.example.com";
.port = "80";
.probe = default;
}
backend origin {
.host = "origin.example.com";
.port = "80";
.probe = default;
}
sub vcl_init {
new parents = directors.round_robin();
parents.add_backend(be1);
parents.add_backend(be2);
new fdir = directors.fallback();
fdir.add_backend(parents.backend());
fdir.add_backend(origin);
}
sub vcl_recv {
// if remap rule for example.com to go to origin.example.com
if (req.http.host == "example.com") {
set req.backend_hint = fdir.backend();
}
}
Notes:
- Nesting directors (especially
shard
directors) is not well documented so some assumptions might not hold true. - Backends health check will be done using probes and not with responses returned from the backend.