Varnish Cache 运维教程 / 第03章:VCL 语言基础
第03章:VCL 语言基础
3.1 VCL 简介
VCL(Varnish Configuration Language)是 Varnish 的专属配置语言。它不是简单的声明式配置,而是一种领域特定语言(Domain Specific Language, DSL),经过编译后执行。VCL 允许您精确控制 Varnish 处理每个请求的行为。
VCL 版本历史
| VCL 版本 | Varnish 版本 | 主要变化 |
|---|---|---|
| VCL 1.0 | 1.x | 初始版本 |
| VCL 2.0 | 2.x | 引入多个子程序 |
| VCL 3.0 | 3.x | 正则表达式改进 |
| VCL 4.0 | 4.0 | 重大语法重构 |
| VCL 4.1 | 4.1+ | 当前标准版本,改进 grace/keep 机制 |
VCL 处理模型
# 每个 VCL 文件必须声明版本
vcl 4.1;
VCL 代码被编译为 C 代码,然后编译为共享库(.so),由 Varnish 运行时加载:
VCL 文件 → C 代码 → .so 共享库 → 加载执行
3.2 基本语法
3.2.1 文件结构
# 文件开头必须声明 VCL 版本
vcl 4.1;
# 导入 VMOD 模块
import std;
# 定义后端服务器
backend default {
.host = "127.0.0.1";
.port = "8080";
}
# 定义 ACL(访问控制列表)
acl local {
"localhost";
"192.168.0.0"/24;
}
# 定义子程序
sub vcl_recv {
# 子程序内容
}
3.2.2 注释
# 单行注释(VCL 推荐使用 #)
// 另一种单行注释风格
/* 多行注释
可以跨越多行 */
3.2.3 语句与分号
sub vcl_recv {
# 每条语句以分号结尾
set req.http.X-Debug = "true";
# 条件语句不需要分号
if (req.url ~ "^/api/") {
return (pass);
}
# 最后一条语句前的分号可以省略(不推荐)
return (hash)
}
3.2.4 字符串
# 双引号字符串
set req.http.Host = "www.example.com";
# 字符串拼接(使用 + 运算符)
set req.http.X-Full-URL = "https://" + req.http.Host + req.url;
# 长字符串可以使用 {} 跨行
set req.http.X-Long = {This is a
multi-line string
that spans several lines};
# 特殊字符
set req.http.X-Quote = "He said \"Hello\"";
3.3 数据类型
3.3.1 基本数据类型
| 类型 | 说明 | 示例 |
|---|---|---|
| STRING | 字符串 | "hello", req.url |
| INT | 整数 | 42, obj.hits |
| REAL | 浮点数 | 3.14 |
| BOOL | 布尔值 | true, false |
| DURATION | 时间间隔 | 30s, 5m, 1h, 1d |
| BYTES | 字节大小 | 1k, 1m, 1g |
| TIME | 时间戳 | now |
| IP | IP 地址 | 192.168.1.1, ::1 |
| REGEX | 正则表达式 | "^/api/.*", "\.jpg$" |
| ACL | 访问控制列表 | local |
3.3.2 时间间隔(DURATION)
# 时间单位
sub vcl_recv {
# 秒:s
set req.http.X-Time1 = "30s";
# 分钟:m
set req.http.X-Time2 = "5m";
# 小时:h
set req.http.X-Time3 = "1h";
# 天:d
set req.http.X-Time4 = "1d";
# 周:w
set req.http.X-Time5 = "1w";
# 年:y
set req.http.X-Time6 = "1y";
}
sub vcl_backend_response {
# 设置 TTL
set beresp.ttl = 300s; # 5 分钟
set beresp.ttl = 1h; # 1 小时
set beresp.grace = 24h; # 24 小时
}
3.3.3 字节大小(BYTES)
# 字节单位
sub vcl_recv {
# 字节:无单位
set req.http.X-Size1 = "1024";
# 千字节:k
set req.http.X-Size2 = "1k"; # 1024 字节
# 兆字节:m
set req.http.X-Size3 = "1m"; # 1048576 字节
# 吉字节:g
set req.http.X-Size4 = "1g"; # 1073741824 字节
}
# 使用场景
sub vcl_backend_response {
# 限制缓存对象最大大小
if (beresp.body.bytes > 100m) {
return (abandon);
}
}
3.4 操作符
3.4.1 比较操作符
| 操作符 | 说明 | 示例 |
|---|---|---|
== | 等于 | req.method == "GET" |
!= | 不等于 | req.method != "POST" |
< | 小于 | obj.hits < 10 |
<= | 小于等于 | obj.hits <= 10 |
> | 大于 | obj.hits > 10 |
>= | 大于等于 | obj.hits >= 10 |
~ | 正则匹配 | req.url ~ "^/api/" |
!~ | 正则不匹配 | req.url !~ "\.css$" |
3.4.2 逻辑操作符
sub vcl_recv {
# AND(与)
if (req.method == "GET" && req.url ~ "^/api/") {
return (hash);
}
# OR(或)
if (req.url ~ "^/admin" || req.url ~ "^/login") {
return (pass);
}
# NOT(非)
if (!req.http.Cookie) {
return (hash);
}
# 复合条件
if ((req.method == "GET" || req.method == "HEAD")
&& req.url !~ "^/admin"
&& !req.http.Authorization) {
return (hash);
}
}
3.4.3 赋值操作符
sub vcl_recv {
# 基本赋值
set req.http.X-Request-ID = "12345";
# 数值赋值
set req.http.X-Timeout = "30s";
# 删除头部
unset req.http.Cookie;
unset req.http.X-Debug;
# 条件赋值(使用 if/else)
if (req.url ~ "^/api/") {
set req.http.X-Backend = "api";
} else {
set req.http.X-Backend = "web";
}
}
3.4.4 正则表达式
sub vcl_recv {
# 基本正则匹配
if (req.url ~ "^/products/[0-9]+$") {
# 匹配 /products/123 这样的 URL
set req.http.X-Product-ID = regsub(req.url, "^/products/([0-9]+)$", "\1");
}
# 常用正则模式
# 匹配静态资源
if (req.url ~ "\.(css|js|jpg|png|gif|ico|woff2|svg)$") {
set req.http.X-Static = "true";
}
# 匹配 API 路径
if (req.url ~ "^/api/v[0-9]+/") {
set req.http.X-API = "true";
}
# 不区分大小写匹配
if (req.url ~ "(?i)\.PDF$") {
set req.http.X-PDF = "true";
}
}
3.4.5 字符串操作
import std;
sub vcl_recv {
# regsub - 替换第一个匹配
set req.http.X-Path = regsub(req.url, "\?.*$", "");
# regsuball - 替换所有匹配
set req.http.X-Clean = regsuball(req.url, "[^a-zA-Z0-9/]", "_");
# 字符串长度
set req.http.X-URL-Length = std.integer(req.url, 0);
# 转小写
set req.http.X-Lower = std.tolower(req.http.Host);
# 转大写
set req.http.X-Upper = std.toupper(req.http.X-Method);
}
3.5 子程序(Subroutines)
3.5.1 子程序类型
VCL 有两种类型的子程序:
- 内置子程序(Built-in Subroutines):由 Varnish 在特定处理阶段自动调用
- 自定义子程序(Custom Subroutines):用户定义,通过
call语句调用
3.5.2 完整的请求生命周期子程序
客户端请求处理流程:
vcl_recv → 接收请求,决定处理策略
│
├── return(hash) → vcl_hash → vcl_hit/vcl_miss
│ │
│ ├── vcl_hit → vcl_deliver
│ └── vcl_miss → vcl_backend_fetch
│
├── return(pass) → vcl_pass → vcl_backend_fetch
│
├── return(pipe) → vcl_pipe
│
├── return(synth) → vcl_synth
│
└── return(purge) → vcl_purge
后端响应处理流程:
vcl_backend_fetch → 发起后端请求
│
├── return(deliver) → vcl_backend_response → vcl_deliver
│
├── return(retry) → 重试后端请求
│
├── return(error) → vcl_backend_error
│
└── return(abandon) → 放弃后端请求
响应发送流程:
vcl_deliver → 发送响应给客户端
│
├── return(deliver) → 完成响应发送
│
└── return(synth) → 生成合成响应
3.5.3 内置子程序详解
vcl_recv
请求到达时首先调用的子程序,用于决定请求的处理策略。
sub vcl_recv {
# 可用的返回动作:
# return (hash) - 进行缓存查找
# return (pass) - 直接传递到后端
# return (pipe) - 管道模式
# return (synth(status, reason)) - 合成响应
# return (purge) - 清除缓存
# return (restart) - 重新处理请求
# 常见处理逻辑
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
return (hash);
}
vcl_hash
用于计算缓存键。默认使用 URL 和 Host 头部。
sub vcl_hash {
# 默认会自动 hash 以下内容:
# hash_data(req.url);
# hash_data(req.http.host); 或 hash_data(server.ip);
# 可以添加额外的 hash 数据
if (req.http.Accept-Encoding ~ "gzip") {
hash_data("gzip");
}
# 添加设备类型到 hash
if (req.http.User-Agent ~ "Mobile") {
hash_data("mobile");
}
return (lookup);
}
vcl_hit
缓存命中时调用。
sub vcl_hit {
# 返回动作:
# return (deliver) - 发送缓存的响应
# return (synth) - 合成响应
# return (restart) - 重新处理
# return (pass) - 绕过缓存
# return (miss) - 当作缓存未命中
# 检查缓存对象是否过期
if (obj.ttl >= 0s) {
return (deliver);
}
# 对象已过期,检查是否有 grace 可用
if (obj.ttl + obj.grace > 0s) {
# 使用过期的缓存对象(grace 模式)
return (deliver);
}
return (miss);
}
vcl_miss
缓存未命中时调用。
sub vcl_miss {
# 返回动作:
# return (fetch) - 从后端获取
# return (synth) - 合成响应
# return (restart) - 重新处理
# return (pass) - 绕过缓存
return (fetch);
}
vcl_backend_fetch
发起后端请求前调用。
sub vcl_backend_fetch {
# 返回动作:
# return (fetch) - 发起后端请求
# return (error) - 返回错误
# return (abandon) - 放弃请求
# 修改后端请求
set bereq.http.X-Forwarded-For = client.ip;
return (fetch);
}
vcl_backend_response
收到后端响应后调用。
sub vcl_backend_response {
# 返回动作:
# return (deliver) - 缓存并发送响应
# return (retry) - 重试后端请求
# return (error) - 返回错误
# return (abandon) - 放弃响应
# 设置缓存时间
if (bereq.url ~ "\.(css|js)$") {
set beresp.ttl = 1h;
} else if (bereq.url ~ "\.(jpg|png|gif)$") {
set beresp.ttl = 7d;
} else {
set beresp.ttl = 5m;
}
# 不缓存错误响应
if (beresp.status >= 400) {
set beresp.ttl = 0s;
}
return (deliver);
}
vcl_deliver
发送响应给客户端前调用。
sub vcl_deliver {
# 返回动作:
# return (deliver) - 发送响应
# return (synth) - 合成响应
# 添加调试头部
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT (" + obj.hits + ")";
} else {
set resp.http.X-Cache = "MISS";
}
# 删除内部头部
unset resp.http.X-Powered-By;
unset resp.http.Server;
return (deliver);
}
vcl_synth
生成合成响应。
sub vcl_synth {
# 返回动作:
# return (deliver) - 发送合成响应
# return (restart) - 重新处理
# 自定义错误页面
if (resp.status == 750) {
# 自定义重定向
set resp.status = 301;
set resp.http.Location = "https://www.example.com" + resp.reason;
set resp.reason = "Moved";
return (deliver);
}
if (resp.status == 760) {
# 自定义错误页面
set resp.status = 503;
set resp.http.Content-Type = "text/html; charset=utf-8";
synthetic({"<!DOCTYPE html>
<html>
<head><title>503 Service Unavailable</title></head>
<body>
<h1>服务暂时不可用</h1>
<p>请稍后再试。</p>
</body>
</html>"});
return (deliver);
}
return (deliver);
}
vcl_pipe
管道模式,用于不支持缓存的协议(如 WebSocket)。
sub vcl_pipe {
# 返回动作:
# return (pipe) - 继续管道传输
# 注意:管道模式下 Varnish 只是转发字节流
# 需要设置正确的头部以支持 WebSocket
if (req.http.upgrade ~ "(?i)websocket") {
set req.http.Connection = "upgrade";
set req.http.Upgrade = "websocket";
}
return (pipe);
}
vcl_purge
处理缓存清除请求。
sub vcl_purge {
# 返回动作:
# return (synth(status, reason)) - 返回响应
# return (restart) - 重新处理
return (synth(200, "Purged"));
}
vcl_pass
直接传递请求到后端。
sub vcl_pass {
# 返回动作:
# return (fetch) - 获取后端响应
# return (synth) - 合成响应
# return (restart) - 重新处理
return (fetch);
}
3.5.4 自定义子程序
# 定义自定义子程序
sub check_auth {
# 检查认证
if (!req.http.Authorization) {
return (synth(401, "Unauthorized"));
}
}
sub normalize_url {
# URL 标准化
set req.url = regsub(req.url, "\#.*$", "");
set req.url = regsub(req.url, "\?.*$", "");
}
sub set_cache_policy {
# 根据 URL 设置缓存策略
if (req.url ~ "^/api/") {
set req.http.X-Cache-TTL = "60";
} else if (req.url ~ "\.(css|js)$") {
set req.http.X-Cache-TTL = "3600";
} else {
set req.http.X-Cache-TTL = "300";
}
}
# 在主子程序中调用自定义子程序
sub vcl_recv {
call normalize_url;
call set_cache_policy;
if (req.url ~ "^/admin") {
call check_auth;
}
return (hash);
}
3.6 内置变量
3.6.1 请求对象(req)
| 变量 | 类型 | 说明 | 可写 |
|---|---|---|---|
req.url | STRING | 请求 URL(不含 Host) | 是 |
req.http.* | STRING | 请求头部 | 是 |
req.method | STRING | 请求方法(GET/POST 等) | 是 |
req.proto | STRING | HTTP 协议版本 | 否 |
req.backend_hint | BACKEND | 后端服务器提示 | 是 |
req.hash_ignore_busy | BOOL | 忽略 busy 对象 | 是 |
req.hash_always_miss | BOOL | 强制缓存未命中 | 是 |
req.restarts | INT | 重启次数 | 否 |
req.storage | STEVEDORE | 存储引擎 | 是 |
3.6.2 后端请求对象(bereq)
| 变量 | 类型 | 说明 | 可写 |
|---|---|---|---|
bereq.url | STRING | 后端请求 URL | 是 |
bereq.http.* | STRING | 后端请求头部 | 是 |
bereq.method | STRING | 后端请求方法 | 是 |
bereq.backend | BACKEND | 当前后端服务器 | 否 |
bereq.connect_timeout | DURATION | 连接超时 | 是 |
bereq.first_byte_timeout | DURATION | 首字节超时 | 是 |
bereq.between_bytes_timeout | DURATION | 字节间超时 | 是 |
3.6.3 后端响应对象(beresp)
| 变量 | 类型 | 说明 | 可写 |
|---|---|---|---|
beresp.status | INT | 响应状态码 | 是 |
beresp.reason | STRING | 响应原因短语 | 是 |
beresp.http.* | STRING | 响应头部 | 是 |
beresp.do_esi | BOOL | 启用 ESI 处理 | 是 |
beresp.do_gzip | BOOL | 启用 Gzip 压缩 | 是 |
beresp.do_gunzip | BOOL | 启用 Gzip 解压 | 是 |
beresp.do_stream | BOOL | 启用流式传输 | 是 |
beresp.ttl | DURATION | 缓存 TTL | 是 |
beresp.grace | DURATION | Grace 时长 | 是 |
beresp.keep | DURATION | Keep 时长 | 是 |
beresp.age | DURATION | 对象已存在时长 | 否 |
beresp.backend | BACKEND | 后端服务器 | 否 |
beresp.uncacheable | BOOL | 是否不可缓存 | 是 |
beresp.was_304 | BOOL | 是否为 304 响应 | 否 |
3.6.4 缓存对象(obj)
| 变量 | 类型 | 说明 | 可写 |
|---|---|---|---|
obj.status | INT | 响应状态码 | 否 |
obj.reason | STRING | 响应原因 | 否 |
obj.hits | INT | 缓存命中次数 | 否 |
obj.ttl | DURATION | 剩余 TTL | 是 |
obj.grace | DURATION | 剩余 Grace | 是 |
obj.age | DURATION | 对象年龄 | 否 |
obj.http.* | STRING | 响应头部 | 是 |
obj.uncacheable | BOOL | 是否不可缓存 | 否 |
obj.storage | STEVEDORE | 存储引擎 | 否 |
3.6.5 响应对象(resp)
| 变量 | 类型 | 说明 | 可写 |
|---|---|---|---|
resp.status | INT | 响应状态码 | 是 |
resp.reason | STRING | 响应原因 | 是 |
resp.http.* | STRING | 响应头部 | 是 |
resp.is_streaming | BOOL | 是否流式传输 | 否 |
3.6.6 服务器对象(server)
| 变量 | 类型 | 说明 |
|---|---|---|
server.identity | STRING | 服务器标识 |
server.hostname | STRING | 服务器主机名 |
server.ip | IP | 服务器 IP |
server.port | INT | 服务器端口 |
3.6.7 客户端对象(client)
| 变量 | 类型 | 说明 |
|---|---|---|
client.ip | IP | 客户端 IP 地址 |
client.identity | STRING | 客户端标识 |
3.6.8 时间相关变量
| 变量 | 类型 | 说明 |
|---|---|---|
now | TIME | 当前时间 |
sub vcl_recv {
# 使用时间变量
if (now.hour >= 22 || now.hour < 6) {
# 夜间模式
set req.http.X-Night-Mode = "true";
}
# 基于日期的条件
if (now.date == "2026-01-01") {
# 特殊日期处理
set req.http.X-Holiday = "true";
}
}
3.7 返回动作汇总
3.7.1 vcl_recv 可用返回动作
| 返回动作 | 说明 | 后续子程序 |
|---|---|---|
hash | 进行缓存查找 | vcl_hash |
pass | 绕过缓存 | vcl_pass |
pipe | 管道模式 | vcl_pipe |
synth(status, reason) | 合成响应 | vcl_synth |
purge | 清除缓存 | vcl_purge |
restart | 重新处理 | vcl_recv |
3.7.2 返回动作速查表
| 子程序 | 可用返回动作 |
|---|---|
| vcl_recv | hash, pass, pipe, synth, purge, restart |
| vcl_hash | lookup |
| vcl_hit | deliver, miss, pass, synth, restart |
| vcl_miss | fetch, synth, restart |
| vcl_pass | fetch, synth, restart |
| vcl_pipe | pipe |
| vcl_backend_fetch | fetch, error, abandon |
| vcl_backend_response | deliver, retry, error, abandon |
| vcl_backend_error | deliver, retry |
| vcl_deliver | deliver, synth, restart |
| vcl_synth | deliver, restart |
| vcl_purge | synth, restart |
3.8 条件语句
3.8.1 if-else 语句
sub vcl_recv {
# 基本 if 语句
if (req.method == "GET") {
return (hash);
}
# if-else 语句
if (req.url ~ "^/api/") {
set req.http.X-Backend = "api";
} else {
set req.http.X-Backend = "web";
}
# if-elseif-else 语句
if (req.url ~ "^/api/v1/") {
set req.http.X-API-Version = "1";
} elseif (req.url ~ "^/api/v2/") {
set req.http.X-API-Version = "2";
} else {
set req.http.X-API-Version = "unknown";
}
}
3.8.2 嵌套条件
sub vcl_recv {
if (req.method == "GET") {
if (req.url ~ "^/products/") {
if (req.http.Cookie ~ "session=") {
# 有 session 的产品页请求
return (pass);
} else {
# 无 session 的产品页请求
return (hash);
}
} else {
return (hash);
}
} else {
return (pass);
}
}
3.9 注意事项
重要
- VCL 文件必须以
vcl 4.1;开头- 每个内置子程序必须有明确的
return语句- VCL 中不能使用循环(for/while),这是设计决策
- 字符串拼接使用
+运算符,不是模板字符串- 正则匹配使用
~操作符,捕获组使用\1,\2引用- VCL 变量的作用域限于当前请求的处理流程
set和unset操作不可撤销
3.10 业务场景
场景一:多条件路由
sub vcl_recv {
# 根据多种条件路由到不同后端
if (req.http.Host ~ "^api\.") {
set req.backend_hint = api_backend;
} elseif (req.http.Host ~ "^static\.") {
set req.backend_hint = static_backend;
} elseif (req.url ~ "^/admin") {
set req.backend_hint = admin_backend;
call check_admin_auth;
} else {
set req.backend_hint = web_backend;
}
}
场景二:URL 标准化
sub vcl_recv {
# 移除尾部斜杠
if (req.url != "/" && req.url ~ "/$") {
set req.url = regsub(req.url, "/+$", "");
}
# 统一转小写(Host)
set req.http.Host = std.tolower(req.http.Host);
# 移除默认端口
set req.http.Host = regsub(req.http.Host, ":(80|443)$", "");
# 重定向带 www 到不带 www
if (req.http.Host ~ "^www\.(.+)$") {
return (synth(750, regsub(req.http.Host, "^www\.(.+)$", "https://\1" + req.url)));
}
}