3. VCL - Varnish 配置语言详解

本文编译自:
users-guide

本节讲述如何使用 VCL 编写处理 HTTP 流量的策略。

Varnish 的配置系统与其他软件不同,一般的配置是使用配置指令,用于打开或关闭某个配置项。而 Varnish 使用 VCL 语言进行配置。

每一个入站请求流经 Varnish 时,会被 VCL 定义的策略处理。你可以通过修改 VCL 代码,改变对请求的处理。Varnish 不仅仅是一个缓存,它还是一个强大的 HTTP 处理器,你可以对 HTTP 请求做如下的事情:

  • 将某些请求调度至某些后端服务器
  • 对请求和响应进行修改
  • 根据请求和响应的任意属性,做特定的操作

VCL 被编译成二进制代码,并在请求到来时被执行。VCL 对性能的影响可忽略不计。

VCL 文件组成 subroutines,不同的 subroutines 在不同的时候被执行。比如有一个 subroutine 是当接收到请求时执行,另一个是当从后端服务器获取到响应时执行。

如果在你的 subroutine 中不调用 action,该 subroutine 执行完毕之后,Varnish 会执行内建的 VCL 代码。这些代码在 builtin.vcl 文件中,它们被注释了。

目录:

  • VCL 语法
  • 内建的 subroutines
  • Request and response VCL objects
  • actions
  • 后端服务器
  • 多个后端服务器
  • Varnish 中的后端服务器和虚拟主机
  • 调度器
  • 健康检查
  • Hashing
  • 不工作的后端服务器
  • 使用 inline C to 扩展 Varnish
  • VCL Examples
  • 设备检测

VCL 语法


VCL 的语法从 C 语言继承了很多,所以它读起来像简单的 C 或者 Perl。代码段被花括号括起来,语句以分号结尾,注释的方式可以是 C,C++,Perl 中的注释方式,可随意选择你习惯的注释语法。但 VCL 不支持循环和跳转语句。

本节介绍 VCL 语法中较为重要的部分,详细的介绍参考 reference 文档。

Strings


基本的字符串被双引号引用,比如 " ... ",其中不可包含换行符。\ 没有转义的特殊含义,比如在 regsub() 中,你不需要数 \ 的个数:

regsub("barf", "(b)(a)(r)(f)", "\4\3\2p") -> "frap"

长字符串被 {" ... "} 引用,其中可包含任何字符,包括 " 字符,换行符及其它控制字符,除了 NUL (0x00) 字符。如果你真的想要把 NUL 字符放入字符串中,有一个 VMOD 模块可以创建这样的字符串。

Access control lists (ACLs)


访问控制列表 ACLs。

声明一个 ACL,可以创建及初始化一个访问控制列表,它有一个名字,它可以被用于匹配 client 地址:

acl local {
  "localhost";         // myself
  "192.0.2.0"/24;      // and everyone on the local network
  ! "192.0.2.23";      // except for the dialin router
}

如果一个 ACL entry 指定了一个不能解析的主机名,那么这个 ACL 列表与任何地址都可以匹配。如果这个不能被解析的主机名前面有一个 ! 符号,它会拒绝每一个与它进行比较的地址,这可能不是你希望的效果。如果把一个 ACL entry 放入圆括号 () 中,它将被忽略。

将一个 IP 地址与一个 ACL 进行匹配,使用 ~ 运算符:

if (client.ip ~ local) {
  return (pipe);
}

Operators


在 VCL 中可以使用的操作符:

=   赋值
==  等于
~   匹配,用于 regular expressions 或 ACLs 
!   逻辑非.
&&  逻辑与
||  逻辑或

Subroutines


subroutine 可被称为子例程。

一个 subroutine 是一组代码,可被重用,下面是一个定义 subroutine 的示例:

sub pipe_if_local {
  if (client.ip ~ local) {
    return (pipe);
  }
}

在 VCL 中,subroutine 不可接受参数,也没有返回值。

调用 subroutine,使用 call 关键字:

call pipe_if_local;

Varnish 有许多内置的 subroutines,当 transaction 流经 Varnish 时,它们将被调用。内置的 subroutine 命名为 vcl_*,你自己定义的 subroutine 不可以命名为以 vcl_ 起始的名字。

内建的 subroutines


Varnish 处理 client 的请求和后端服务器的响应时,会调用多个内置的 subroutines 进行处理。通过 CLI 执行 vcl.load 和 vcl.discard 时,也会调用内置的 subroutines。

下面对前端(client-side)和后端(backend-side)的处理分别进行介绍:

