强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第03章:VCL 语言基础

第03章:VCL 语言基础

3.1 VCL 简介

VCL(Varnish Configuration Language)是 Varnish 的专属配置语言。它不是简单的声明式配置,而是一种领域特定语言(Domain Specific Language, DSL),经过编译后执行。VCL 允许您精确控制 Varnish 处理每个请求的行为。

VCL 版本历史

VCL 版本Varnish 版本主要变化
VCL 1.01.x初始版本
VCL 2.02.x引入多个子程序
VCL 3.03.x正则表达式改进
VCL 4.04.0重大语法重构
VCL 4.14.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
IPIP 地址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 有两种类型的子程序:

  1. 内置子程序(Built-in Subroutines):由 Varnish 在特定处理阶段自动调用
  2. 自定义子程序(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.urlSTRING请求 URL(不含 Host)
req.http.*STRING请求头部
req.methodSTRING请求方法(GET/POST 等)
req.protoSTRINGHTTP 协议版本
req.backend_hintBACKEND后端服务器提示
req.hash_ignore_busyBOOL忽略 busy 对象
req.hash_always_missBOOL强制缓存未命中
req.restartsINT重启次数
req.storageSTEVEDORE存储引擎

3.6.2 后端请求对象(bereq)

变量类型说明可写
bereq.urlSTRING后端请求 URL
bereq.http.*STRING后端请求头部
bereq.methodSTRING后端请求方法
bereq.backendBACKEND当前后端服务器
bereq.connect_timeoutDURATION连接超时
bereq.first_byte_timeoutDURATION首字节超时
bereq.between_bytes_timeoutDURATION字节间超时

3.6.3 后端响应对象(beresp)

变量类型说明可写
beresp.statusINT响应状态码
beresp.reasonSTRING响应原因短语
beresp.http.*STRING响应头部
beresp.do_esiBOOL启用 ESI 处理
beresp.do_gzipBOOL启用 Gzip 压缩
beresp.do_gunzipBOOL启用 Gzip 解压
beresp.do_streamBOOL启用流式传输
beresp.ttlDURATION缓存 TTL
beresp.graceDURATIONGrace 时长
beresp.keepDURATIONKeep 时长
beresp.ageDURATION对象已存在时长
beresp.backendBACKEND后端服务器
beresp.uncacheableBOOL是否不可缓存
beresp.was_304BOOL是否为 304 响应

3.6.4 缓存对象(obj)

变量类型说明可写
obj.statusINT响应状态码
obj.reasonSTRING响应原因
obj.hitsINT缓存命中次数
obj.ttlDURATION剩余 TTL
obj.graceDURATION剩余 Grace
obj.ageDURATION对象年龄
obj.http.*STRING响应头部
obj.uncacheableBOOL是否不可缓存
obj.storageSTEVEDORE存储引擎

3.6.5 响应对象(resp)

变量类型说明可写
resp.statusINT响应状态码
resp.reasonSTRING响应原因
resp.http.*STRING响应头部
resp.is_streamingBOOL是否流式传输

3.6.6 服务器对象(server)

变量类型说明
server.identitySTRING服务器标识
server.hostnameSTRING服务器主机名
server.ipIP服务器 IP
server.portINT服务器端口

3.6.7 客户端对象(client)

变量类型说明
client.ipIP客户端 IP 地址
client.identitySTRING客户端标识

3.6.8 时间相关变量

变量类型说明
nowTIME当前时间
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_recvhash, pass, pipe, synth, purge, restart
vcl_hashlookup
vcl_hitdeliver, miss, pass, synth, restart
vcl_missfetch, synth, restart
vcl_passfetch, synth, restart
vcl_pipepipe
vcl_backend_fetchfetch, error, abandon
vcl_backend_responsedeliver, retry, error, abandon
vcl_backend_errordeliver, retry
vcl_deliverdeliver, synth, restart
vcl_synthdeliver, restart
vcl_purgesynth, 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 注意事项

重要

  1. VCL 文件必须以 vcl 4.1; 开头
  2. 每个内置子程序必须有明确的 return 语句
  3. VCL 中不能使用循环(for/while),这是设计决策
  4. 字符串拼接使用 + 运算符,不是模板字符串
  5. 正则匹配使用 ~ 操作符,捕获组使用 \1, \2 引用
  6. VCL 变量的作用域限于当前请求的处理流程
  7. setunset 操作不可撤销

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)));
    }
}

3.11 扩展阅读