1.client-side


1.1 vcl_recv


vcl_recv 被调用的时机是:

  • 当一个请求接收完成,并且被解析之后
  • 重启之后
  • as the result of an ESI include

vcl_recv 子例程用于决定:是否对一个请求提供服务,它很可能会对请求进行修改,然后再决定如何做进一步处理。

A backend hint may be set as a default for the backend processing side.

vcl_recv 子例程可使用 return() 结合下面的其中一个关键字进行终止:

hash
    请求的对象被认为是一个可能被缓存的对象,将继续对其进行处理。将控制权转交给 vcl_hash 子例程。

pass
    转换至 pass 模式。控制权最终交给 vcl_pass 子例程。

pipe
    转换至 pipe 模式。控制权最终交给 vcl_pipe 子例程。

synth(status code, reason)
    转移到 vcl_synth 子例程,synth() 的参数值被预置为 resp.status 和 resp.reason

purge
    清除请求的对象,以及它的变量(variants)。控制权先交给 vcl_hash,最终交给 vcl_purge

[vcl_synth][2]
[2]:https://www.varnish-cache.org/docs/4.0/users-guide/vcl-built-in-subs.html#vcl-synth

1.2 vcl_pipe


进入 pipe 模式时,vcl_pipe 子例程将被调用。在这个模式中,请求将被传递给后端服务器,这时 Varnish 会降级成为一个 TCP 代理,只充当一个数据流的通道,不会对数据进行任何修改,当 client 或 server 端决定关闭连接时,该模式结束。在调用 vcl_pipe 之后,对于一个处于 pipe 模式的连接,其他任何的 VCL 子例程都不会被调用。

vcl_pipe 子例程可使用 return() 结合下面的其中一个关键字进行终止:

pipe
    继续以 pipe mode 运行

synth(status code, reason)
    转移到 vcl_synth 子例程,synth() 的参数值被预置为 resp.status 和 resp.reason

1.3 vcl_pass


在进入 pass 模式时,vcl_pass 将被调用,请求被转发给后端服务器,后端服务器的响应被转发给 client,但是响应不会被缓存。来自该 client 连接的后续请求,将被正常处理。

vcl_pass 子例程可使用 return() 结合下面的其中一个关键字进行终止:

fetch
    继续以 pass mode 运行 - 发起一个对后端服务器的请求

restart
    重启该 transaction。增加 restart 计数器的计数。如果计数超过了 max_restarts,Varnish 发出一个错误:guru meditation error.

synth(status code, reason)
    转移到 vcl_synth 子例程,synth() 的参数值被预置为 resp.status 和 resp.reason

1.4 vcl_hit


当缓存查找成功,vcl_hit 将被调用。缓存对象可能会过期,其 ttl 可能为 0 或者负数,with only grace or keep time left.

vcl_hit 子例程可使用 return() 结合下面的其中一个关键字进行终止:

deliver
    发送该对象。如果该对象过期,将触发一个 fetch 调用,更新该对象。

fetch
    尽管缓存命中,但是会同步地从后端服务器更新缓存对象。控制权最终转交给 vcl_miss。

pass
    转换至 pass 模式。控制权最终交给 vcl_pass 子例程。

restart
    重启该 transaction。增加 restart 计数器的计数。如果计数超过了 max_restarts,Varnish 发出一个错误:guru meditation error.

synth(status code, reason)
    转移到 vcl_synth 子例程,synth() 的参数值被预置为 resp.status 和 resp.reason

1.5 vcl_miss


当缓存查找失败,或者当 vcl_hit 返回一个 fetch 时,调用 vcl_miss。

vcl_miss 用于决定是否尝试从后端服务器获取文件。

A backend hint may be set as a default for the backend processing side.

vcl_miss 子例程可使用 return() 结合下面的其中一个关键字进行终止:

fetch
    从后端服务器获取请求的对象。控制权最终转交给 vcl_backend_fetch。

pass
    转换至 pass 模式。控制权最终交给 vcl_pass 子例程。

restart
    重启该 transaction。增加 restart 计数器的计数。如果计数超过了 max_restarts,Varnish 发出一个错误:guru meditation error.

synth(status code, reason)
    转移到 vcl_synth 子例程,synth() 的参数值被预置为 resp.status 和 resp.reason

1.6 vcl_hash


当 vcl_recv 为请求创建了一个 hash 值时被调用。使用该值作为 key 进行缓存查找。

vcl_hash 子例程只能以 return(lookup) 终止:

lookup
    在缓存中查找请求的对象。如果从 vcl_recv 返回 return(purge),控制权转交给 vcl_purge。
    否则,如果缓存查找的结果是 hit,控制权转交给 vcl_hit;如果缓存查找的结果是 miss,控制权转交给 vcl_miss;
    如果缓存查找的结果是 hit on a hit-for-pass 对象 (object with obj.uncacheable == true),控制权转交给 vcl_pass。

1.7 vcl_purge


执行 purge 之后,vcl_purge 被调用,缓存对象被清除(失效),其所有变量(variants)将被回避。

vcl_purge 子例程可使用 return() 结合下面的其中一个关键字进行终止:

restart
    重启该 transaction。增加 restart 计数器的计数。如果计数超过了 max_restarts,Varnish 发出一个错误:guru meditation error.

synth(status code, reason)
    转移到 vcl_synth 子例程,synth() 的参数值被预置为 resp.status 和 resp.reason

1.8 vcl_deliver


发送对象给客户端前调用,除了将一个 vcl_synth 结果发送给客户端时不会调用。

vcl_deliver 子例程可使用 return() 结合下面的其中一个关键字进行终止:

deliver
    发送对象给 client

restart
    重启该 transaction。增加 restart 计数器的计数。如果计数超过了 max_restarts,Varnish 发出一个错误:guru meditation error.

synth(status code, reason)
    转移到 vcl_synth 子例程,synth() 的参数值被预置为 resp.status 和 resp.reason

1.9 vcl_synth


调用 vcl_synth 可以发送一个 synthetic 对象给客户端。synthetic 对象由 VCL 生成,不是从后端获取的。可使用 synthetic() 函数构造 synthetic 对象。

vcl_synth 定义了一个对象,该对象不会被缓存,与其相反,vcl_backend_error 所定义的对象可能最终被缓存。

vcl_synth 子例程可使用 return() 结合下面的其中一个关键字进行终止:

deliver
    直接将 vcl_synth 定义的对象发送给客户端,不调用 vcl_deliver

restart
    重启该 transaction。增加 restart 计数器的计数。如果计数超过了 max_restarts,
    Varnish 发出一个错误:guru meditation error.

2. backend-side


2.1 vcl_backend_fetch


对后端服务器发送请求时调用 vcl_backend_fetch。在这个子例程中,我们一般会修改请求,然后才发送给后端服务器。

vcl_backend_fetch 子例程可使用 return() 结合下面的其中一个关键字进行终止:

fetch
    从后端服务器获取对象

abandon
    放弃对后端发起请求。除非后端请求是一个 background fetch,否则控制权将被转交给 client-side 的 vcl_synth,
    其 resp.status 被设置为 503。

2.2 vcl_backend_response


当成功从后端服务器获取到 response headers 时,调用 vcl_backend_response。

vcl_backend_response 子例程可使用 return() 结合下面的其中一个关键字进行终止:

deliver
    对于一个 304 响应,创建一个更新的缓存对象。否则,从后端获取对象的 body,然后发起  delevery 返回给客户端。
    很可能是并行的(streaming)

abandon
    放弃对后端发起请求。除非后端请求是一个 background fetch,否则控制权将被转交给 client-side 的 vcl_synth,
    其 resp.status 被设置为 503。
        
retry
    重试发起 backend transaction。增加重试计数,如果重试次数超过 max_retries,控制权转交给 vcl_backend_error

2.3 vcl_backend_error


当尝试从后端获取对象失败,或则重试次数超过 max_retries 时,vcl_backend_error 将被调用。

VCL 生成一个 synthetic 对象,可使用 synthetic() 函数构造 synthetic 对象的 body。

vcl_backend_error 子例程可使用 return() 结合下面的其中一个关键字进行终止:

deliver
    发送 vcl_backend_error 定义的对象,可能的话,缓存该对象。就如同该对象是从后端获取的一般。这也被称为 "backend synth"。

retry
    重试发起 backend transaction。增加重试计数,如果重试次数超过 max_retries,调用 client-side 的 vcl_synth,
    其 resp.status 被设置为 503。

3. vcl.load / vcl.discard


3.1 vcl_init


当加载 VCL 之后,vcl_init 被调用。一般用于初始化 VMODs。

vcl_init 子例程可使用 return() 结合下面的其中一个关键字进行终止:

ok
    正常返回,VCL 继续加载

fail
    停止加载这个 VCL

3.2 vcl_fini


当一个 VCL 被废弃,当该 VCL 处理完所有请求,调用 vcl_fini。一般用于清除 VMODs。

vcl_fini 子例程可使用 return() 结合下面的其中一个关键字进行终止:

ok
    正常返回,VCL 将被废弃。

Request and response VCL objects


在 VCL 中有一些重要的对象需要注意,这些对象可以使用 VCL 语言访问和操控。

req

req 是请求对象。当 Varnish 收到请求之后,req 对象被创建,请求被填入该对象中。在 vcl_recv 中,你大部分的工作是对该对象进行的。

bereq

bereq 是后端请求对象。Varnish 构造该对象,然后发送给后端服务器。这个对象是基于 req 对象创建的。

beresp

beresp 是后端响应对象。该对象包含后端响应的 headers。如果要修改一个来自后端的响应,实际的操作是在 vcl_backend_response 子例程中对 beresp 对象进行修改。

resp

resp 对象,其内容发送给客户端的 HTTP response,必要时,一般在 vcl_deliver 中对其进行修改。

obj

obj 是用于缓存的对象,它是只读的。

actions


actions 是在终止一个内置子例程时,配合 return() 使用的,如 return(pass),最常用的 actions 是这些:

pass

当你返回 pass,请求将被传递给后端服务器,随后的响应将从后端服务器传递回来。响应不会被缓存。pass 可从 vcl_recv 中返回。

hash

如果从 vcl_recv 中返回 hash,Varnish 将尝试从缓存中查找并返回请求的对象。

pipe .. XXX:What is pipe? benc

如果从 vcl_recv 返回 pipe,将会进入 pipe 模式,Varnish 将前端与客户端的连接,以及与后端服务器的连接合并成一个数据流的通道,Varnish 不对数据做任何修改,只是将数据在两端发送,所以你的日志是不完整的。

deliver

将对象发送给客户端。通常是从 vcl_backend_response 中返回 deliver。

restart

重启对请求的处理。你可以重启整个 transaction,对 req 对象的修改将被保留。

retry

重试对后端发起请求。这个 action 可以从 vcl_backend_response 或 vcl_backend_error 中返回。当你不喜欢后端返回的响应时,可以这样使用。

后端服务器


后端服务器是 Varnish 中的一个概念,指真正提供内容的 web 服务器。它们也被称为 “origin” server 或 “upstream” server。后端 web 服务器通过 Varnish 的缓存功能对访问进行加速。

我们来编辑配置文件,如果是编译安装,路径为 /usr/local/etc/varnish/default.vcl,如果是软件包安装,路径为 /etc/varnish/default.vcl

其中多数被注释了,其中可能有如下内容:

vcl 4.0;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

这里定义了一个后端服务器,名为 default。当 Varnish 需要从后端获取内容,它会连接到 127.0.0.1:8080。

Varnish 可定义多个后端服务器,并且可以联合多个后端服务器为一个后端集群,进行负载均衡。

多个后端服务器


在上面的基础上,我们引入一个 java 服务器:

backend java {
    .host = "127.0.0.1";
    .port = "8000";
}

在 vcl_recv 中,我们根据请求的 URL,给请求打上不同的 hint,使其发往指定的后端服务器。

sub vcl_recv {
    if (req.url ~ "^/java/") {
        set req.backend_hint = java;
    } else {
        set req.backend_hint = default;
    }
}

如果要将移动设备发出的请求,发给专门的后端服务器,可以做这样的判断:

if (req.http.User-agent ~ /mobile/) .. 

Varnish 中的后端服务器和虚拟主机


Varnish 完全支持虚拟主机。

我们在 vcl_recv 中为 HTTP 请求设定路由,如果希望基于“虚拟主机”做路由,那么就对 req.http.host 变量做检查,比如:

sub vcl_recv {
    if (req.http.host ~ "foo.com") {
        set req.backend_hint = foo;
    } elsif (req.http.host ~ "bar.com") {
        set req.backend_hint = bar;
    }
}

第一个正则表达式,能匹配 "foo.com", "www.foo.com", "zoop.foo.com",以及其他以 "foo.com" 结尾的主机名。

也可以使用 == 进行准确的匹配:

sub vcl_recv {
    if (req.http.host == "foo.com" || req.http.host == "www.foo.com") {
        set req.backend_hint = foo;
    }
}

调度器


我们可以将多个后端服务器组成一组,Varnish 中的组被称为 directors。将多个后端服务器组成一组可以提高性能、容错性和伸缩性。

我们定义多个后端服务器,然后组成一个组。这要求你加载一个 VMOD:directors 模块。然后在 vcl_init 中调用一些 actions:

import directors;    # load the directors

backend server1 {
    .host = "192.168.0.10";
}
backend server2 {
    .host = "192.168.0.11";
}

sub vcl_init {
    new bar = directors.round_robin();
    bar.add_backend(server1);
    bar.add_backend(server2);
}

sub vcl_recv {
    # send all traffic to the bar director:
    set req.backend_hint = bar.backend();
}

这个 director 是一个轮询调度器。另外还有一个随机调度器。如果一个后端服务器失效,Varnish 可以检查到,然后不再调度请求给该后端服务器。

健康检查


我们来设置一个包含两个后端服务器的 director,并且设置健康检查,首先我们定义后端服务器:

backend server1 {
    .host = "server1.example.com";
    .probe = {
        .url = "/";
        .timeout = 1s;
        .interval = 5s;
        .window = 5;
        .threshold = 3;
    }
}

backend server2 {
    .host = "server2.example.com";
    .probe = {
        .url = "/";
        .timeout = 1s;
        .interval = 5s;
        .window = 5;
        .threshold = 3;
    }
}

probe 引入了健康检查。此例中,Varnish 每 5 秒进行一次检查,检查超时为 1 秒。如果最近 5 次检查中,有 3+ 次成功,该后端被标记为 “healthy”,否则被标记为 “sick”。发起检查,其实是对后端服务器地址 http://host/ 发起一个 GET 请求.

更多信息可参考 Probes

我们现在来定义 director:

import directors;

sub vcl_init {
    new vdir = directors.round_robin();
    vdir.add_backend(server1);
    vdir.add_backend(server2);
}

现在可以使用 vdir 作为 backend_hint,参考上一小节的描述,比如:

sub vcl_recv {
    # send all traffic to the vdir director:
    set req.backend_hint = vdir.backend();
}

Varnish 不会将流量调度给被认为不健康的后端主机。

如果所有后端服务器失效了,Varnish 可以返回 stale content(陈旧的内容)。参考 Misbehaving servers

请注意,Varnish 对所有加载的 VCL 进行检查,而且会将相同的检查进行合并。所以如果你加载了很多的 VCL,注意不要修改 probe 配置。卸载 VCL 之后,检查不会继续,更多信息请求参考:ref:reference-vcl-director

Hashing


Varnish 对内容做缓存时,也要存下对应的 hash key。在默认的情况下,hash key 是对 Host 首部字段的值或者 IP 地址做 hash 计算,并且对 URL 做 hash 计算,得到的值,为 hash key。

the default vcl:

sub vcl_hash {
    hash_data(req.url);
    if (req.http.host) {
        hash_data(req.http.host);
    } else {
        hash_data(server.ip);
    }
    return (lookup);
}

在默认 vcl_hash 中,首先对 URL 计算 hash 值,然后继续对 Host 字段或者 IP地址计算 hash 值。

浏览器一般会把 hostnames 转为小写,Varnish 不做这种转换,所以在建立 hash key 时,对 hostnames 和 URL 的大小写是敏感的。所以理论上来说,"Varnish.org/" and "varnish.org/" 计算出的 hash key 是不同的。

这里对 hash 值的计算是逐步累加的,所以你可以在 hash 计算中加入特定测试区域的值,然后基于该测试区域的值,可以提供不同的缓存内容。

比如,通过客户端的 IP 地址获知其所在的国家,基于这个提供不同的语言的网站页面(需要加载一些 VMOD 获知国家的代码),比如:

In vcl_recv:

set req.http.X-Country-Code = geoip.lookup(client.ip);

And then add a vcl_hash:

sub vcl_hash {
    hash_data(req.http.X-Country-Code);
}

vcl_* 系列子例程中的默认代码会在执行完我们自己编写的代码后自动执行,所以在 vcl_hash 中,默认情况下,VCL 会自动对 host 和 URL 计算 hash 值,这部分不需要我们去做。

但要注意,不要轻易调用 return(lookup),因为它会终止默认 VCL 的执行,可能会导致 hash key 计算时,没有加入 host 和 URL。

不工作的后端服务器


Varnish 可以屏蔽不正常的 web/应用 服务器。

优雅模式


当有多个客户端请求同一个页面时,Varnish 只对 backend 发起一个请求。有的产品称之为“请求合并”。

如果你的站点每秒有几千的点击量,请求的等待队列可能变得相当的长。这可能导致两个问题,一是“惊群效益” - 突然开启一千个线程,使系统负载一下子到达上限;二是 - 没人愿意等待,人的耐心是有限的。面对这个问题,Varnish 可做的是,即使缓存对象超时(TTL)也不更新,向用户提供过期的服务页面。

所以,要提供过期内容,我们可以这样设置,当缓存对象达到超时时间(TTL),让其在缓存中多待 2 分钟,如下:

sub vcl_backend_response {
  set beresp.grace = 2m;
}

这样设置以后,Varnish 允许在缓存对象超时的 2 分钟之内,继续返回已经过期的缓存对象。而且虽然 Varnish 返回过期对象,它会第一时间将更新纳入计划中,等到 2 分钟后,更新会异步进行。

你也可以对默认的处理逻辑进行一定的修改,在 vcl_hit 中定义:

sub vcl_hit {
   if (obj.ttl >= 0s) {
       // A pure unaltered hit, deliver it 缓存对象没有过期
       return (deliver);
   }
   if (obj.ttl + obj.grace > 0s) {
       // Object is in grace, deliver it 缓存对象过期了,但在 grace mode 的时间范围内
       // Automatically triggers a background fetch 自动触发一个对上游服务器的 fetch 动作
       return (deliver);
   }
   // fetch & deliver once we get the result 超出了 grace mode 的时间,对上游服务器发起 fetch 动作,并且更新缓存对象
   return (fetch);
}

这里,优雅模式的逻辑是很清晰的。

如果你激活了健康检查,你可以知道一个 backend 是否是正常的,你可以在确认了 backend 是正常的之后,再使用 优雅模式 。我们把第二个 if 语句替换为:

if (!std.healthy(req.backend_hint) && (obj.ttl + obj.grace > 0s)) {
      return (deliver);
} else {
      return (fetch);
}

所以,总的来说,优雅模式解决了两个问题:

  • 返回过期的缓存对象,以避免等待队列过长
  • 对于是否返回过期对象,可由你进行控制

使用 inline C to 扩展 Varnish


(Here there be dragons. Big and mean ones.) 一般不建议使用 inline C,所以这里就不翻译了。

You can use inline C to extend Varnish. Please note that you can seriously mess up Varnish this way. The C code runs within the Varnish Cache process so if your code generates a segfault the cache will crash.

One of the first uses of inline C was logging to syslog.:

# The include statements must be outside the subroutines.
C{
        #include <syslog.h>
}C

sub vcl_something {
        C{
                syslog(LOG_INFO, "Something happened at VCL line XX.");
        }C
}

To use inline C you need to enable it with the vcc_allow_inline_c parameter.

VCL Examples


修改请求首部:


例如,对于 /images 目录的请求,我们希望移除请求中的 cookie:

sub vcl_recv {
  if (req.url ~ "^/images") {
    unset req.http.cookie;
  }
}

这样做以后,如果请求的 URI 是以 /images 起始的,那么请求的 "Cookie:" 首部将被移除。

修改后端响应


对于后端的图片类型的响应,我们在 vcl_backend_response 中做进一步处理:

  1. 删除 “Set-Cookie” 首部,这是为了避免创建 hit-for-pass 对象,它不会被缓存
  2. 设置 TTL 时间为 1h
    sub vcl_backend_response {
       if (bereq.url ~ "\.(png|gif|jpg)$") {
         unset beresp.http.set-cookie;
         set beresp.ttl = 1h;
      }
    }

访问控制列表 ACLs


访问控制列表的创建和使用:

# Who is allowed to purge.... 允许谁进行 PURGE 操作
acl local {
    "localhost";
    "192.168.1.0"/24; /* and everyone on the local network */
    ! "192.168.1.23"; /* except for the dialin router */
}

sub vcl_recv {
  if (req.method == "PURGE") {
    if (client.ip ~ local) {
       return(purge);
    } else {
       return(synth(403, "Access denied."));
    }
  }
}

Implementing websocket support


Websockets 技术,是在 HTTP 之上创建一个双向的流式通道。

要通过 Varnish 运行 websockets,需要使用 pipe,并且复制 Upgrade 首部:

sub vcl_pipe {
     if (req.http.upgrade) {
         set bereq.http.upgrade = req.http.upgrade;
     }
}
sub vcl_recv {
     if (req.http.Upgrade ~ "(?i)websocket") {
         return (pipe);
     }
}

设备检测


所谓设备检测,是通过请求中的 User-Agent 首部判断客户端使用的设备类型。

使用场景举例:给小屏幕的移动端客户(一般也是高延迟的网络)发送 size reduced files,或者提供客户端能识别的 streaming video codec。

对这种使用场景,有几种策略可以使用:

  1. 重定向至另一个 URL
  2. 对于特定的客户端使用不同的 backend
  3. 修改对 backend 发起的请求,使 backend 发送裁剪过的内容

为了讲清楚如何使用这些策略,我们假设存在一个首部名为 X-UA-Device(req.http.X-UA-Device),它可以区分不同的客户端类型:

设置该首部是很简单的:

sub vcl_recv {
    if (req.http.User-Agent ~ "(?i)iphone" {
        set req.http.X-UA-Device = "mobile-iphone";
    }
}

对于识别客户端,Varnish 提供了商业的和免费的方案。社区的免费方案是根据正则来做,请参考: https://github.com/varnish/varnish-devicedetect/.

1. 基于同一个 URL 提供不同的服务内容


基于同一个 URL 提供不同的服务内容,其中涉及的要点如下:

  1. 对客户端进行检测(非常简单,只需要 include devicedetect.vcl 并调用它)
  2. 如何告诉 backend 客户端的类型?举例来说,可以设置一个 header,或者改变一个 header,甚至改变 backend 请求的 URL
  3. 对 backend 返回的响应进行修改,添加 "Vary" header,这样 Varnish 会启动内部的处理
  4. 在最后阶段修改发送给客户端的响应,so any caches outside our control don't serve the wrong content

这些都需要去做,而且要保证,对于每一个 URL,每一个设备类型,只对应一个缓存对象。

Example 1: 发送 HTTP header 给 backend


The basic case is that Varnish adds the 'X-UA-Device' HTTP header on the backend requests, and the backend mentions in the response 'Vary' header that the content is dependant on this header.

Everything works out of the box from Varnish' perspective.

VCL:

sub vcl_recv {
    # call some detection engine that set req.http.X-UA-Device
}
# req.http.X-UA-Device is copied by Varnish into bereq.http.X-UA-Device

# so, this is a bit counterintuitive. The backend creates content based on
# the normalized User-Agent, but we use Vary on X-UA-Device so Varnish will
# use the same cached object for all U-As that map to the same X-UA-Device.
#
# If the backend does not mention in Vary that it has crafted special
# content based on the User-Agent (==X-UA-Device), add it.
# If your backend does set Vary: User-Agent, you may have to remove that here.
sub vcl_backend_response {
    if (bereq.http.X-UA-Device) {
        if (!beresp.http.Vary) { # no Vary at all
            set beresp.http.Vary = "X-UA-Device";
        } elseif (beresp.http.Vary !~ "X-UA-Device") { # add to existing Vary
            set beresp.http.Vary = beresp.http.Vary + ", X-UA-Device";
        }
    }
    # comment this out if you don't want the client to know your
    # classification
    set beresp.http.X-UA-Device = bereq.http.X-UA-Device;
}

# to keep any caches in the wild from serving wrong content to client #2
# behind them, we need to transform the Vary on the way out.
sub vcl_deliver {
    if ((req.http.X-UA-Device) && (resp.http.Vary)) {
        set resp.http.Vary = regsub(resp.http.Vary, "X-UA-Device", "User-Agent");
    }
}

Example 2: Normalize the User-Agent string


Another way of signalling the device type is to override or normalize the 'User-Agent' header sent to the backend.

For example:

User-Agent: Mozilla/5.0 (Linux; U; Android 2.2; nb-no; HTC Desire Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1

becomes:

User-Agent: mobile-android

when seen by the backend.

This works if you don't need the original header for anything on the backend. A possible use for this is for CGI scripts where only a small set of predefined headers are (by default) available for the script.

VCL:

sub vcl_recv {
    # call some detection engine that set req.http.X-UA-Device
}

# override the header before it is sent to the backend
sub vcl_miss { if (req.http.X-UA-Device) { set bereq.http.User-Agent = req.http.X-UA-Device; } }
sub vcl_pass { if (req.http.X-UA-Device) { set bereq.http.User-Agent = req.http.X-UA-Device; } }

# standard Vary handling code from previous examples.
sub vcl_backend_response {
    if (bereq.http.X-UA-Device) {
        if (!beresp.http.Vary) { # no Vary at all
            set beresp.http.Vary = "X-UA-Device";
        } elseif (beresp.http.Vary !~ "X-UA-Device") { # add to existing Vary
            set beresp.http.Vary = beresp.http.Vary + ", X-UA-Device";
        }
    }
    set beresp.http.X-UA-Device = bereq.http.X-UA-Device;
}
sub vcl_deliver {
    if ((req.http.X-UA-Device) && (resp.http.Vary)) {
        set resp.http.Vary = regsub(resp.http.Vary, "X-UA-Device", "User-Agent");
    }
}

Example 3: Add the device class as a GET query parameter


If everything else fails, you can add the device type as a GET argument.

http://example.com/article/1234.html --> http://example.com/article/1234.html?devicetype=mobile-iphone

The client itself does not see this classification, only the backend request is changed.

VCL:

sub vcl_recv {
    # call some detection engine that set req.http.X-UA-Device
}

sub append_ua {
    if ((req.http.X-UA-Device) && (req.method == "GET")) {
        # if there are existing GET arguments;
        if (req.url ~ "\?") {
            set req.http.X-get-devicetype = "&devicetype=" + req.http.X-UA-Device;
        } else {
            set req.http.X-get-devicetype = "?devicetype=" + req.http.X-UA-Device;
        }
        set req.url = req.url + req.http.X-get-devicetype;
        unset req.http.X-get-devicetype;
    }
}

# do this after vcl_hash, so all Vary-ants can be purged in one go. (avoid ban()ing)
sub vcl_miss { call append_ua; }
sub vcl_pass { call append_ua; }

# Handle redirects, otherwise standard Vary handling code from previous
# examples.
sub vcl_backend_response {
    if (bereq.http.X-UA-Device) {
        if (!beresp.http.Vary) { # no Vary at all
            set beresp.http.Vary = "X-UA-Device";
        } elseif (beresp.http.Vary !~ "X-UA-Device") { # add to existing Vary
            set beresp.http.Vary = beresp.http.Vary + ", X-UA-Device";
        }

        # if the backend returns a redirect (think missing trailing slash),
        # we will potentially show the extra address to the client. we
        # don't want that.  if the backend reorders the get parameters, you
        # may need to be smarter here. (? and & ordering)

        if (beresp.status == 301 || beresp.status == 302 || beresp.status == 303) {
            set beresp.http.location = regsub(beresp.http.location, "[?&]devicetype=.*$", "");
        }
    }
    set beresp.http.X-UA-Device = bereq.http.X-UA-Device;
}
sub vcl_deliver {
    if ((req.http.X-UA-Device) && (resp.http.Vary)) {
        set resp.http.Vary = regsub(resp.http.Vary, "X-UA-Device", "User-Agent");
    }
}

Different backend for mobile clients


If you have a different backend that serves pages for mobile clients, or any special needs in VCL, you can use the 'X-UA-Device' header like this:

backend mobile {
    .host = "10.0.0.1";
    .port = "80";
}

sub vcl_recv {
    # call some detection engine

    if (req.http.X-UA-Device ~ "^mobile" || req.http.X-UA-device ~ "^tablet") {
        set req.backend_hint = mobile;
    }
}
sub vcl_hash {
    if (req.http.X-UA-Device) {
        hash_data(req.http.X-UA-Device);
    }
}

Redirecting mobile clients


If you want to redirect mobile clients you can use the following snippet.

VCL:

sub vcl_recv {
    # call some detection engine

    if (req.http.X-UA-Device ~ "^mobile" || req.http.X-UA-device ~ "^tablet") {
        return(synth(750, "Moved Temporarily"));
    }
}

sub vcl_synth {
    if (obj.status == 750) {
        set obj.http.Location = "http://m.example.com" + req.url;
        set obj.status = 302;
        return(deliver);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • 1.介绍 运维日常: 2.Web Page Cache: varnish2.0,3.0处理过程 varnish4....
    尛尛大尹阅读 3,321评论 0 0
  • 缓存的基础知识 1、程序本身具有局部性 时间局部性过去访问到的数据,也有可能被两次访问 空间局部性一个数据被访问到...
    魏镇坪阅读 1,928评论 1 3
  • 头皮瘙痒常令人情绪非常烦躁,坐立不安。那么头皮为什么会痒呢? 小编百度了一下,得到了这样的答案:头皮瘙痒是头皮菌群...
    Extreme腻阅读 1,334评论 0 0
  • 一、我是莫语 我是莫语,一个身材还不错的女人。我爱跑步,爱写字,爱看电影,爱看书。经常晚上十一点前就睡觉了。 半年...
    周美妍阅读 946评论 6 39
  • Gentoo Linux改为采用滚动更新。Gentoo Linux的更新频密度可达到每周皆提供更新版。 升级系统的...
    孤逐王阅读 7,077评论 0 2